Agreement payments
For a payment that is not itself a public proof of H, the buyer's agent approves a small agreement payment that carries H, and transact signs the payment for the resource only after its receipt.
A card charge, a Stripe payment, a Lightning invoice, a checkout in which the buyer signs nothing: these payments do not
put H anywhere public. For such a pairing, the proof of agreement is a second, small payment that does: the
agreement payment. The seller names an agreement URL in its offer. The agreement payment is a payment like any
other, so the buyer's agent approves it before anything is signed. transact then pays it, waits until it is recorded,
and only then signs the payment for the resource. confirm and finish do none of this; see
Driving the steps yourself.
The pairings reference marks each pairing whose payment is not a public proof. In the
registry, it is pattern.publicProof === false.
The exchange
-
Compare. The gate fetches and compares the ATR as for any pairing. A mismatch ends here.
-
Ask. The gate sends one
GETto the agreement URL, with no payment, no redirect and a 10-second deadline. This request and every paid one ask forAccept-Encoding: identity; a200or a402with any otherContent-Encodingisagreement-failed, its body unread.200: taken as an agreement already recorded. The body must be the receipt for this H, and the gate goes on to step 7.202: another agreement payment for this ATR is settling. The gate declinesagreement-pendingand signs nothing.402: thePAYMENT-REQUIREDheader, padded standard base64 (RFC 4648 section 4) of an x402 document, is the agreement's payment request.- anything else:
agreement-failed.
-
Choose the option. The gate reads the request's options in document order and takes the first one your signer can pay with a pairing whose payment is itself a public proof, the same rule as for the payment's own options. An option is payable only if its
maxTimeoutSecondsis a JSON number with an integral value from 1 to 253 − 1 (60and60.0are the same value): the gate passes any other over, and declinesno-payable-optionwhen it finds no option it can pay. The request's legal context must advertise the same H: another H ishash-mismatch, and nothing is signed. -
Hand it over for approval. The call returns
approve, the agreement payment, and signs nothing:Member What it is urlThe agreement URL. optionThe option the gate pays: its amount(in the asset's atomic units),asset,payToandnetworkare the terms the agent approves.requiredThe agreement URL's payment request, as it served it. Your agent approves it as it approves any payment, or does not. To approve it, call again with
approvedset toapprove, unchanged. -
Pay. With
approved, the gate first reads itsrequiredwhole, as step 3 reads the agreement URL's request: a request the protocol package's read refuses as a whole, for its version, its legal context or agreement URL, or anacceptsthat is not a list of at most 32 entries holding an option, is declinedoffer-unreadablewith the read's code, as the unpaid request declines it, and nothing is fetched or signed. Itsoptionmust then be one ofrequired.accepts, compared as the protocol package compares JSON values, by RFC 8785 form: member order does not count, and60.0is60. Any other option is declinedoffer-unreadablewithx402/option-not-in-document, and nothing is fetched or signed. The gate then builds exactly that payment with H and hands it to your signer (or toagreementSigner, where you gave one). It sends it in thePAYMENT-SIGNATUREheader, with no second unpaid request. A signed payment nested past 64 levels, or one JSON cannot write, is declinedsigned-not-bound, "The agreement payment is not plain JSON.", and is not sent. -
Wait for the receipt. After a
202, the gate waits theRetry-Afterseconds (one to nine digits, surrounding spaces, tabs, CRs and LFs aside, at least 1; 2 when absent or in any other form, an HTTP-date included) and sends the same payment again. After a5xxor a timeout it does the same after 2 seconds. Each paid request has a deadline of the option'smaxTimeoutSeconds(at most 120) plus 70 seconds; the whole exchange ends atmaxTimeoutSecondsplus 180 seconds, withagreement-pending. Any other answer, a redirect included, isagreement-failed. Within one call the gate never signs a second agreement payment; every call withapprovedsigns a new one (see Once the agreement payment is sent). A200is the receipt:
{
"atrHash": "0x8b1e122580ae3f6a8c3d36a24294e1260a87bde70f597279b973e39310072938",
"agreed": true,
"network": "eip155:84532",
"transaction": "0xabababababababababababababababababababababababababababababababab"
}atrHash must be this H, agreed must be true, and network and transaction must be strings. Anything else is
agreement-failed, and the payment for the resource is never signed. The receipt is the agreement URL's statement:
the gate checks its form and that its atrHash is H, and does not look up the transaction it names. A 200 to the
first, unpaid request is taken as an agreement already recorded. The public proof is that transaction on network,
which anyone can read.
- Pay the resource. Only then does the gate sign the payment the offer asked for, which it built, unsigned, at the
comparison: a build that refuses declines before any agreement is asked.
transactreturns the receipt beside the payment asagreement.
Every decline after the agreement payment is sent carries it as moved: keep it, and call again without approved to
learn whether it is recorded (see Once the agreement payment is sent).
Ending the exchange
transact and agree take a signal, an AbortSignal, in their options. Aborting it ends the agreement exchange at
its next step: a request in flight is abandoned and a wait is cut short.
- Before the agreement payment is sent, the call declines
agreement-failed. Nothing is sent; the signer may have signed it already, and that signature is discarded. - After it is sent, the call declines
agreement-pending, with the payment asmoved.
The signal ends the agreement exchange only. Once the receipt is in hand, the payment for the resource is signed as usual.
Once the agreement payment is sent
Once the agreement payment is sent, a decline carries it as moved. A decline agreement-pending means the agreement
URL has not recorded the payment in time, or your signal ended the wait, and it may settle yet. To collect the
receipt, call again without approved: the gate asks the agreement URL unpaid and goes on once it answers 200 with
the receipt, or declines agreement-pending again while it answers 202, signing nothing. A 402 returns the
agreement payment for approval again, and whether to pay a second time is your agent's decision. Never call again with
approved for the same agreement: every call with approved signs and sends a new agreement payment.
Example
The seller offers an ERC-7710 payment, whose signed form does not carry H, and names an agreement URL beside the link. The seller builds the agreement URL from a random token, as it builds the link, not from H: unpaid, the agreement URL answers with a challenge that advertises the link. A local stand-in plays the seller and the agreement URL:
import { , type , type , type Signer, type } from "@integraledger/terms";
import { } from "@integraledger/lcp/x402";
import { } from "viem/accounts";
const = new ().(
'{"atrVersion":"1","id":"0f8fad5b-d9cb-469f-a165-70867728950e","x402":{"z":1.0,"a":"caf\\u00e9"},"seller":{"note":"Café — 30 días ✓"}}',
);
const = "0x8b1e122580ae3f6a8c3d36a24294e1260a87bde70f597279b973e39310072938";
const = `https://atr.seller.example/${.()}`;
const = `https://api.seller.example/agreement/${.()}`;
const = {
: "exact",
: "eip155:84532",
: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
: 60,
};
const = { : { : "sha256", : , : }, : {} };
// The resource's offer: an ERC-7710 payment, with the agreement URL beside the link.
const = {
: 2,
: { : "https://api.seller.example/v1/quote" },
: [{ ..., : "10000", : { : "USDC", : "2", : "erc7710" } }],
: {
: { ..., : { ...., : } },
},
};
// The agreement URL's own payment request: one unit, paid with an EIP-3009 authorization whose nonce is H.
const = {
: 2,
: { : },
: [{ ..., : "1", : { : "USDC", : "2" } }],
: { },
};
const : string[] = [];
const : = async (, ) => {
if ( === ) return new ();
if ( !== ) return new (null, { : 404 });
if (.?.["PAYMENT-SIGNATURE"] === ) {
.("agreement URL: 402");
return new (null, { : 402, : { "PAYMENT-REQUIRED": (.()) } });
}
.("agreement URL: paid, 200");
return .({ : , : true, : "eip155:84532", : `0x${"ab".(32)}` });
};
// The published Anvil development key. Never use it for real funds.
const = ("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
const : Signer = {
: `eip155:84532:${.}`,
async (: ): <> {
.(`signer: ${.}`);
if (. === "eip712") {
return .(. as <typeof .>[0]);
}
if (. === "erc7710") {
// A delegation from the wallet's own ERC-7710 tooling; placeholder values for this stand-in.
return { : `0x${"11".(20)}`, : "0x1234", : . };
}
throw new (`unexpected ${.}`);
},
};
// The first call returns the agreement payment for approval, and signs nothing.
const = await (, , , );
if ("decline" in ) throw new (`${..}: ${..}`);
if (!("approve" in )) throw new ("this pairing pays an agreement first");
const { , , , } = ..;
.(`approve: ${} of ${} to ${} on ${}`);
// Your agent approves it like any other payment; the second call carries it back unchanged.
const = await (, , , , { : . });
if ("decline" in ) throw new (`${..}: ${..}`);
if ("approve" in ) throw new ("the approved agreement payment is paid in this call");
.(.("\n"));
.("receipt for H:", .?. === );agreement URL: 402
approve: 1 of 0x036CbD53842c5426634e7929541eC2318f3dCF7e to 0x209693Bc6afc0C5328bA36FaF03C514EF312287C on eip155:84532
signer: eip712
agreement URL: paid, 200
signer: erc7710
receipt for H: trueThe first call asked the agreement URL for its payment request and returned it, with no signer call. The second call signed the approved agreement payment (EIP-712, carrying H), and asked for the ERC-7710 delegation only after the receipt.
Driving the steps yourself
agree(bytes, url, signer, fetch, { approved, inputs, signal }) runs steps 2 to 6 on their own, for an agent that
confirms with confirm and finishes with finish. confirm and finish neither pay nor check the agreement; only
transact and agree do. When chosen.agreement is set, the pairing's payment is not itself a public proof of H: do
not sign yet. Call agree(bytes, chosen.agreement, signer, fetch), have your agent approve the approve it returns,
call agree again with { approved }, and sign request only with its receipt in hand:
confirmreturnschosen.agreement, the URL, beside the request. Do not sign the request yet.- Call
agreewithconfirmed.bytes, that URL and your signer. It returns{ approve, bytes, h }and signs nothing, or{ receipt }where the agreement is already recorded. - Once your agent approves it, call
agreeagain withapproved: approve. It returns{ receipt }. - Only with the receipt in hand, sign
confirmed.requestand callfinish.finishtakes no receipt: it completes the payment whether or not the agreement is paid.
For x402/batch-settlement/cloudflare, confirm returns request: null, but its build is the payment: transact
returns it as signed, and after confirm you get it with finish(bytes, chosen, null, binding), once the receipt is
in hand.
The MCP tools keep the same order: atr_confirm returns no request until you pass a receipt for H, and atr_agree
returns the agreement payment to approve before it signs anything. As with transact, the receipt is the agreement
URL's statement: atr_confirm checks its form and that its atrHash is H, and does not look up the transaction it
names.
When the offer names no agreement URL
A pairing whose payment is not a public proof, in an offer with no agreement URL, is declined with
agreement-not-offered right after the comparison. Nothing is signed.
Last updated on