// integrate

Nine lines, one address.

The typed interface is published so reads are checked when your contract is built rather than when it runs. A version bump never changes an existing field, so a consumer written against version 1 keeps reading a correct answer out of a version 2 record.

the interface

@gl.contract_interface
class VouchsafeIface:
    class View:
        def read(self, who: str) -> dict: ...
        def read_strict(self, who: str) -> dict: ...
        def qualifies(self, who: str, min_settled: int,
                      max_failed: int, min_age_days: int,
                      strict: bool) -> dict: ...
        def reporter_quality(self, reporter: str) -> dict: ...
        def was_reported(self, reporter: str,
                         report_id: str) -> dict: ...

    class Write:
        def report(self, subject: str, report_id: str,
                   evidence: str) -> str: ...
        def register_consumer(self, name: str) -> None: ...
Read the criteria

read cost

read
four counters, fixed cost
read_strict
the same, minus entries under appeal - also fixed
tail length
capped at 12 forever
who pays
the calling contract

Every read happens inside somebody else’s transaction, so the cost of reading is a cost they pay. That single fact decides how entries age out - and it is why neither published read iterates over anything. The one view that walks the tail is entries, which this website calls and no consuming contract needs.

The read cost here is unmeasured. GENLAYER STUDIO is gasless, so a receipt from it says nothing about whether this read fits in a caller’s budget on a live network. That measurement is a launch blocker for this product, not a nice-to-have.

reading, in your own contract

def _ratio_for(self, borrower: str) -> tuple[int, str]:
    # view() is synchronous: the answer arrives inside THIS transaction,
    # before any collateral is locked. That is the whole product.
    verdict = VouchsafeIface(self.vouchsafe).view().qualifies(
        borrower,
        MIN_SETTLED,   # settled dealings this market wants to see
        0,             # failures it tolerates
        MIN_AGE_DAYS,  # days of record it wants behind them
        True,          # strict: a failure under appeal is not counted here
    )
    if verdict["passes"]:
        return EARNED_RATIO, str(verdict["reason"])   # 120%, earned
    return BASE_RATIO, str(verdict["reason"])         # 150%, the stranger price
deployed at 0xCbD5…0B
The reference market has no custody. open_position is payable and nothing pays back out - closing a position settles it and reports the outcome, and never returns the collateral. Repayment and liquidation are a lending market’s problem, not this file’s, and building half of them would bury the nine lines worth copying. Safe on a gasless network, and nowhere else.

reporting back

@gl.public.view
def vouchsafe_ack(self) -> bool:
    # Vouchsafe calls this before it accepts anything from you.
    # An account cannot answer it. That is the entire check.
    return True

# ... then, when a dealing finishes:
gl.get_contract_at(VOUCHSAFE).emit(on="finalized").report(
    borrower,                     # the subject of the dealing
    f"pos-{borrower}-{n}",        # your own id, deduplicated per reporter
    evidence,                     # what happened, in your own words
)

Before it accepts anything from you, report calls vouchsafe_ack back on your contract. An account has no code and cannot answer, so a person cannot type a report into a form - and a contract cannot become a reporter by accident. Without that rule the record is a review site, and review sites get brigaded within a week.

The obvious version of this check does not work, and it is worth knowing why before you write your own: a cross-contract write is an emitted message, an emitted message is its own transaction, and its origin is the emitting contract. So sender == origin for your report exactly as it does for a person’s direct call.

field / meaning

knownfalse for an address with no record
unresolvedevidence did not decide
completeddealings that finished as agreed
first_seenage of the record, not of the address
latefinished, but not on time
settledcompleted, late and failed together
faileddid not finish
age_dayswhole days since the record was created
contestedhow many failed entries are under appeal
weightedsummed weight of settled dealings: 1, 3 or 5 each, for the size the network agreed on - moves with settled, so read_strict drops the weight of an appealed failure too
totalevery entry ever, including ones that aged out
tiernew, building, established or mixed - presentation, not a score
self_readthe subject is in this call's own view-call stack
versionthe interface version this record answers with
Unknown must read as neutral, never as bad. Unresolved is not a failure. Age belongs beside any count you show a user.

who can change what

the owner can
  • stop new reports - set_paused
  • change the appeal bond - set_contest_bond
  • change the cooling period - set_cooling
  • hand the role to another address - transfer_ownership
the owner cannot
  • stop a read. The pause reaches writes only, so your transaction never fails because somebody pressed a button
  • change a counter, an entry, an outcome or a weight. There is no method that does it
  • delete a record, or edit one
  • rewrite the criteria. They are constants in the contract, so changing them means a redeploy at a new address
owner
0x3e1D268c8B1Ba7d042968ab713467C5631831513
reports
open
reads
always open

Worth checking rather than believing: the administration section of contracts/vouchsafe.py is four methods long, and none of them touches a record.

from typescript

import { createVouchsafe } from "vouchsafe-adapter";

const vouchsafe = createVouchsafe({ address: VOUCHSAFE });

// the same two view methods a consuming contract calls
const rec = await vouchsafe.readStrict(borrower);

if (rec.known && rec.settled >= 10 && rec.failed === 0) {
  // ...and the same numbers that contract just saw
}

The adapter is a thin wrapper over the same two view methods, so the app and the consuming contract show the same numbers. It computes nothing the contract does not - tier included, which is why that lives on chain rather than in the package.

what you need

record
0x95DB78691cD6E31D99ca1FfDe129d3F3CE4114A5
network
GENLAYER STUDIO
test address
nothing reported yet
adapter
packages/vouchsafe-adapterin the repo, not yet on npm
interface version
2
criteria version
v1.3
● liveread from 0x95DB…A5on GENLAYER STUDIOexplorer