Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Exact-payload approval: financial intent as immutable revisions

From cognokratos/tauros-revenue · docs/concepts/exact-payload-approval.md · pinned revision facbbc927eb4

A human approval authorizes one exact, immutable financial payload, never "whatever the invoice currently contains".

The failure this prevents

The naive design stores an invoice as one mutable row with an approved flag:

agent creates invoice (1,200 USDC to 0xAAA…)
human approves                       ← approved = true
agent edits destination to 0xBBB…    ← still approved = true
system pays 0xBBB…

Patches such as "clear the flag on every edit" depend on every code path remembering to clear it, including future ones and ones written by someone else. The flaw is that the thing approved and the thing executed are not the same object.

The model

Tauros never edits financial content in place:

Invoice  (identity, owner, lifecycle)
  ├── Revision 1   payload ─sha256→ e6c1…   decision: changes_requested
  ├── Revision 2   payload ─sha256→ b0ed…   decision: approved   ← what is authorized
  └── (a Revision 3 would need its own decision)
  • InvoiceRevision is append-only. No update or destroy action exists, and only an Invoice action can create one.
  • Each revision is sealed when created: its financial payload is written in a canonical form, stored (canonical_payload), and hashed (payload_hash).
  • Approval names a revision and its hash. A revision gets at most one decision (a unique index on revision_id).
  • Changing anything (an amount, the destination, the due date) means a new revision with a new hash, which is undecided.

What is in the payload

Everything that decides who pays what, where and when; nothing that is only presentation or record metadata. The full table, with reasons, is in DOMAIN_MODEL.md and in Tauros.Revenue.FinancialPayload.

Two design choices are worth arguing about:

  • The reasoning is excluded. It explains the intent but is not the intent. A retry that words its explanation differently is still the same proposal (see idempotency).
  • The destination's address is included even though destinations are immutable. The payload should describe the payment on its own, without trusting that another table never changes.

Canonicalization

Two payloads that mean the same must produce the same bytes:

PitfallCanonical rule
map key orderkeys sorted at every level
"1.50" vs "1.5" vs "15E-1"decimals normalized
é composed vs decomposedtext in Unicode NFC
whitespacenone
a future layouta schema version tag inside the payload

Line order is kept: an invoice is an ordered document, and reordering it is a different document.

test/tauros/revenue/financial_payload_test.exs proves both halves: the same intent always hashes the same, and every material change hashes differently.

Approving

Invoice.approve requires revision_id and payload_hash: the approver states what they saw. Inside one transaction, with the invoice row locked, Invoice.Changes.Decide checks:

CheckFailure
this exact decision was already made by this approversuccess, nothing written (a retry)
any other decision exists for the revision409 already_decided
the state machine allows the move from the current state409 invalid_transition
the revision is the invoice's current one409 stale_revision
the hash is that revision's hash409 payload_mismatch
re-sealing the stored fields reproduces the stored hash409 payload_integrity
to approve: the destination (locked) is still active409 destination_inactive

The payload_integrity check means a revision altered directly in the database is never approved: its hash no longer matches its contents. (Making the tables append-only at the database level is Epic 5.)

Concurrency

SituationOutcome
the approver double-clicks, or retries after a timeoutone approval; both calls succeed
two tabs: one approves, the other rejectsthe first to lock the invoice wins; the other gets already_decided
the agent revises while the human is reviewingthe human's approval names the old revision: stale_revision; they review the new one
the destination is deactivated while approval is pendingapproval fails (destination_inactive); the human requests changes
the destination is deactivated after approvalthe approval stands (history is not rewritten); issuing (Epic 6) must re-check

Row locks serialize commands on one invoice; the unique index on approvals is the backstop if anything ever bypassed them.

In the UI

The review screen (Needs review) names who proposed and who decides, and shows the payload in plain language ("Acme Inc owes 1200.00 USDC, payable on Arbitrum One to 0x…, due 2026-11-04"), the lines, the destination's state, the revision number, the hash and the canonical bytes. The decision form carries the revision_id and payload_hash that were on screen. If anything changed, the human is told and shown the new revision.

This chapter is maintained in cognokratos/tauros-revenue beside the code it teaches. The book shows docs/concepts/exact-payload-approval.md at revision facbbc927eb4b9090937524ca02486c026f1f025 (branch main). View source at this revision · Report a correction.

Corrections are made upstream against the current main branch and appear here when the book's pin for this source is updated.