Seller
Assemble the ATR, advertise its hash, check the payment, and read the settlement.
The seller is the party serving the resource. Its side of the binding has four steps: assemble the ATR for this
request, advertise H in the challenge, read H back from the payment it receives, and read the settlement from the
rail. This guide walks them on x402 with x402/exact/eip155/eip3009. Every pairing has the same members, so the
shape is the same on every surface.
sequenceDiagram
participant B as Buyer
participant S as Seller
participant St as Seller's storage
participant F as Facilitator
B->>S: GET /v1/quote
Note over S: requestCommitment, tie, assemble
S->>St: write the ATR's bytes at the link
Note over S: advertise H and the link in the 402
S-->>B: 402 PaymentRequired
B->>S: the payment, carrying H
Note over S: bound(payment) equals the H it issued
S->>F: verify and settle
F-->>S: the settlement transaction
Note over S: reference, then status through a reader
The facilitator is the x402 role that verifies and settles. It is the seller's to choose; this package does not call it.
1. Assemble the ATR for this request
The ATR's binding slot ties the record to one payment. On x402, tie(accepts, request) records every option the
challenge offers, exactly as issued, and the commitment to the request it answers: the method, the path, the query
and SHA-256 over the body exactly as received. The party slots carry whatever the parties agreed, as bytes.
2. Store the bytes and link them
Write the ATR's bytes to your own storage before the challenge goes out, and serve them unchanged at an https link.
If the write fails, send no challenge: a buyer that cannot fetch the record declines.
3. Advertise H
advertise(doc, h, link, offer) returns a copy of the challenge with extensions.legalContext set to
{"type":"sha256","value":H,"legalContextUrl":link}, and leaves every other extension and every option as they were.
On pairings that carry H in the option itself, such as x402/exact/solana (extra.memo), it also places H there.
4. Check the payment
bound(payment) reads H from what the buyer presented: here, the EIP-3009 authorization's nonce. Match it with
hashEquals against the H you issued for this request. The token contract verifies the signature when it executes
the transfer; bound does not.
Refuse a payment whose H you did not issue for this request, and one whose H you have already accepted. Those two facts are your records, not this package's.
5. Read the settlement
reference(payment) gives the keys for finding this payment on chain: the network, the token, the time bound, a
digest of the transfer, and the log that carries H. After the facilitator settles, status reads the settlement
transaction through a reader you supply, and recover reads H back from that transaction alone.
The whole flow
This program runs every step above. A Map stands in for the seller's storage, a random key for the buyer's signer,
and a reader built from one receipt for the chain. No network is used.
import { , , } from "@integraledger/lcp";
import { , type EvmReader, type } from "@integraledger/lcp/evm";
import {
,
,
,
type ,
type ,
} from "@integraledger/lcp/x402";
import type { } from "viem";
import { , } from "viem/accounts";
const : = {
: "exact",
: "eip155:84532",
: "10000",
: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
: 60,
: { : "USDC", : "2" },
};
const : = {
: 2,
: { : "https://api.seller.example/v1/quote" },
: [],
};
// 1. Assemble the ATR for this request.
const = await ({ : "GET", : "/v1/quote", : new () });
if ("refused" in ) throw new (.);
const = new ().('{"text":"One quote for 10000 base units of USDC."}');
const = await ((), (., ), [["terms", ]]);
if ("refused" in ) throw new (.);
// 2. Store the bytes and link them.
const = `https://atr.seller.example/${.}`;
const = new ([[, .]]);
// 3. Advertise H.
const = .(, ., , );
if ("refused" in ) throw new (.);
.("advertised:", .(.?.["legalContext"]?.) === .({
: "sha256",
: .,
: ,
}));
// The buyer's side, in brief: see the buyer guide.
const = (());
const = await .({ : , : , : ., : 1790000000 }, .);
if ("refused" in ) throw new (.);
const = .(await .(. as ));
if ("refused" in ) throw new (.);
// 4. Check the payment.
const = await .();
if (typeof !== "string") throw new (.);
.("bound to the issued H:", (, .), .());
// 5. Read the settlement. The facilitator's settle answer names the transaction.
const = await .();
if ("refused" in ) throw new (.);
const : = `0x${"11".(32)}`;
const : EvmReader = {
: "eip155:84532",
: async () => ({
: 1,
: 100n,
: [
{
: . as ,
: [, `0x${..(2).().(64, "0")}`, ],
: "0x",
},
],
}),
: async () => ( === "finalized" ? 100n : 105n),
: async () => null,
};
const = await .({ : ., : ., , }, );
.("status:", ., "finality" in ? . : .);
const = await .({ : ., : ., }, );
.("recovered from the chain alone:", typeof === "string" && (, .));advertised: true
bound to the issued H: true true
status: settled finalized
recovered from the chain alone: trueWhat the seller keeps
Keep H, the link and the reference for each payment. The bytes live in your storage at the link; the buyer keeps its own copy.
What this package does not do
It holds no key and signs nothing. It does not call the facilitator, move funds, or decide whether to serve a request. It never compares amount, payee, asset, timing or payer with the ATR's content: the binding is the one thing it checks.
On other surfaces
The members are the same everywhere; what tie, advertise and bound handle differs:
- x402: the
legalContextextension, and every x402 pairing. - MPP: H as the challenge id.
- Agentic checkouts: ACP, UCP, AP2, ACK, card networks and A2A.
- Channels, sessions and subscriptions: one ATR for a whole channel.
Last updated on