Three operations. Each has one owner, one input and one output. Everything vry:st does is a composition of these, and nothing in the protocol requires a fourth.
Owner: the issuer. Input: the facts it verified about you. Output: a signed credential, bound to your wallet, encrypted on your device.
Attestation is off-chain and free. The issuer never learns where or when you use the credential afterward. If the issuer later revokes it, the next proof will fail; existing receipts are untouched.
await vryst.attest({
issuer: "0xKYC…",
schema: "residency/v1"
});
// → { credentialId: "cred_8a1…" }
Owner: the holder. Input: a published requirement (claim, accepted issuers, max TTL, scope policy). Output: a zero-knowledge proof.
The proof shows that a credential from some accepted issuer satisfies the claim and is not revoked. It does not show which issuer, which credential, or any field value. Proving runs in WebAssembly on the device; typical time is under a second.
const proof = await vryst.prove({
claim: "jurisdiction in permitted_set and age >= 18",
reveal: [] // keep it empty
});
Owner: the holder (via an optional relayer). Input: the proof, a TTL, a scope. Output: a receipt — claim id, result, scope, expiry, nullifier.
A receipt has no owner field. It is findable by the verifier it was scoped to and linkable to you only through a nullifier that verifier and you can compute. Using a relayer keeps your gas-paying address out of the record entirely.
const receipt = await vryst.settle(proof, {
ttl: "30d",
scope: "0xMarket…",
relayer: "default"
});
// → vry:st/0x9f2a…c41e
One credential answers as many questions, at as many venues, as you like. Nothing is re-uploaded.
Same claim, different venue, different nullifier. Two verifiers cannot join their records.
Receipts stop existing. Proving again is cheaper than remembering.