Pay with the gate
Compare, then build and sign, then finish. One call with transact, or two with confirm and finish when your signer lives elsewhere.
Every payment through the gate follows the same order: compare the served bytes with H, build the payment with the hash
the gate computed, have your signer sign exactly that request, and finish only when the pairing reads H from the
completed payment, or the proof is the agreement payment. You can run the whole order with one call, transact, or
split it with confirm and finish.
What you pass
Every gate function takes the same few values:
| Value | What it is |
|---|---|
doc | The seller's payment request, as the protocol delivers it: an x402 PaymentRequired object, a list of MPP challenges, or the pairing's document. Pass it as parsed JSON; the gate never needs the raw response. |
binding | The pairing's binding, from @integraledger/lcp (TypeScript) or a constant of integraledger_terms (Python). The pairings reference names each one. |
signer | Your wallet: an account in CAIP-10 form (eip155:84532:0xf39F…2266) and a sign(request) method. See signers. |
fetch | Your HTTP client. TypeScript: WHATWG fetch, or anything with its call shape. Python: an httpx.AsyncClient. Your network policy (proxy, egress rules, timeouts of your own) lives here. |
inputs | The buyer's own values a pairing's build needs beside the offer, such as a recent blockhash or an account sequence. Most pairings need none. See rails. |
The account names the network the buyer pays on. The gate pays the first option (an x402 accepts entry, an MPP
challenge) in document order that the pairing serves on the account's network. To pay a different option, remove the
others from the document before you pass it.
One call: transact
transact compares, pays the agreement your agent approved where the pairing needs one, calls your signer, and
finishes:
import { } from "@integraledger/terms";
import { } from "@integraledger/lcp/x402";
import type { Signer } from "@integraledger/terms";
declare const : unknown; // the seller's PAYMENT-REQUIRED document, parsed
declare const : Signer;
const = await (, , , globalThis., { : {} });
if ("decline" in ) {
.(.., ..);
} else if ("approve" in ) {
// The pairing needs an agreement payment first, and nothing was signed. result.approve.option is its amount, asset,
// payTo and network. Once your agent approves it, call transact again with { approved: result.approve }.
} else {
// result.signed: the payment to send (null when the pairing has nothing for the buyer to sign)
// result.bytes: the ATR's exact bytes, your copy of the record
// result.h: their SHA-256
// result.agreement: the agreement's receipt, where the pairing needs one
// result.landed: a landed receipt to keep beside the payment, for the few pairings that return one
}The signer is called only after the comparison matched: for this pairing's requests, and, without agreementSigner,
first for the approved agreement payment. On any decline before it, the signer is never called.
transact's options take four members, all optional:
inputs: the buyer's own values the build needs.agreementSigner: the signer that pays the agreement, where it differs from the one that pays the resource (for example a card payment whose agreement is paid from an EVM wallet). Without it,signerpays both.approved: the agreement payment your agent approved, exactly as a previous call returned it inapprove. See agreement payments.signal: anAbortSignalthat ends the agreement exchange.
Any other member, or a member of the wrong kind, is declined with no-payable-option and the detail
<protocol>/input-malformed, before any fetch, and so is a signer or agreementSigner whose account is not a
string.
Two calls: confirm, then finish
When the key lives somewhere else (a hardware wallet, a signing service, another process), split the order:
confirm(doc, binding, account, fetch, inputs)reads the offer, fetches and compares, and returns the signing request built with H, together withchosenand the ATR's bytes.- Your signer signs
request, exactly as given. finish(bytes, chosen, signature, binding)rebuilds the same payment fromchosenand the bytes, joins the signature, and returns the payment only when the pairing reads the hash of those bytes from the completed payment, or, where the proof is the agreement payment, without that reading.
chosen is plain JSON and the bytes are bytes, so both cross process boundaries. finish does not trust anything it
did not rebuild: it recomputes H from the bytes you hand back.
import { , , type } from "@integraledger/terms";
import { , type } 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 = {
: 2,
: { : "https://api.seller.example/v1/quote" },
: [
{
: "exact",
: "eip155:84532",
: "10000",
: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
: 60,
: { : "USDC", : "2" },
},
],
: { : { : { : "sha256", : , : }, : {} } },
};
const : = async () => ( === ? new () : new (null, { : 404 }));
// 1. Compare, and build the request with H.
const = await (, , "eip155:84532:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", );
if ("decline" in ) throw new (..);
if (. === null || .. !== "eip712") throw new ("expected an EIP-712 request");
// What crosses to the signing side and back: plain JSON and the bytes.
const = .({ : ., : .(.).("base64") });
// 2. Sign exactly the request, elsewhere. The published Anvil development key; never use it for real funds.
const = ("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
const = await .(
.. as <typeof .>[0],
);
// 3. Finish from what came back.
const = .();
const = await (new (.(.atr, "base64")), .chosen, , );
if ("decline" in ) throw new (..);
if ("next" in ) throw new ("this pairing signs in one step");
.("nonce is H:", (. as )... === );nonce is H: trueA request of null
Some pairings give the buyer nothing to sign: the seller's own flow completes the payment (an MPP card or stripe
charge, an ACP checkout without delegation, a UCP checkout without a mandate). For these, confirm returns request: null after the comparison, and transact returns signed: null. The comparison was the whole of your step; complete
the payment through the protocol's own flow.
For a pairing whose build is itself the payment (x402/batch-settlement/cloudflare), confirm also returns request: null: call finish with a null signature, and it completes the payment with no signer call, as transact does.
Payments signed in steps
A few pairings take two signatures: a channel funding and then its first voucher (mpp/session/evm and others). For
these, finish returns { next, h } instead of the payment. Have your signer sign next, then call finish again with
every answer so far, in order, as a list:
import { , type Chosen, type , type } from "@integraledger/terms";
import type { } from "@integraledger/terms";
declare const : ;
declare const : Chosen;
declare const : ;
declare const : ;
declare function (: ): <>;
const : [] = [await ()];
let = await (, , [0]!, );
while (!("decline" in ) && "next" in ) {
.(await (.));
= await (, , , );
}transact does this for you. It never calls the signer more than twice for one payment.
Sending the payment
The gate returns the payment in the protocol's own form. You send it as that protocol defines:
| Protocol | How the payment travels |
|---|---|
| x402 over HTTP | Base64 of the UTF-8 bytes of the payment's JSON, in the PAYMENT-SIGNATURE request header, on a retry of the original request. |
| x402 over MCP | The payment object in the tool call's _meta["x402/payment"]. |
| MPP | The credential in the field the challenge selects, Authorization by default, after the Payment scheme name. |
| Card, AP2, UCP, ACP, ACK | The pairing's own message: the TAP field, the mandate, the checkout completion, as its protocol defines. |
Where transact or finish also returned landed, keep it beside the payment. check reads the payment with it. The
caller passes as landed only the receipt its own reader read for the payment, as finish or transact returned it,
never a value another party sent.
After you send it
- The seller answers that the payment is still settling. Send the same payment again. Never sign a new one: the payment already carries H, and a second signature would be a second payment.
- A decline carries
moved. Your signer moved the payment itself (a Lightning invoice paid, a transaction broadcast) before the gate declined.moved.signedis that payment, with the bytes and their hash. Keep it and present it again rather than signing a new one. See keeping the record. - The agreement payment may settle yet. Once the agreement payment is sent, a decline is
agreement-pendingwith the payment asmoved. To collect the receipt, call again withoutapproved: the gate asks the agreement URL unpaid and goes on once it answers200with the receipt, or declinesagreement-pendingagain while it answers202, signing nothing. Never call again withapprovedfor the same agreement: every call withapprovedsigns and sends a new agreement payment. - Any other decline. Nothing was sent, and nothing should be. The declines reference says what each code means. Whether to deal with the seller another way is your principal's decision.
In a Cloudflare Worker
Pass the Worker's own fetch. The gate asks with redirect: "manual": the Workers runtime refuses
redirect: "error" with a TypeError, and under "manual" it answers a redirect with the 3xx itself.
Measured on Cloudflare's runtime (wrangler dev --remote, wrangler 4.141.0, compatibility date 2026-09-01), with this
package, @integraledger/lcp and every optional peer dependency of lcp installed:
- With and without the
nodejs_compatflag,confirmfetched a link that answered200, hashed the bytes and compared them with the advertised hash. - A link that answered
301or302was declinedatr-unfetchable, and the fetch did not throw. - Without the optional peer dependencies, wrangler's build stopped at
Could not resolvefor each one the bundle imports.
In Python
The Python gate has the same functions, with Python names and dataclass results:
| TypeScript | Python |
|---|---|
transact(doc, binding, signer, fetch, { inputs, agreementSigner, approved, signal }) | await transact(doc, binding, signer, client, inputs=…, agreement_signer=…, approved=…, signal=…) |
confirm(doc, binding, account, fetch, inputs) | await confirm(doc, binding, account, client, inputs) |
finish(bytes, chosen, signature, binding) | finish(atr_bytes, chosen, signature, binding) |
check(bytes, presented, binding, landed?) | check(atr_bytes, presented, binding, landed=None) |
{ decline: { code, detail } } | Declined(code, detail, moved) |
See the Python guide.
Last updated on