Skip to content
Integra Agentic Terms

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:

ValueWhat it is
docThe 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.
bindingThe pairing's binding, from @integraledger/lcp (TypeScript) or a constant of integraledger_terms (Python). The pairings reference names each one.
signerYour wallet: an account in CAIP-10 form (eip155:84532:0xf39F…2266) and a sign(request) method. See signers.
fetchYour 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.
inputsThe 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, signer pays both.
  • approved: the agreement payment your agent approved, exactly as a previous call returned it in approve. See agreement payments.
  • signal: an AbortSignal that 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:

  1. confirm(doc, binding, account, fetch, inputs) reads the offer, fetches and compares, and returns the signing request built with H, together with chosen and the ATR's bytes.
  2. Your signer signs request, exactly as given.
  3. finish(bytes, chosen, signature, binding) rebuilds the same payment from chosen and 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: true

A 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:

ProtocolHow the payment travels
x402 over HTTPBase64 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 MCPThe payment object in the tool call's _meta["x402/payment"].
MPPThe credential in the field the challenge selects, Authorization by default, after the Payment scheme name.
Card, AP2, UCP, ACP, ACKThe 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.signed is 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-pending with the payment as moved. 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. Never call again with approved for the same agreement: every call with approved signs 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_compat flag, confirm fetched a link that answered 200, hashed the bytes and compared them with the advertised hash.
  • A link that answered 301 or 302 was declined atr-unfetchable, and the fetch did not throw.
  • Without the optional peer dependencies, wrangler's build stopped at Could not resolve for each one the bundle imports.

In Python

The Python gate has the same functions, with Python names and dataclass results:

TypeScriptPython
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.

Edit on GitHub

Last updated on

On this page