Repository navigation
[Proposal] On-chain settlement-side mandate binding for AP2 #255
Description
On-chain settlement-side mandate binding for AP2
Acknowledgment of repo scope
Per CONTRIBUTING.md, the AP2 core specification has been donated to FIDO; this repo's scope is samples and SDK only. The core proposal below is intended for the FIDO working group; I'm filing here to:
- Surface the on-chain reference implementation to AP2 sample/SDK maintainers and contributors who may want to consume it,
- Get an early read on whether the envelope shape aligns with what the SDK currently emits, so the FIDO submission can land with concrete sample/SDK alignment evidence.
Happy to redirect this discussion to the FIDO working group if that's the appropriate venue.
Context
AP2 specifies the off-chain agent-payment authorization protocol — verifiable agent credentials, intent mandates, payment mandates. The gap, when AP2 lands on a public chain, is that the on-chain settlement layer typically doesn't verify the mandate; it trusts the wallet client or the relaying facilitator to honor the constraints.
We've shipped a stablecoin (RIVR) where the AP2-compatible mandate envelope is verified at the token contract. Every transferring path runs the full constraint chain: principal+agent EIP-712 signatures (EIP-1271 fall-through for smart accounts), per-tx cap, cumulative cap, asset allowlist, expiry, sanctions, KYA, Travel Rule attestation. The token reverts with a typed RefusalReason if any check fails.
Proposal
A reference on-chain implementation of the AP2 IntentMandate / PaymentMandate constraints, with a normative MandateEnvelope EIP-712 typehash so AP2-emitting agents can target multiple compliant chains without re-shaping the mandate per-chain.
The constraint envelope is rail-agnostic. The same shape applies to card-network mandates and ACH/SEPA/FedNow agent-bound authorizations. Rivier — the platform shipping this — is an agentic commerce + finance platform spanning fiat and crypto rails; RIVR is the crypto-side reference settler. Submissions to fiat-rail venues are happening in parallel.
Reference implementation
MIT, at https://github.com/rivier-ai/rivr.
- Token contract:
contracts/src/rivier-token/RivierTokenCCT.sol(transferWithMandateentry point) - Mandate type:
contracts/src/rivier-token/types/MandateEnvelope.sol - Test suite:
contracts/test/rivier-token/— 113 forge tests covering thedryRunbyte-equivalence invariant (off-chain simulation byte-equivalent to on-chain revert), namespace sum-preservation under fuzzing, replay protection, and reentrancy safety.
Audit: not yet commissioned. Independent contributor, pre-funding; engagement is planned post-launch.
What I'm asking for
- Sample/SDK maintainer feedback on whether the envelope shape aligns with what the AP2 SDK currently emits.
- Pointer to the right FIDO working-group venue for the normative submission.
- Whether the
AgentAttestationv1extensionDatashape (model id, version, confidence, reasoning hash, prompt hash, MCP server DID, TEE attestation hash, policy compliance proof) aligns with FIDO's existing attestation taxonomy.
FIDO Alliance Liaison membership is on the roadmap once the project's legal entity is formed; for now I'm contributing as an individual.
Disclosure: parts of this issue body were drafted with assistance from an AI model and reviewed before submission. The reference implementation, test suite, and design decisions are human-authored.
Activity
Ectsang commentedon May 7, 2026
The envelope shape makes sense — especially the typed
RefusalReasonreverts. On-chain enforcement at the token level is the cleanest way to guarantee constraints aren't bypassed.The part I'd push on: the cumulative cap counter lives in contract storage, so it only sees spend on that chain. An agent operating across RIVR on Base and a card rail via Stripe has two independent counters. I hit the same problem from the off-chain side (#207) — ended up building an external authority with authorize/commit/refund/query verbs that tracks spend across rails. Got a sample approved here (#252) showing the pattern.
Might be worth adding an optional
attestationfield to theMandateEnvelope— a pointer to an off-chain authority's signed budget state. Then on-chain and off-chain enforcement compose instead of competing.chopmob-cloud commentedon May 16, 2026
Operational notes on the envelope-shape question from someone who shipped JCS-canonical mandate binding for AP2 samples (PR #253). The on-chain-side proposal here is the natural next layer above what the SDK currently emits, and the alignment story is genuinely tractable.
On the envelope-shape alignment question (your direct ask)
Yes, the
MandateEnvelopeEIP-712 typehash you describe is consumable with minor SDK-side changes. The current AP2 PaymentMandate that the SDK emits (and that #253 canonicalises via JCS) carries:This maps directly onto your on-chain
MandateEnvelopestructure. The only structural delta we'd need on the SDK side is:RefusalReasonenum mapping — the typed revert path on RIVR is informationally richer than what the SDK currently emits for off-chain refusals. Worth lifting that enum into the SDK so off-chain and on-chain refusals share one taxonomy.Cross-rail cumulative-cap counter
Picking up @Ectsang's point from #207 / their reply above: the cumulative counter living in contract storage is correct for the on-chain settlement layer but doesn't see spend on parallel rails. We hit this operationally on AlgoVoi's APM (Agentic Payment Mandate) implementation — a single human can have a mandate that spends across 7 chains, and the £-denominated cumulative cap is enforced at the platform balance layer, not per-chain.
Two patterns we've found work:
Pattern A — Platform-side authority of truth (what we do for APM): the cumulative counter lives in a single Postgres row keyed on
(account_id, period_key); on-chain settlement is the side-effect, not the source of truth. Every payment-decision path holds an advisory lock onhashtext(mandate_id)to prevent TOCTOU. The on-chain transfer can fail or reorg, but the platform-side counter has already been decremented and is the audit-trail anchor.Pattern B — On-chain authority of truth (what RIVR does): the counter is in contract storage, the canonical refusal happens on-chain, and the off-chain layer is advisory. Strong for single-chain merchants but breaks for cross-rail spend.
The right answer probably depends on whether the merchant is multi-rail or single-rail. Worth a spec note non-normatively that the constraint-envelope shape SHOULD remain rail-portable (your point) but the cumulative-cap source of truth is implementor-defined. A merchant on RIVR-only treats the contract as source of truth; an agentic-commerce platform spanning fiat + crypto rails treats their internal ledger as source of truth, with on-chain settlement events as second-confirmation receipts.
Alignment with the wider spec convergence
Today's spec activity across the agentic-payments ecosystem is converging on one canonicalisation rule (SHA-256(JCS(...)) lowercase hex) and one envelope grammar (
*_pointerblock +off_vm_anchorblock +verification_recipe) across four evidence classes. See x402-foundation/x402#2322 (Compliance category + content_hash JCS rule) and #2334 (privacy_class).Your
MandateEnvelopetypehash sits naturally inside the cryptographic-class evidence shape from #2334 (cryptographic × public-settlement cell): the typed RefusalReason revert is the on-chain-anchored proof-of-determination. Worth a coordination note in the FIDO submission that the constraint-envelope hash should compute the same way on AP2-emitting agents whether the rail is RIVR / x402-on-Base / Solana Pay / Algorand USDC — single canonicalisation rule, single verifier code path, multiple settlement rails.On RIVR specifically
The full constraint chain enforced at the token contract (principal+agent EIP-712, EIP-1271 fall-through, caps, expiry, sanctions, KYA, Travel Rule) is exactly the policy surface we'd want for AP2 settlement compliance. The fact that you've shipped it on a live stablecoin gives this proposal real operational weight that a vapourware spec wouldn't carry.
One concrete question: the Travel Rule attestation at the token-contract layer — how does that compose with off-chain Travel Rule providers (TRP / OpenVASP) that operate before the on-chain settlement? Our compliance flow runs Travel Rule pre-settlement via off-chain attestation under EU TFR; running the same attestation as a token-contract refusal check is interesting but creates a second policy enforcement point. Worth thinking through whether they're complementary or whether one of them should be canonical.
Sample / SDK alignment plan
If you'd like a concrete next-step PR to align our existing AP2 samples with your envelope shape:
JCS_hashrule and the on-chainMandateEnvelopetypehashRefusalReasonenum into the shared schema so the AP2 sample for "refusal" returns the same enum value whether the refusal happened at the off-chain validator or the on-chain settlereth_signTypedData_v4call hashes the same bytes the on-chain check verifiesHappy to do (1) and (2) in a follow-up PR if useful — alignment work is small and surfaces the cross-protocol consistency story. (3) is FIDO-side per your acknowledgement of repo scope.
PR reads ready for early-read feedback from our side; will follow the FIDO submission discussion when it surfaces.
evidai commentedon Jun 4, 2026
Adding a data point from a production x402 implementation in case the comparison is useful.
We've been running LemonCake (https://lemoncake.xyz) as an x402 gateway since early 2026. Some notes on the AP2/x402 composition question:
Mandate-equivalent at our layer is the Buyer Key:
bk_…is long-lived (hashed at rest), carries hard caps (per_mint_max,daily_max,monthly_max), and is scoped to one seller. It's effectively a pre-authorized mandate — "agent may spend up to X for this seller without further human approval".lc_agent_charges.Settlement side:
PaymentIntentwith idempotency viacharge_ref(unique column). Direct Charge + 3% application_fee.Receipts: charges land in
lc_agent_chargeswith thecharge_refas the canonical identifier; we surface them on the public/api/<slug>page and via the agent's wallet endpoint. AP2's settlement-side mandate binding would map cleanly onto thatcharge_ref.Production: $1 off-session card charge through the full loop verified 2026-06-02.
30-second demo (no signup): https://lemoncake.xyz/demo
(Disclosure: I'm working on LemonCake — sharing as a reference implementation that already runs both x402 and Stripe Direct Charge in production, not pitching.)
chopmob-cloud commentedon Jun 6, 2026
Following on from @evidai's LemonCake data point — adding the crypto-rail side.
AlgoVoi runs the same cross-chain cap problem from the other direction: crypto settlement across 8 chains (Base, Algorand, Solana, Stellar, Hedera, Tempo, VOI, ARC testnet). The problem @Ectsang identified — "cumulative cap counter lives in contract storage, only sees spend on that chain" — is real and we hit it directly.
Our fix is gateway-level, not chain-level: the spend cap lives in the agent session JWT, not in any on-chain counter. Settlement attestations (
SETTLED / PENDING_FINALITY / REVERSED) flow back to the same session and decrement the same cap regardless of which chain settled. Cross-chain audit trail:prev_hash + content_hashlinks all settlement events for a session, so a verifier can reconstruct cumulative spend across chains from the audit chain alone.The
settled_payment_reffield ties each settlement back to the original mandate by content address — so the on-chain side proposal here slots in as a verifiable anchor for what the gateway-level cap is tracking.AlgoVoi (chopmob-cloud) — docs.algovoi.co.uk/settlement-attestation
Ram9199 commentedon Jun 13, 2026
I am collecting real-world incidents around agent mandates, payments, and high-risk tool authorization.
Was settlement-side mandate binding driven by actual incidents, near misses, or customer requirements where an agent payment/action could not be tied to a specific mandate and policy? What changed afterward - signed mandates, proof-of-possession, audit logs, manual approval, split credentials, or no change?
Not asking about any product. I am trying to understand whether agent authorization/provenance pain is already operational in commerce/payment flows.
chopmob-cloud commentedon Jun 13, 2026
@Ram9199 Honest answer: design driven, not a customer incident or an external near miss. The binding came out of our own cross-chain settlement work early in AlgoVoi, before it was ever a problem reported from the field.
The concrete thing that surfaced it: a cumulative spend cap held in per-chain contract storage only ever sees spend on its own chain, so an agent settling across several chains can stay under the cap on each one while exceeding it in aggregate. We hit that directly while building multi-chain settlement.
So the design we evolved keeps the cap off-chain. The cap value rides the agent session token rather than any single chain's contract storage, and each successful settlement, on whichever rail it lands, records spend against that one per-session counter, so the accounting is chain agnostic by construction. Settlement outcomes are attested as SETTLED, PENDING_FINALITY, or REVERSED, and the records are hash chained, each row carrying prev_hash and content_hash, so a verifier can reconstruct cumulative spend across chains from the audit chain alone. The result was not signed mandates or manual approval added after the fact, but a cap location and accounting design we chose up front, because the per-chain version could not express a cross-chain limit.
AmitabhainArunachala commentedon Jul 12, 2026
Direct answer to your question, from a live implementation rather than a design doc:
We built and deployed a paid
/mandate/verifyendpoint specifically because the agent-payment mandate binding gap showed up operationally, not just as a spec exercise. The endpoint checks 7 mandate fields that we found unenforced in existing agent-commerce stacks:pay_toin the x402 402 challenge?The concrete operational signal: when we exposed a live x402 endpoint and external agents began discovering it (156 unique IPs, 500+ 402 hits in a day), zero completed payment — they received the 402 challenge with payment instructions but never sent a payload. The gap is not "can the agent pay" but "does the agent have a mandate that authorizes this specific spend, bound to this specific resource, within a cap, with a receipt obligation." Without mandate enforcement, a wallet-equipped agent that pays is an uncontrolled spend surface.
The endpoint is live and x402-gated ($0.25 on Base/USDC) so an agent with a mandate can verify its own mandate programmatically before committing:
If you have a real agent mandate JSON (or a near-miss where an agent payment was not tied to a specific mandate/policy), send it — we will run the 7-check matrix and return the gap report. That is the concrete operational data point behind the design question you are asking.
chopmob-cloud commentedon Jul 12, 2026
Picking up your two still-open asks (2 and 3), since the envelope-shape one is largely covered above.
2. The FIDO venue
This repo's own docs answer it more precisely than the CONTRIBUTING pointer does.
docs/index.mdstates that standardization continues "within the Agentic Authentication Technical and Payments Technical Working Groups in FIDO." So the normative submission actually straddles two named groups, and it is worth splitting it that way rather than filing it as one piece:MandateEnvelope, the constraint chain, the typedRefusalReason) is Payments Technical Working Group material.AgentAttestationshape in your third ask is Agentic Authentication Technical Working Group material.Both are member-driven groups, so the intake path is FIDO membership rather than a public GitHub venue, which lines up with the liaison membership you already have on the roadmap. The two announcements linked from
docs/index.md(the Google AP2-to-FIDO donation post and the FIDO "Standards for Trusted AI Agent Interactions" post) are the current public anchors for who is chartered to receive it.3. Attestation taxonomy alignment
Partial, and the boundary is worth getting right before the submission, because FIDO's attestation model is narrower than the field list suggests.
In WebAuthn terms (the taxonomy the Agentic Authentication group inherits), attestation conveys, quoting the spec, "the provenance of an authenticator and the data it emits; including, for example: credential IDs, credential key pairs, signature counters." It attests the signer and its key, not arbitrary claims about the subject. The defined attestation types are Basic, Self, AttCA, and AnonCA (plus None), and the statement formats are packed, tpm, android-key, android-safetynet, fido-u2f, apple, and none.
Mapping your
extensionDataonto that:TEE attestation hashis the one field that is attestation in FIDO's sense. It maps onto the platform/hardware provenance atpm-style attestation statement carries: proof about where the key lives. That aligns cleanly.model id,version,confidence,reasoning hash,prompt hash, andpolicy compliance proofare not attestation under this taxonomy. They are claims about the agent's decision, not provenance of the signer. FIDO has no attestation type or format that models them.MCP server DIDsits in between: it is a subject/identity binding, not authenticator provenance.The cleaner framing for the WG, which keeps the FIDO model intact, is to treat the TEE piece as the attestation and everything model/reasoning/policy related as signed payload that the attestation covers rather than as attestation itself. Concretely: the authenticator attestation vouches for the key; the key signs an object that carries the agent-decision claims; a verifier trusts the claims because they were signed by a key whose provenance the attestation established. That preserves the FIDO separation between "who/what is the signer" and "what did the signer assert," and it is the distinction a reviewer in that group will look for first. Collapsing both into one "attestation" blob is the thing most likely to get bounced.
None of that changes your on-chain design, it just routes the two halves to the two groups that own them and frames the attestation half in the vocabulary the group already uses.