Skip to content

Completeness: matching the provider's records against the receipts ​

A verified receipt proves what went through the gateway. Nothing on the gateway's side can see an action that never went through it: whoever holds the Stripe key can issue a refund straight through Stripe's API. The provider's own record is the only place that action shows. agent-custody completeness lets an auditor check it, with a read-only key of their own.

What it reports ​

For a window and a set of tools, it lists the provider's objects and matches each to a verified receipt by the provider's object id, which the receipt's result carries.

sectionmeaningwhat to do
provider actions with no receiptan object the provider created in the window that no verified receipt namesthe finding this command exists for: an action taken outside the gateway, or by a gateway whose receipts you were not given
receipts the provider does not havean executed receipt in the window whose object the provider has no record ofa receipt for something that did not happen at the provider, or an object in another account or mode
mismatchesa matched pair whose amount, currency (ignoring case), or a beneficiary field differsthe receipt and the provider disagree about what happened
one provider object, several receiptsmore than one verified receipt names the same provider object; each is still compared with itone action recorded twice, or receipts that contradict each other
forwarded without an outcome receipta call committed to the log before it was forwarded (precommit) whose outcome receipt is missingthe gateway stopped after forwarding; this explains a provider object that has no receipt, without guessing which one
unmatchablea verified, executed receipt whose provider id cannot be read, or whose amount, currency, or beneficiary argument cannot be compared, because a capture rule withholds itmatch it by hand against the values disclosed with agent-custody reveal
not verifieda receipt file that failed verification, or a valid receipt or authorization stored under a name that is not its signed receipt idit is not matched, so its provider object shows as having no receipt

The result is COMPLETE only when every section but the matches is empty, and the command exits 1 otherwise.

Only receipts that verify against the keys you give are matched. A forged or edited receipt file cannot cover for a provider action, because it fails verification and its object then appears as having no receipt. Denied, withheld, and failed calls are not expected to have a provider object, and reconciliation receipts are skipped, because the original receipt is the one that names the object.

The provider is listed fifteen minutes either side of the window (marginMs in code), so a receipt signed just inside the window still finds an object created just outside it. The margin does not widen the period reported on: a pair is reported only when its receipt or its object falls inside the window, and a call forwarded without an outcome only when its authorization was signed inside it.

An authorization is tied to its outcome by the receipt id both of them sign, never by the file name. A valid receipt copied under the name of a missing outcome therefore hides nothing: it is reported as not verified, and the call is still listed as forwarded without an outcome.

Running it ​

bash
export STRIPE_AUDIT_KEY=rk_live_...   # a restricted key with read access to refunds, held by the auditor
agent-custody completeness --receipts ./receipts --issuer-key gateway.pub --principal-key principal.pub \
  --log log.jsonl --provider stripe --object refunds --key-env STRIPE_AUDIT_KEY \
  --tool stripe.refund --since 2026-10-01T00:00:00Z --until 2026-11-01T00:00:00Z
flagdefaultmeaning
--receipts <dir>the auditor's copy of the receipts directory, authorization files included
--issuer-key, --principal-key, --approval-key, --log-key, --log, --log-idhow each receipt is verified, as for verify
--provider stripethe provider; Stripe is the only one so far
--objectrefundsrefunds, payouts, or transfers
--key-env NAMEthe environment variable holding the read-only key; never a flag value
--tool <name>each tool whose executed calls create provider objects; repeatable
--since, --untilthe window, ISO times; --until is exclusive
--id-pathidwhere the provider's object id sits in the JSON of the result's first text item; Stripe's own response uses id
--amount-field, --currency-fieldamount, currencythe call arguments compared with the provider's amount and currency
--beneficiary <arg>=<field>a call argument that must equal a provider field, such as charge=charge or customer_id=metadata.customer_id; repeatable
--jsonthe full report as JSON

Use a restricted Stripe key with read-only access to the objects you check. The command only lists objects: it never creates, retries, or reconciles anything. The operator's reconcile command is a different thing: it settles one financial operation through the gateway and writes a receipt.

Limits ​

  • The receipts you were given. The check proves completeness against those receipts. A second gateway, or a second receipts directory, that issued receipts you were not handed shows up as provider actions with no receipt, which is the correct finding.
  • One provider account and mode per run. Test-mode and live-mode objects are separate lists.
  • The id must be readable. A capture rule that withholds the result content also withholds the object id inside it, and such receipts are reported as unmatchable, never as matched or missing. A rule that keeps content in clear (result: { mode: "digest", clear: ["content"] }) leaves the id readable. Likewise, keep the amount, currency, and beneficiary arguments in clear, or those comparisons are reported as unmatchable. Reading values from disclosures is still to come.
  • Stripe first. Other providers, and on-chain transfers from an agent's wallet, are ProviderSource implementations still to come.

In code, checkCompleteness({ receiptsDir, verify, source, tools, sinceMs, untilMs }) returns the report, stripeSource({ secretKey, object }) lists Stripe objects, and formatCompleteness(report) prints it. Example 35 runs the whole check against a stand-in for Stripe's list API.