Getting started
Install the buyer gate, pay a seller's x402 request only when the served ATR matches its hash, and watch the gate refuse a tampered record.
This page takes you from nothing to a signed payment in TypeScript and in Python. Both examples use the seller document, the ATR and the payer key from the shared vectors, and a local stand-in for the network, so they run offline and print the same values every time.
Requirements
- TypeScript: Node.js
>=26.11.1. The package is ESM only. - Python: Python
>=3.11.
Install
npm install @integraledger/terms@0.3.1 @integraledger/lcp@0.4.2@integraledger/terms is the gate. @integraledger/lcp exports the bindings you pass to it, one per pairing.
pip install integraledger-termsThe Python package is self-contained: its bindings are part of it.
The examples below also use a wallet library to sign: viem in TypeScript
(npm install viem) and eth-account in Python
(pip install eth-account). The gate itself never signs; any wallet that can sign the request works.
Pay a seller, in TypeScript
The seller answered a request with an x402 402 Payment Required. Its PAYMENT-REQUIRED document offers one option on
Base Sepolia (eip155:84532) and, in extensions.legalContext, advertises H and the link to the ATR.
import { , type , type Signer } from "@integraledger/terms";
import { , type } from "@integraledger/lcp/x402";
import { } from "viem/accounts";
// The ATR, exactly as the seller's link serves it. The gate hashes these bytes and never parses them.
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/${.()}`;
// The seller's x402 payment request, advertising H and the link.
const = {
: 2,
: { : "https://api.seller.example/v1/quote" },
: [
{
: "exact",
: "eip155:84532",
: "10000",
: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
: 60,
: { : "USDC", : "2" },
},
],
: {
: { : { : "sha256", : , : }, : {} },
},
};
// A stand-in for the network: the link serves the ATR. In production, pass globalThis.fetch.
const : = async () => ( === ? new () : new (null, { : 404 }));
// The published Anvil development key. Never use it for real funds.
const = ("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
const : Signer = {
: `eip155:84532:${.}`,
async () {
if (. !== "eip712") throw new (`this wallet signs EIP-712 only, not ${.}`);
return .(. as <typeof .>[0]);
},
};
const = await (, , , );
if ("decline" in ) throw new (`${..}: ${..}`);
if ("approve" in ) throw new ("this pairing pays no agreement first");
const = . as ;
.("H ", .);
.("signed nonce", ...);
.("kept bytes ", ..);
// x402 over HTTP sends the payment as base64 of the UTF-8 bytes of its JSON, in the PAYMENT-SIGNATURE header.
const = .(.(.), "utf8").("base64");
.("header ", . > 0 ? "ready" : "empty");H 0x8b1e122580ae3f6a8c3d36a24294e1260a87bde70f597279b973e39310072938
signed nonce 0x8b1e122580ae3f6a8c3d36a24294e1260a87bde70f597279b973e39310072938
kept bytes 138
header readyWhat happened:
transactread the offer through thex402/exact/eip155/eip3009binding and chose the option on the signer's network.- It fetched the link and computed SHA-256 over the 138 bytes it received. The result matched the advertised H.
- It built an EIP-3009
TransferWithAuthorizationwhosenonceis that hash, and handed it to your signer. - It completed the payment with the signature, read the nonce back out of what was signed, and returned the payment because the nonce is the hash of the bytes it compared.
Keep result.bytes with the payment. They are your copy of the record, and the hash in the payment shows which record
it was.
Watch the gate refuse
Change one byte of what the link serves: 30 días becomes 31 días. The seller still advertises the same H.
import { , type , type Signer } from "@integraledger/terms";
import { } from "@integraledger/lcp/x402";
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", : , : }, : {} } },
};
// The link serves a record that differs from the advertised one by one byte.
const = new ().(
'{"atrVersion":"1","id":"0f8fad5b-d9cb-469f-a165-70867728950e","x402":{"z":1.0,"a":"caf\\u00e9"},"seller":{"note":"Café — 31 días ✓"}}',
);
const : = async () => new ();
let = 0;
const : Signer = {
: "eip155:84532:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
async () {
++;
return "0x";
},
};
const = await (, , , );
.("decline ", "decline" in ? .. : "none");
.("signer calls", );decline hash-mismatch
signer calls 0The signer was never called. A decline is a value, never an exception: every gate function returns either its result
or { decline: { code, detail } }. The declines reference lists every code.
Pay a seller, in Python
The same payment, with httpx for the network and eth-account as the wallet.
import asyncio
import uuid
import httpx
from eth_account import Account
from eth_account.messages import encode_typed_data
from integraledger_terms import X402_EXACT_EIP155_EIP3009, Declined, transact
# The ATR, exactly as the seller's link serves it.
ATR = (
'{"atrVersion":"1","id":"0f8fad5b-d9cb-469f-a165-70867728950e","x402":{"z":1.0,"a":"caf\\u00e9"},'
'"seller":{"note":"Café — 30 días ✓"}}'
).encode()
H = "0x8b1e122580ae3f6a8c3d36a24294e1260a87bde70f597279b973e39310072938"
LINK = f"https://atr.seller.example/{uuid.uuid4()}"
OFFER = {
"x402Version": 2,
"resource": {"url": "https://api.seller.example/v1/quote"},
"accepts": [
{
"scheme": "exact",
"network": "eip155:84532",
"amount": "10000",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
"maxTimeoutSeconds": 60,
"extra": {"name": "USDC", "version": "2"},
}
],
"extensions": {"legalContext": {"info": {"type": "sha256", "value": H, "legalContextUrl": LINK}, "schema": {}}},
}
# The published Anvil development key. Never use it for real funds.
KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
class Wallet:
account = "eip155:84532:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
async def sign(self, request):
if request["kind"] != "eip712":
raise ValueError(f"this wallet signs EIP-712 only, not {request['kind']}")
signed = Account.sign_message(encode_typed_data(full_message=dict(request["typedData"])), KEY)
return "0x" + bytes(signed.signature).hex()
def seller(request: httpx.Request) -> httpx.Response:
"""A stand-in for the network: the link serves the ATR."""
return httpx.Response(200, content=ATR) if str(request.url) == LINK else httpx.Response(404)
async def main() -> None:
async with httpx.AsyncClient(transport=httpx.MockTransport(seller)) as client:
result = await transact(OFFER, X402_EXACT_EIP155_EIP3009, Wallet(), client)
if isinstance(result, Declined):
raise SystemExit(f"{result.code}: {result.detail}")
assert result.signed is not None
print("H ", result.h)
print("signed nonce", result.signed["payload"]["authorization"]["nonce"])
print("kept bytes ", len(result.atr_bytes))
asyncio.run(main())H 0x8b1e122580ae3f6a8c3d36a24294e1260a87bde70f597279b973e39310072938
signed nonce 0x8b1e122580ae3f6a8c3d36a24294e1260a87bde70f597279b973e39310072938
kept bytes 138In production, pass an httpx.AsyncClient() of your own. The gate asks it for one GET of the link, with no redirect.
Next
- Concepts: the record, the hash, pairings, bindings and the gate.
- Pay with the gate:
transact, andconfirmthenfinishfor a signer that lives elsewhere. - Signers: every kind of signing request, and what your signer returns for each.
- Rails: what each rail needs from you.
- MCP server: give an agent the gate as tools.
Last updated on