Skip to content
VOUCHVOUCHDocs

Guides

Exit certificates

For a swap rail, an exchange or a desk that receives shielded ZEC: ask for a certificate, check it under your own policy, and match the deposit when it lands. Vouch returns evidence; you decide.

On this page

#The flow

An exit, from request to releaseRailswap · exchange · deskHolderpof-proveVerify APIpof-attest rail APIZcashlightwalletdrequest (kind exit)X · period · unmoved since H · intentprove --exitcircuit 2 · names the notescertificate.pofPOST /v1/certificatesverify under policyperiod · H · reuse registrypre-cleared + webhookdeposit txspends exactly the certified notesPOST …/match { txid }GetTransactionthe deposit’s nullifiersexit_matchesevery spend certified?matched | mismatch + webhookThe rail releases or holds the deposit under its own policy
The certificate is checked before the deposit is sent, and matched when it lands. The service returns evidence; the rail decides what to release.

Two checks, at two moments. Before the deposit: is the certificate valid under your policy (amount, period, unmoved-since block, no reuse)? After the deposit lands: does the transaction spend exactly the certified notes? The first gives you a pre-cleared deposit to expect; the second ties that deposit to the evidence.

#Choose a policy

KnobWhat it buys youTypical choice
ThresholdCovers the deposit you expectThe quote amount, rounded up to 0.125 ZEC
Unmoved since HThe coins predate an incident and have not moved sinceA block before the incident you care about
Max anchor ageThe “not moved” fact is recentSmall: a few hundred blocks
Period (epoch)The same coins certified twice to you show as AlreadyUsedThis month
IntentThe certificate is for this deposit onlyYour quote or deposit id

#1 · Ask

Build a request at /request with Exit certificate selected, and hand the link to the holder with the deposit address.

JSON
{ "v": 1, "id": "c1ea4e7e5a1f0002", "claim": "HoldsAtLeast", "zatoshi": "100000000",
  "audience": "rail:example", "expiryDays": 7, "kind": "exit", "epoch": 202610, "dormantSince": 4410000 }

The holder runs pof-prove prove --request …. An exit certificate names every usable note (up to five), bound to the request id as the deposit intent.

#2 · Check the certificate

Shell
pof-verify check certificate.pof --audience rail:example --anchors anchors.json \
  --epoch 202610 --dormant-since 4410000 --seen your-registry.json

Valid means: at least that much, in notes unmoved since the block, made for you in this period, and none of these notes already used with you this period. Record the result’s tags in your registry. Or run the same check in the browser at /rail, or through the API:

HTTP
POST /v1/certificates   { "certificate": "<base64url>", "audience": "rail:example", "epoch": 202610,
                          "dormantSince": 4410000, "intent": "c1ea4e7e5a1f0002" }
→ 200 { "status": "pre-cleared", "certificate": { "id": "…", "revealed": [ … ], "tags": [ … ] }, "result": { … } }
→ 422 { "status": "refused", "result": { … } }      (with a reason: not a certificate, another intent, …)

#3 · Match the deposit

Matching a deposit to a certificateCertificate namesnf₁nf₂Deposit Anf₁nf₂Deposit B · with a dummy spendnf₁nf₂nf ?Matchedevery spent note was certifiedMismatchone nullifier was never named
Matched means every nullifier the deposit reveals is one the certificate named. A dummy spend reveals a nullifier that looks exactly like an uncertified note, so exit transactions must be built without them.
Shell
pof-anchor tx --network testnet --txid <deposit txid> > deposit.json
pof-verify match --cert certificate.pof --deposit deposit.json        # exit 0: matched

POST /v1/certificates/{id}/match   { "txid": "<deposit txid>" }
→ { "status": "matched" | "mismatch", "certificate": { "deposit": { "result": { "kind": "Matched", "spent": 2 } } } }

Matched means every note the deposit spends is one the certificate named. Value inside one transaction is pooled, so a single uncertified spend makes the whole deposit a mismatch.

#The real testnet exit

The first attempt gave Mismatch: the wallet split change into four outputs, so the bundle carried three dummy spends whose nullifiers looked like uncertified notes. It is kept in fixtures/testnet-exit-first-attempt.* as a documented finding.

The clean run consolidated the notes first, certified both at anchor 4,460,391 (fixtures/proofs/testnet-exit.pof), then sent deposit ff0a2488543a58b852b22c426313ea00c00de1e79a6fa1710dc3cbae69ab256a, spending exactly those notes into two outputs: Matched. Try both on /rail.

#Webhooks and the store

EventWhen
certificate.preclearedA certificate passed your policy.
deposit.matchedThe deposit spends only certified notes.
deposit.mismatchThe deposit spends a note the certificate did not name.

Bodies are { event, certificate, at }, signed with X-Vouch-Signature: sha256=<hmac> under POF_WEBHOOK_SECRET. Set POF_RAIL_TOKEN to require a bearer token on every write, and POF_STORE to persist the registry. The store holds ids, tags, revealed nullifiers and verdicts, never proof bytes.

#What it does not say

Unmoved since block H says when the coins last moved, not where they were before H. Coins stolen long before H and left untouched would pass, which is why H is your policy’s choice. It is evidence for your risk process, never a “clean” stamp, and not a legal answer to any regulation.

You see the certified notes’ nullifiers before the deposit lands, so you could see if the holder spent them elsewhere instead. Holders are told to make certificates only when they are ready to send.