Buyer
Read the challenge, compare the served bytes with H, build and sign, and finish.
The buyer's side of the binding is the buyer gate: compare, then build and sign, then
finish. This guide writes it with this package's pieces on x402 with x402/exact/eip155/eip3009. For a ready-made
gate with the fetch bounds and declines already in place, use the buyer packages in
integra-agentic-terms.
1. Compare
read(doc) gives H, the link, and the options the pairing can pay. Fetch the link with bounds, keep the bytes exactly
as received, and compare their hash with H. On any failure, stop before anything is signed.
The fetch below is the one the shared vectors fix: one GET, no redirects, a 10-second deadline over headers and
body, and at most 1 MiB, counted as the body streams. It asks with redirect: "manual" and reads a 3xx answer, or the
opaqueredirect answer with status 0 that browsers give, as a failure. Cloudflare Workers refuse
redirect: "error" with a TypeError, and under "manual" a Worker's fetch answers a redirect with the 3xx
itself (measured on Cloudflare's runtime with wrangler 4.141.0).
2. Build and sign
build(choice, h) returns what the signer signs with H in its place. On x402/exact/eip155/eip3009 that is EIP-712
typed data for TransferWithAuthorization whose nonce is H, valid until now plus the option's
maxTimeoutSeconds. Your signer signs it; this package never holds a key.
Pass the H you compared. Never read H from the document again between the comparison and build.
3. Finish
complete(signature) gives the x402 payment, echoing the challenge's resource and extensions unchanged. Before
sending it, read H back with bound(payment) and check that it equals the H you compared: the signer signed what
you asked it to sign.
The whole flow
The seller's challenge below was advertised for an ATR the buyer can fetch. A stand-in fetch serves the bytes, so no
network is used.
import { , } from "@integraledger/lcp";
import { , type } from "@integraledger/lcp/x402";
import type { } from "viem";
import { , } from "viem/accounts";
const = 1_048_576;
/** One GET, no redirects, a 10-second deadline, and at most 1 MiB. The bytes as received, or why not. */
async function (: string, : typeof = ): < | string> {
const = new ();
const = (() => .(), 10_000);
try {
const = await (, { : "manual", : . });
const = . === "opaqueredirect" || (. >= 300 && . <= 399);
if ( || . !== 200 || . === null) return "atr-unfetchable";
const : [] = [];
let = 0;
for await (const of .) {
+= .;
if ( > ) {
.();
return "atr-too-large";
}
.();
}
const = new ();
let = 0;
for (const of ) {
.(, );
+= .;
}
return ;
} catch {
return "atr-unfetchable";
} finally {
();
}
}
// What the seller served: the ATR's bytes at the link, and the 402 challenge that advertises their hash.
const = new ().('{"atrVersion":"1","id":"0f8fad5b-d9cb-469f-a165-70867728950e","terms":"10000 units"}');
const = "https://atr.seller.example/quote-0001";
const = await ();
const : = {
: 2,
: { : "https://api.seller.example/v1/quote" },
: [
{
: "exact",
: "eip155:84532",
: "10000",
: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
: 60,
: { : "USDC", : "2" },
},
],
: {
: {
: { : "sha256", : , : },
: {},
},
},
};
const : typeof = async () => new ();
// 1. Compare.
const = .();
if ("refused" in ) throw new (`decline: ${.}`);
const = await (., );
if (typeof === "string") throw new (`decline: ${}`);
if (!(await (), .)) throw new ("decline: hash-mismatch");
.("compared:", . === );
// 2. Build and sign.
const = (());
const = ..[0]!;
const = await .(
{ : , , : ., : .(.() / 1000) },
.,
);
if ("refused" in ) throw new (`decline: ${.}`);
.("signing:", .., "with nonce H:", ... === .);
const = await .(. as );
// 3. Finish.
const = .();
if ("refused" in ) throw new (`decline: ${.}`);
const = await .();
if (typeof !== "string" || !(, .)) throw new ("decline: signed-not-bound");
.("finished: the payment carries H");compared: true
signing: TransferWithAuthorization with nonce H: true
finished: the payment carries HSend payment to the seller in x402's PAYMENT-SIGNATURE header, as the x402 specification describes, and keep
served: it is your copy of what you agreed to.
Choosing among options
A challenge may offer several options on several rails. pairingOf(option) names the pairing that serves each one,
and each pairing's read returns only the options it can pay. Choose an option your accounts can pay, then use that
pairing's build. The same H rides in whichever you choose.
When the challenge carries an agreement URL
Some pairings' payments carry H in nothing public. For those, read also returns agreement: an https URL the
buyer pays first, whose payment carries H publicly. Pay the full payment only after the agreement's 200 receipt
names the same H. See the agreement URL.
Channels and sessions
In a channel, the buyer compares once, at the opening: one ATR covers the whole channel. See Channels, sessions and subscriptions.
Last updated on