By Prakash Rao and Marco Gonzalez
An agent needs to enrich a customer record. The tool that does it costs money.
Nothing in x402 v2 says who is allowed to approve that. Nothing in MCP 2026-07-28 says it either. Both specs moved price and routing metadata into HTTP headers, which means your gateway can now see what a tool call costs. Seeing a price is not the same as having the authority to approve one.
We wrote about where that authority should sit in 402 Payment Required: What Enterprise MCP Servers Owe the Agents That Pay Them, with the Agentic AI Foundation. Then we took it to AGNTCon / MCPCon Japan in Tokyo and let an agent actually do it, live, on a test network.
If you missed either the post or the session, here is everything you need before we start.
First, the catch-up
What x402 is. HTTP has had a 402 Payment Required status code sitting unused since 1997, reserved but never specified. x402 finally gave it a meaning: a server answers 402 with the terms it wants, the client retries with a signed payment authorization, and the server returns the result plus evidence of settlement. v2 (December 2025) moved all of that into headers: PAYMENT-REQUIRED, PAYMENT-SIGNATURE and PAYMENT-RESPONSE, instead of burying it in bodies.
What MCP is. The Model Context Protocol is how an AI agent discovers and calls tools. The 2026-07-28 revision removed sessions and the Mcp-Session-Id header, added Mcp-Method and Mcp-Name as headers, and hardened authorization with issuer validation per RFC 9207.
Why those two releases matter together. Both moved the interesting metadata out of request bodies and into headers. Your gateway, rate limiter and WAF can now see who is calling, which tool, and what it costs, without parsing a single JSON-RPC body. Metering and routing for paid agent traffic got dramatically cheaper.
And that is exactly where the problem starts. Header visibility is not authority. The moment your infrastructure can see the price, it has to decide something about it, and neither spec tells you who gets to make that call. That was the argument in our AAIF post: the spending decision belongs to a budget authority the agent cannot reach, and where you put it is the part that will still matter after the protocol details have moved on.
What we did in Tokyo. Stopped arguing and ran it: a live agent paying for MCP tool calls on a test network, including the path where it tries to spend on something it was not approved for.
The cast
Five moving parts appear throughout. You do not need to memorize these; they are here so the diagrams read cleanly.
| Component | Its one job |
|---|---|
| Agent | Decides what work to do. Treated as hostile throughout. Holds no private key. |
| Enforcement gateway | The only permitted route out. Sees every request twice and can refuse at the boundary. |
| Authorization service | Decides whether spending is permitted, and reserves the budget. The agent cannot influence it. |
| Signing service | Produces signatures. Reads what it signs, and can refuse. |
| Protected resource | The tool that costs money, behind a paid server. |
Our threat model, so you know what is and is not being defended: the agent process and every input to it are hostile. The gateway, the services and their policies are honest but fallible. Compromised binaries, malicious operators, key extraction from hardware and chain-level attacks are out of scope. Different problem, different post.
Now follow one request all the way through. It gets interesting about two-thirds of the way down, when the request that arrives at the boundary turns out not to be the request that was approved.
The agent goes shopping
Our agent has a task stated in business terms: enrich record 42. It does not know what that costs, and it does not know which provider will do it.
So the first problem is immediate: how do you let a hostile thing shop around, without handing it a wallet first?
It asks, and gets refused on purpose:
GET /tools/enrich?record=42 HTTP/1.1
Host: resource.example
HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: eyJ2IjoyLCJhY2NlcHRzIjpbey...
That header is base64 JSON. Decoded, it is an offer:
{
"v": 2,
"accepts": [{
"scheme": "exact",
"network": "<test-network>",
"asset": "<stablecoin-contract>",
"payTo": "<payee-address>",
"maxAmountRequired": "<amount>",
"resource": "https://resource.example/tools/enrich",
"maxTimeoutSeconds": 60,
"nonce": "<server-chosen>"
}]
}
That is the HTTP binding. Over MCP the same negotiation happens, but not in the same place. Worth knowing before you meter anything at the edge:
| HTTP binding | MCP binding | |
|---|---|---|
| Price challenge arrives as |
402 status + PAYMENT-REQUIRED
|
terms in the tool result |
| Routing metadata | the URL path |
Mcp-Method / Mcp-Name headers |
| Visible to infra without parsing a body | yes | the challenge is not |
The MCP binding hands your gateway a 200 for a call nobody has paid for yet. If your metering logic keys on status codes, it will sail straight past that.
Here is the first design decision that matters more than it looks. Asking is free.
If finding out what something costs itself costs money, then every agent needs spending authority before it can plan, which pushes the authorization decision to the least informed moment in the entire flow. Free discovery is what lets this first pass carry no authority to consume value at all. The boundary can admit it precisely because admitting it commits the organization to nothing.
And because this pass is cheap to allow, the boundary can do something quietly important while it is forwarding:
sequenceDiagram
autonumber
participant W as Agent
participant G as Enforcement gateway
participant R as Protected resource
W->>G: candidate request, identity present,<br/>no authority to consume value
G->>G: validate assertion, apply a lighter policy
G->>G: audience translation: mint a downstream credential<br/>scoped to one resource and one action
G->>R: forward, carrying only that credential
R-->>G: resource-terms response, the quote
G->>G: record the witnessed terms, from the return leg
G-->>W: the quote
The gateway saw the quote itself, on the way back. Remember that. It becomes the whole game at the boundary.
Note also what crossed the boundary: not the enterprise credential, but a freshly minted one restricted to one resource and one action. Useful for nothing else. That is audience translation, and it is how you avoid handing a downstream service something it could turn around and reuse.
Someone has to approve it
Now the agent wants to actually do it. This is the moment the money becomes real, and the moment somebody other than the agent has to say yes.
Four decisions are tangled together here, and the useful move is to pull them apart and give each one an owner:
| Decision | Who owns it |
|---|---|
| What work to do | the agent |
| Who is asking | identity provider, at the gateway |
| What it costs | the resource server, from the arguments |
| Whether it is permitted | a budget authority the agent cannot reach |
That last row is the one people collapse, and collapsing it is how an agent ends up able to approve its own spending.
The authorization service does four things in order. Each one is load-bearing:
It fetches the quote from the gateway, never from the request. The agent is hostile; its claim about what it was offered is worthless. The boundary witnessed the real terms while the agent was shopping.
It filters on capability before it looks at price. Capability and compliance narrow the field first. A filter, not a ranking. No price advantage restores an excluded candidate. Get this backwards and you have a system that will happily route regulated data somewhere cheap.
It reserves the budget before any attempt is made, as one indivisible state transition. This is what stops concurrent agents sharing an allocation from collectively overspending it: the commitment is taken up front rather than discovered afterwards.
Then it binds the whole meaning of the action into one digest, and signs a receipt over it.
// authorization service
const fields = deriveActionFields(proposal, witnessedTerms, selection);
const digest = sha256(canonicalize(fields)); // RFC 8785 serialization
await reserveAtomically(allocation, ceiling); // before any attempt
const receipt = signReceipt({
decision: 'allow',
digest, // the binding
policyVersion,
expiresAt: now() + SHORT_TTL
});
One binding, over the whole meaning of the action. Alter any bound element and the digest differs.
The signer that can still say no
The agent has a receipt. It needs a signature. And here the architecture does something that surprises people:
The component that approved the action cannot sign it. The component that signs it can still refuse.
// policy-aware signing service
async function sign(request) {
const payload = parseStructuredPayload(request); // it can read what it signs
const params = recoverConsequentialParameters(payload);
if (!withinOwnConstraints(params)) {
return refuse('constraint_violation'); // even though the gate approved
}
return signWithNonExportableKey(payload);
}
A custody service that signs an opaque digest enforces nothing about meaning. Handed a blob, it has no basis on which to form an opinion at all. This one parses the payload first, recovers the parameters that actually matter, and applies its own constraints. It can refuse an action the authorization gate already approved.
Meanwhile the gate holds no key whatsoever. It is incapable of producing a signature. That is not a policy choice you can accidentally configure away. It is a property of who holds what.
There is a boring reason this split survives contact with a roadmap, and it persuades engineers faster than the security argument: the rules about what a key may be used for and the rules about what policy permits change at different rates, for different reasons, owned by different teams. Putting them in one component means every policy change touches the thing holding your keys. Nobody wants that.
And then it gets refused
The signed request heads for the exit. The receipt is valid. The signature verifies.
And the boundary refuses it.
sequenceDiagram
autonumber
participant W as Agent
participant G as Enforcement gateway
participant R as Protected resource
W->>G: authorized request + valid receipt
Note right of W: something changed after approval
G->>G: re-derive the action from the bytes that will actually leave
G->>G: recompute the digest
G--xW: DENIED: mismatch, fail closed
Note over G,R: nothing leaves
Here is why, and it is the single most important line in the whole design:
The gateway does not read the digest out of the receipt. It rebuilds it from the bytes that are actually leaving.
// enforcement gateway, egress path
async function release(outgoing, receipt) {
if (!verifySignature(receipt)) return failClosed('bad_receipt');
if (expired(receipt)) return failClosed('expired_receipt');
// Re-derive from the request that will actually leave, not from the receipt.
// RFC 8785 fixes the serialization; a normative rule both sides build
// against fixes which fields participate.
const rebuilt = canonicalize(deriveActionFields(outgoing));
if (sha256(rebuilt) !== receipt.digest) {
return failClosed('digest_mismatch'); // substituted, or altered after approval
}
// A valid receipt is necessary, not sufficient.
const verdict = await policy.evaluate(deriveActionFields(outgoing));
if (!verdict.allow) return failClosed(verdict.reason);
return forward(translateAudience(outgoing));
}
Two independent opinions have to agree. One was formed at approval time, from the proposal; the other is formed here, from reality. Verifying a decision is not the same as verifying the thing the decision was about, and almost every system that gets this wrong gets it wrong right here.
Notice what a mismatch catches in both directions. Substituting to a more expensive resource is a budget failure. Substituting to a cheaper one can send regulated data somewhere not approved for it. Same mismatch, same refusal.
And notice failClosed on every path. Where required verification cannot be completed, the request is denied. A verifier that cannot answer is never treated as one that said yes.
Settling up
When a request does pass, the ending is deliberately unglamorous. The agent signs the value authorization but submits nothing; a facilitator submits it and bears the execution fee, so the workload needs no local balance of any kind, of any asset. Then one trace identifier binds the identity subject, the signing request, the nonce and the settlement record together, so a completed action can be reconstructed end to end from evidence held entirely outside the agent.
Neither direction requires trusting it. That is the point.
Here is the whole path, once, now that you know what each step is defending against:
sequenceDiagram
autonumber
participant W as Agent
participant G as Enforcement gateway
participant A as Authorization service
participant S as Signing service
participant R as Protected resource
Note over W,R: DISCOVERY, no authority to spend
W->>G: discovery pass, no authority to consume value
G->>R: forward with a scoped downstream credential
R-->>G: the quote
G->>G: record the witnessed terms
Note over W,R: AUTHORIZATION
W->>A: request authorization
A->>G: fetch witnessed terms, never from the request
A->>A: capability filter, then cost
A->>A: reserve atomically, before any attempt
A-->>W: signed receipt, bound to the digest
Note over W,R: SIGNING
W->>S: request a signature
S->>S: parse, recover parameters, enforce own constraints
S-->>W: signature from a non-exportable key
Note over W,R: EGRESS
W->>G: authorized request + receipt
G->>G: re-derive from outgoing bytes, recompute digest
G->>G: independent policy re-evaluation
G->>R: release, or refuse
What the stage taught us
Three things we did not fully appreciate until it was running in front of people.
The refusal is the demo. We prepared a success path and a denial path and rehearsed the success path harder. That was backwards. An authorization architecture is most convincing at the moment nothing happens. The receipt is valid, the signature is good, and the request still does not leave. If you demo one of these, budget your stage time for the refusal.
Canonicalization determinism is not a detail, it is the design. Two independent derivations only help if they agree. RFC 8785 fixes how to serialize; it does not fix which fields participate. Every ambiguity you leave in that question becomes a future incident that presents as a mysterious denial and resolves three layers away. Write the membership rule down as a normative artifact both sides build against, not as behaviour each side reimplements from prose.
Reachability is not authority. The agent in this flow calls the authorization service over an ordinary API, like any client. It simply cannot move its own limit. An agent that cannot reach the authorization service is isolated; an agent that reaches it and still cannot change the answer is governed. Those are very different systems and only one of them is useful.
Neither x402 v2 nor MCP 2026-07-28 makes the spending decision for you. They make the price visible and the routing cheap. Where the authority sits is still yours to place, and that placement will outlive both protocols.
The AAIF post has the full argument and the five obligations a paid tool server owes its callers.
Code is illustrative and abbreviated to the checks under discussion.
Patent pending. U.S. Provisional Application No. 64/149,249, filed 6 September 2026 (docket Y26S008-PPA). A provisional application only: no patent has been granted and no patent number is asserted.
Top comments (0)