Verifiable Intent: signing what the user authorized
Visa's Trusted Agent Protocol signs who is knocking on the merchant's door. Verifiable Intent signs what the human on the other end actually approved. Those are different problems, and until March nobody had published an open format for the second one.
Mastercard announced Verifiable Intent on 5 March 2026, co-developed with Google and with support commitments from Fiserv, IBM, Checkout.com, Basis Theory and Getnet. The framing from Mastercard's chief digital officer Pablo Fourez was compact: "As autonomy increases, trust cannot be implied. It must be proven." The spec and a Python reference implementation went up the same day under Apache 2.0 at github.com/agent-intent/verifiable-intent, with documentation at verifiableintent.dev.
This is the authorization half of the problem we covered from the identity side in Visa's Trusted Agent Protocol. TAP proves an agent is accountable to somebody. Verifiable Intent proves that a specific purchase falls inside the boundary a specific human drew, and it does so in a format any party can check without trusting the agent's self-report. We cloned the repo on 2 August 2026, read the four normative documents, ran the reference implementation, and found the parts that work and the parts that do not.
The shape: three layers of SD-JWT
Verifiable Intent is a credential format, not a protocol. It defines no endpoints, no transport, no message flow. What it defines is a chain of SD-JWT credentials — Selective Disclosure JWTs, standardized as RFC 9901 in November 2025 — where each layer cryptographically authorizes the next.
Layer 1 is issued by a bank or payment network into a credential provider's wallet. It carries the user's identity claims and, critically, a cnf.jwk claim per RFC 7800 holding the user's public key. Lifetime is roughly a year. In the Mastercard reference profile (vct: "https://credentials.mastercard.com/card") the always-visible claims include pan_last_four and scheme; email is selectively disclosable.
Layer 2 is signed by the key L1 bound. This is the user's mandate — the record of what they authorized. It carries an sd_hash claim that equals B64U(SHA-256(ASCII(serialized_L1))), pinning it to the exact serialized form of the credential above it.
Layer 3 exists only when the user delegates to an agent. It is signed by a key the user bound inside the L2 mandates, and it carries the concrete values: this merchant, this cart, this amount.
// The chain, as the spec states it normatively
L1 cnf.jwk = user public key // issuer binds the user
L2 signed by user private key
L2 sd_hash = hash(serialized L1)
L2 mandate cnf.jwk = agent public key // autonomous mode only
L3 signed by agent private key
L3 sd_hash = hash(L2 + selected disclosures)
L3 header.kid = agent key id // MUST match L2 cnf.jwk.kid
L3 payload has no cnf // terminal delegation
Three details in that block matter more than they look. First, L3 headers MUST NOT contain a jwk parameter — the verifier resolves the agent's key from L2 by matching kid, so an agent cannot ship its own key alongside its own signature. Second, L3 carries no cnf at all, which makes delegation terminal: an agent cannot sub-delegate to another agent. Third, the whole thing is ES256 only. The spec pins alg to "ES256" and _sd_alg to "sha-256" at every layer, and the security model is explicit about why: algorithm confusion attacks live and die on verifiers that read alg from the header they are about to verify.
Two modes, and the one that matters
Immediate mode is two layers. The user reviews the final checkout and the final payment values and signs them directly. L2 mandates carry no cnf claim at all, and the absence is load-bearing: it signals that no further delegation is permitted. Lifetime around fifteen minutes. The agent is a courier.
Autonomous mode is three layers and is the reason the spec exists. The user does not see the final cart. They sign constraints — an amount range, a merchant allowlist, acceptable line items — and bind the agent's public key inside each mandate. L2 uses typ: "kb-sd-jwt+kb", where the +kb suffix means the credential carries key binding for onward delegation. Lifetime runs 24 hours to 30 days, and MUST NOT exceed L1's exp.
Then the agent shops, and produces Layer 3. Here the design does something worth stealing.
The split L3
L3 is not one credential. It is two, produced by the same agent key and sent to different parties.
L3a is the payment mandate. It goes to the payment network and carries payment_instrument, the final payment_amount, and a transaction_id. L3b is the checkout mandate. It goes to the merchant and carries the merchant-signed checkout_jwt, the line items, and a checkout_hash.
The merchant never sees how the purchase was funded. The network never sees what was in the cart. That boundary is not architectural convention — it is enforced at the credential level, because each L3 computes its sd_hash over only the L2 disclosures its recipient is entitled to see. L3a hashes the L2 base JWT plus the payment and merchant disclosures; L3b hashes the base plus the checkout and item disclosures. Neither can reconstruct the other's half.
The two are stitched together by one hash:
checkout_hash = B64U(SHA-256(ASCII(checkout_jwt)))
// carried as `checkout_hash` on the checkout mandate (L3b)
// carried as `transaction_id` on the payment mandate (L3a)
// verifiers MUST check: L3a.transaction_id == L3b.checkout_hash
That single equality is what stops the checkout-payment mismatch attack: an agent authorizing payment for an expensive item while the merchant fulfils a cheap substitute. Both halves reference the same byte string or the chain breaks.
There is a parallel defence for the split-agent attack, where two different agent keys are bound in the checkout and payment mandates of the same L2, letting one agent pick the cart and a second one approve the money. The spec's answer: cnf.jwk across all mandates in an L2 MUST be identical, and verifiers MUST compare them.
Eight constraints
Constraints are where human intent becomes machine-checkable. They appear only in autonomous-mode mandates, as a JSON array inside the selectively disclosable mandate claim. VI v0.1 registers eight types, and verifiers MUST support all of them.
Four bound the shape of the purchase: mandate.checkout.allowed_merchants, mandate.checkout.line_items, mandate.payment.allowed_payees and mandate.payment.reference, the last of which carries the conditional_transaction_id that pairs a checkout mandate with its payment mandate at delegation time.
Four bound the money: mandate.payment.amount_range (per transaction), mandate.payment.budget (cumulative), mandate.payment.recurrence (merchant-managed subscriptions) and mandate.payment.agent_recurrence (agent-managed repeat purchases, where end_date is REQUIRED so the authorization cannot be open-ended).
// A payment mandate's constraint array, verbatim from the spec
{
"type": "mandate.payment.amount_range",
"currency": "USD",
"min": 10000, // integer minor units, ISO 4217
"max": 40000
}
Every amount in the spec is an integer in ISO 4217 minor units. No decimal strings, no float parsing, no ambiguity about whether 1.10 means a dollar ten or a dollar one. This is the same discipline the x402 schemes settled on, and for the same reason.
The validation model is worth naming precisely. The verifier builds a fulfillment — a derived object pulling final values out of L3 — and checks it against the L2 constraints. The merchant identifier is not a field on the L3 checkout mandate; it must be extracted by decoding the merchant-signed checkout_jwt embedded inside it.
Where the cryptography stops
The most honest section of the spec is security-model §4.2, and it is the one every gateway operator should read.
Three of the eight constraints — budget, recurrence, agent_recurrence — are what the spec calls network-enforced. They cannot be verified by a stateless verifier. The signature chain proves the agent's L3 is authentic; it proves nothing about how many L3s the agent has already produced from the same L2.
The spec enumerates why every per-credential defence fails at this: nonce uniqueness stops replay of the same L3, not generation of new ones. aud binding does not help because the agent chooses L3's aud. Short lifetimes bound the replay window, not sequential issuance. sd_hash prevents L2 substitution, not multiplication. And amount_range is checked per L3, never accumulated.
sd_hash.
In the base model, one L2 mandate pair authorizes exactly one L3a plus L3b. Enforcing that "exactly one" is a database problem, not a signature problem. This is the same boundary we hit in the x402 upto scheme, where the signature authorizes a ceiling and something stateful settles the real number, and the same one behind reserve-then-settle in our own billing path. Every credible agent-authorization design converges on it: a signed envelope plus a ledger that remembers.
The known-limitations section is similarly candid. VI v0.1 has no revocation protocol — layered lifetimes and JWKS key removal are the mitigations, with RFC 9701 Token Status Lists flagged as the likely future mechanism. The sub claim in L1 is always visible and persists for the credential's life, so a verifier can link a year of transactions to one user. And §6.6 admits the checkout_jwt structure is implementation-defined: verifying the merchant's signature on it is only a SHOULD in v0.1.
What the repo actually ships
We cloned the repository on 2 August 2026 and ran it. The reference implementation is real: 326 tests pass in about a second, and examples/autonomous_flow.py walks a complete three-layer purchase — issuer credential, user mandate with constraints, agent-produced split L3, merchant verification, network verification — end to end. That is more than most specs at this stage ship.
It is also, as of today, frozen. The repository has five commits. The last one landed on 20 April 2026 and merged a breaking field-alignment change. Nothing since. Against that: 25 open issues, 82 stars, 16 forks. Six of those issues were filed by Ant Financial on 27 March 2026 — trust model, chargeback liability, key and credential handling, intent revocation — and all six are still open with no maintainer response four months later. A TypeScript port arrived as PR #31 on 28 July 2026, unreviewed.
Two findings from reading the code matter more than the commit gap.
Every L2 it issues violates RFC 9901
The L2 format lists each mandate's disclosure digest twice in the signed payload: once as a delegate_payload reference and once in the top-level _sd array. RFC 9901 forbids this on both sides — §4.1: "The same digest value MUST NOT appear more than once in the SD-JWT." §7.1: "If any digest value is encountered more than once in the Issuer-signed JWT payload... the SD-JWT MUST be rejected."
We reproduced it at commit 356c296. The L2 credential produced by the project's own autonomous example carries six digests in _sd and two in delegate_payload, with both of the latter also present in the former — each mandate digest appears exactly twice in a payload that RFC 9901 says must be rejected on sight.
This is not theoretical. sd-jwt-js PR #380 tightened disclosure validation and shipped in v0.20.0 on 29 June 2026. Issue #29, filed 17 July 2026 with a contained two-file fix offered, has zero comments.
The constraint checker defaults to the loose path
The spec defines two strictness modes and states that "regardless of strictness mode, verifiers MUST reject open mandates containing unknown constraint types" — because an unevaluable constraint leaves agent authority unbounded. The reference implements this as a separate is_open_mandate flag on check_constraints(), defaulting to False.
That flag is set to True in exactly three places in the repository, all of them inside tests/. Nothing in src/ or examples/ ever sets it — including the autonomous example, which calls check_constraints(payment_constraints, fulfillment) with both the mode and the flag left at their defaults. Since constraints only ever appear in open mandates, the shipped demonstration path is precisely the configuration the spec says MUST reject unknown types, and does not.
Compounding it: verify_chain() does not check constraints at all. Signature and binding verification is one call; constraint validation is a second call the integrator must remember to make, in the right mode, with the right flag.
None of this is fatal to the design. All of it is fixable in an afternoon. But a draft that standard verifiers reject, sitting untouched for three and a half months while its intended integrators file unanswered questions, is a spec whose adoption clock has not started.
Where it sits in the stack
VI is deliberately narrow. Out of scope: transport, key management, credential provider enrollment, agent platform APIs, dispute resolution, regulatory compliance mapping. It carries no money and defines no endpoints.
Its own protocol-landscape document positions it as the concrete implementation for a layer AP2 describes but leaves open: AP2 names Verifiable Digital Credentials as its trust primitive without prescribing a format. VI proposes to be that format, carried in UCP's dev.ucp.shopping.ap2_mandate extension fields without changing UCP's endpoints. Against the Agentic Commerce Protocol, it is orthogonal — ACP moves a delegated payment token through a checkout API; VI is the evidence that the human sanctioned it.
The convergence is now hard to miss. ACP has allowance. ERC-7715 and session keys have spend permissions. x402 has upto. VI has constraints. Four ecosystems, four vocabularies, one primitive: a signed ceiling on delegated spend, plus something stateful that counts.
What it means for LLM4Agents
VI does not compete with our rail. It sits above it. The credential authorizes; x402 and EIP-3009 move the value. A mandate that says "up to $400 at these payees" is agnostic about whether settlement lands as a card authorization or a USDC transfer.
The gap is the L1 issuer role. VI's Layer 1 is defined as issued by a bank or payment network, and the reference profile carries pan_last_four and scheme. A stablecoin gateway cannot be a VI Issuer as v0.1 is written. That is a real constraint on how far the format travels outside card rails, and the thing to watch: if the mandate vocabulary for agent purchases ends up owned by the networks, agent commerce routes back through card economics by default.
What we can do today is the part the spec says cryptography cannot cover. The gateway is already the stateful enforcer VI demands. Reserve-then-settle tracks cumulative spend per agent per period; that is the same ledger the spec asks payment networks to maintain against an L2's sd_hash. Our constraint surface maps almost one-to-one: allowed_payees onto permitted model and tool endpoints, amount_range onto a per-call ceiling, budget onto the agent's balance cap, agent_recurrence onto scheduled work.
The other immediate value is evidentiary. VI's dispute-evidence package — issuer signature, user signature over constraints, agent signature over final values, timestamps, hash bindings — is a good description of what an agent gateway should be able to produce for any settled call, whether or not a card network is involved. You do not need to be an Issuer to emit that bundle.
Staying on the frontier
Concrete, in order.
First, emit the evidence bundle. For every settled call, persist the tuple that VI treats as dispute-grade: who authorized, under what limits, what the agent actually executed, and the hash binding between the request and the settlement. This is additive to the existing billing path and costs nothing in latency.
Second, build the counters the spec requires. Cumulative spend and occurrence count keyed by a mandate digest, with the base-model rule enforced — one fulfilment per mandate pair unless a recurrence constraint says otherwise. We already have most of this; what is missing is keying it by an external mandate identifier rather than only by our own agent ID.
Third, write the verifier before the issuer. Accepting a VI mandate is the cheaper integration and the more useful one: verify ES256 with an explicit allowlist, walk cnf from L1 to L3, recompute both sd_hash values and the checkout_hash equality, and run constraints in STRICT mode with unknown types rejected. Reject duplicate digests per RFC 9901 §7.1 — which means, today, refusing the reference implementation's own output. Better to be correct and interoperate later than to inherit a conformance bug.
Fourth, propose the stablecoin binding. The missing piece is a payment_instrument type and a payee vocabulary that describe an on-chain settlement rather than a card. That is a small, well-scoped contribution to an Apache-2.0 draft whose maintainers have not yet had to answer whether the format is card-shaped by accident or by design. The unmerged PRs proposing wallet-state and cumulative-budget constraints suggest others are already pushing on the same edge.
Fifth, watch three signals. Whether Mastercard ships the Agent Pay intent APIs it promised in March; whether the TypeScript port lands and with it a second implementation to test interop against; and whether issue #29 gets closed. That last one is the cheapest adoption thermometer available — a spec that leaves standard verifiers rejecting its own credentials for a month is not yet being integrated by anyone who would have noticed.
Verifiable Intent has the right shape. Delegation as a signed chain, constraints as machine-checkable objects, selective disclosure as a privacy boundary between merchant and network, and an honest admission that the last mile is stateful. What it does not yet have is momentum. The design deserves better maintenance than it is getting.
Give your agent a budget it cannot exceed
Per-call ceilings, cumulative caps and a settlement record for every request — on an OpenAI-compatible gateway.
Register an agent