Chio/Docs
LOGIN · JOIN

Approval-Bound Payments

Tie each payment to the exact terms a person approved, spend that approval once, and keep money from standing in for authority.

Concept chio@5f63e6e

On this page

An agent that pays for things raises three questions that are easy to run together: may the agent invoke this tool, did a person agree to these terms, and did the money move. Each has a different witness. A capability grant answers the first, a signed approval the second, a payment proof the third. A system that lets one answer stand in for another can be paid into acting, or approved into paying someone it never named.

Chio keeps the three checks apart. A payment lane spends money only under an approval verified against the exact intent the approver signed, the approval pays once, and no payment proof ever produces the authorization to call a tool. The running example is a refund on order-77: USD 58.00 in USDC on Base, above the USD 50 line where your pricing lead must sign.

Authorization, approval, and payment are separate checks

Each check reads its own input and produces its own result. None of them reads another’s result as a substitute for its own input.

Approval-bound paymentChoose a case.
authorizationapprovalpaymentgrantsadmitsdispatchesverifiesconsumed bypresentsissuesκgrantgToolCallAuthorizationKkerneldrefundτsigned approvalvVerifiedApprovalmtransferEIP-3009ℓpayment laneρpayment artifactRreplay store(request, intent)κgrantgauthorizationKkerneldrefundτapprovalvverifiedmEIP-3009ℓlaneρartifactRreplay store

Three checks, three separate answers

The grant admits the refund call. The signed approval verifies into a VerifiedApproval bound to this request and its intent hash, and the EIP-3009 lane checks chain, payee, and amount against it before it issues the transfer authorization. The replay slot for this request and intent is now spent.

Call
dispatched under the grant
Payment
artifact issued, replay slot consumed
The three checks for one approval-bound refund paid through the EIP-3009 lane. The x402 and Circle lanes take the same VerifiedApproval; budgets, holds, and guards are left out.

The refund dispatches only when the grant admits it, and the transfer is issued only when the lane accepts the approval. A refused payment leaves an admitted call with nothing paid; a paid call with no grant is refused before dispatch. Settlement Rails covers how rail outcomes are observed and recorded on the receipt.

A verified approval binds one request, one intent, and its terms

Declare the rule and the settlement terms on the service. The terms go inside the governed intent, so the approver’s signature covers the chain, the payee, the amount, and the token along with the request:

pricing.tspricing.pysrc/main.rs · in main
import { Computer, USD } from "@chio-protocol/sdk";

const project = await Computer.open("quickstart");
const pricing = await project.service("pricing", { tools: { refund: "src/pricing.py:refund" } });

const grant = await pricing.grant({ to: "did:chio:z6MkbY4…e1R", tools: ["refund"], budget: USD(500), expiresIn: "24h" });
const rule = await pricing.require({ approval: "you", when: "refund.amount > 50" });
const terms = await pricing.settle("refund", {
  rail: "eip3009",
  chain: "eip155:8453",
  token: "USDC",
  payee: "refund.wallet",
});
for (const step of [grant, rule, terms]) console.log(step.describe());
Output
grant      cap-01c3…5a72 to did:chio:z6MkbY4…e1R, allows pricing.refund
rule       refund.amount > 50 waits for your approval
settles    refund over eip3009 on eip155:8453 in USDC, payee from refund.wallet
binding    chain, payee, amount, token contract, dispatch, capability, expiry

A payment lane accepts only a VerifiedApproval, and only verify_governed_approval constructs one. It checks the token’s Ed25519 signature, an Approved decision, the validity window, the expected approver, subject, and request id, and that the intent’s hash equals the hash the approver signed. The intent must carry its settlement terms, the approval may not outlive the token, and no approval lives longer than 3,600 seconds.

The result binds the request id, the intent hash, the approval id, and the terms: chain, payee, amount in minor units, token symbol and contract, settlement currency, dispatch id, capability id, and expiry. Its fields are private and it cannot be cloned, so a copy cannot be paired with a fresh nonce. The raw lane builders that take bare terms are private to the settlement crate.

Each lane checks the terms the approver signed

Your pricing lead approves from the terminal. The pending request shows the terms the signature will cover:

Terminalbash
chio approvals pending
chio approve apr-01c7…9b4e
Output
pending    apr-01c7…9b4e   z6MkbY4…e1R   refund order-77 USD 58.00   waiting 40s
           pays 58.00 USDC on eip155:8453 to 0x7a3f…91be, approval expires in 1h
approved   apr-01c7…9b4e, signed by did:chio:z6Mkf3…Qp9, binds request 2c8d…e017
           intent 6f41…a0b3, which carries the settlement terms

Each lane consumes the approval and compares the payment it is about to authorize with the signed terms. A mismatch returns InvalidBinding naming the lane and the field, and the lane issues nothing.

LaneChecked against the approvalSingle use
x402Chain, payee, amount, dispatch, capability; token by symbolApproval slot
EIP-3009Chain, payee, value, token contract, window inside the expiryApproval slot and nonce
CircleChain, payee, amount, token symbol, dispatchApproval slot
ERC-4337 paymasterNone; gas sponsorship onlyNot a spend authorization

x402 names tokens by symbol, so an approval that pins a token contract is refused on that lane, and the accepted tokens are filtered to the approved symbol. EIP-3009 requires the contract, and its authorization’s validBefore may not pass the approval’s expiry. The paymaster check takes no approval at all, and an executor never reads allowed: true as permission to spend.

One approval pays once

Watch the service while the buyer’s agent retries the refund and redirects it, and a stranger pays for a quote it holds no grant for:

pricing.tspricing.pysrc/main.rs · in main
for await (const call of pricing.events()) console.log(call.summary);
Output
00:00  z6MkbY4  refund  order-77 USD 58.00   pending   approval required
00:41  z6MkbY4  refund  order-77 USD 58.00   allow     apr-01c7…9b4e, paid 0x7a3f…91be
00:52  z6MkbY4  refund  order-77 USD 58.00   refused   approval already consumed
01:30  z6MkbY4  refund  order-77 USD 58.00   refused   EIP-3009 payee mismatch
01:44  z6MkQx8  quote   sku-301 at 80%       refused   paid over x402, holds no grant

The settlement replay store keys each approval on its request id and governed intent hash, and the lane consumes that slot only when it issues a payment artifact; the second refund gets governed approval has already been consumed by an issued settlement artifact (replay rejected). EIP-3009 adds its own store, keyed on the payer, chain, token contract, and nonce, and kept until the authorization’s validBefore. Both stores have durable SQLite forms.

The kernel keeps a third store for dispatch, keyed on subject, request, and intent. With no store configured, a governed call is denied rather than admitted unchecked.

A payment proof never authorizes a tool call

A verified x402 payment yields claims under claim.public_settlement.* and nothing else. ToolCallAuthorization is granted only by from_capability_grant, when a grant names the server and tool, includes invoke, and carries no unmet constraint; a settlement verifier report always returns it denied, without reading a field. The conformance suite holds this with x402_payment_does_not_authorize_tool_call, including forged verdicts and claims.

The x402 adapter also fails closed on the money itself. A facilitator response that omits settled reads as unsettled, and a prepaid-final rail refuses with x402 authorization did not complete final prepayment. Attestations get the same treatment: an EAS or Verax attestation is a display record and never parses as an inclusion proof, as Anchoring shows.

Next: put a rail behind the approval