The ledger
One row per decision, allows included, each carrying the hash of the row before it.
The ledger
Every verdict writes one row, allows included. That row is what an auditor reads, and it is the reason the gate is worth installing rather than just useful.
A row holds the time, the session, the tool, the contract and the specific check inside it, the verdict, the effect the mode gave it, the reason in a sentence, the permission mode the session was running under, and a summary of what was attempted.
{
"id": 12,
"ts": "2026-08-24T06:00:41Z",
"session_id": "b2",
"mandate_id": "coco.x402.payments",
"tool": "payment.send",
"family": "payment",
"pack_id": "x402.offer_freshness",
"check_id": "offer_is_still_valid",
"verdict": "BLOCK",
"effect": "deny",
"reason": "The x402 offer has expired. An approval cannot resume a stale signature, so start a fresh authorisation and the gate evaluates it again",
"permission_mode": "default",
"gate_mode": "enforce",
"context_summary": "10.0 USD to acme",
"prev_hash": "sha256:a6748ecc95819afb...",
"content_hash": "sha256:5b5500634225e9be..."
}Verdict and effect are different fields
The verdict is what the contract produced. The effect is what happened to the call, and only the effect changes with the mode.
That split is what makes an observe-mode ledger a truthful account of what enforce
would have done. A row can say verdict: BLOCK and effect: observed, and
coco report reads exactly those rows.
The chain
Each receipt carries the hash of the one before it, and the chain is written inside a transaction, so two parallel tool calls produce two links rather than a fork.
coco verifyChain verified. 3 receipts, unbroken.coco verify names the first row that does not verify. It checks three things.
The chain itself, the content hash of each receipt, and whether the queryable
columns still say what the hashed receipt says. Rewriting a column is the cheapest
way to alter a record, so it is checked rather than assumed.
What a payment receipt carries
A payment verdict has to be replayable against the state it judged, so the ledger snapshot the runtime supplied is recorded on the receipt rather than remembered. The payment itself is recorded structured, because an auditor asks for the merchant and the amount, not for a summary string.
What the receipts are not
The receipts are not cryptographically signed. They are tamper-evident on one machine, and they would not survive an operator with write access and patience. Signing and external anchoring are the next step, and calling the current state signed would be wrong.
There is a second gap worth naming. The receipt is written by the check itself, so a call that timed out leaves no row at all. The ledger has a hole rather than a wrong answer, and that is true in observe mode as much as in enforce.
Over HTTP there is a third. A verdict the client produced because the gate did not
answer is marked unreached, and no receipt exists for it, because the request
never arrived. That gap is the cost of checking over a network.
Reading it
coco status # mandate, mode, contracts, decision counts
coco ledger # recent decisions
coco ledger --verdict BLOCK # only what was stopped
coco ledger --limit 200 --json # for anything downstream
coco report # what enforce would have stopped, grouped by rule
coco escalations # the queue waiting on youOver HTTP the same rows come back as JSON.
curl -s "$GATE/receipts?limit=50" -H "Authorization: Bearer $COCO_API_KEY"
curl -s "$GATE/ledger/verify" -H "Authorization: Bearer $COCO_API_KEY"There is no daemon and no service to keep running for the local install. The ledger is a SQLite file on the same machine as the gate.
Next
- Reading the ledger for the day-to-day
- Escalations for the queue and the decisions on it