Skip to content
Integra Protocol

Getting started

Install @integraledger/lcp, assemble an ATR, and carry its hash through one x402 payment.

This page takes you from an empty folder to one x402 payment whose signed nonce is the hash of the agreement's record. You play both parties, the seller and the buyer, in one program. Nothing touches a network: a Map stands in for the seller's storage, and a random key for the buyer's wallet.

Three terms carry the whole page:

  • The Agentic Transaction Record (ATR) is the agreement's record, a JSON document the seller serves.
  • The ATR hash (H) is SHA-256 over the ATR's exact bytes.
  • Binding is how H rides in a pairing's payment: the field its specification defines. Here it is the nonce of the EIP-3009 authorization the buyer signs.

Requirements

  • Node.js >=26.10.0. Node runs the TypeScript files on this page directly, by stripping their types.
  • An ESM project. With TypeScript, set "module" and "moduleResolution" to "nodenext" (or "moduleResolution" to "bundler"), so the package's subpath exports resolve.

Install

mkdir lcp-start && cd lcp-start
npm init -y
npm pkg set type=module
npm install @integraledger/lcp viem

@integraledger/lcp is the package; viem signs for the buyer in step 3. Any EIP-712 signer works in its place: the package holds no key and signs nothing.

1. Assemble an ATR

The seller writes the record. assemble(id, binding, content) writes one JSON object in a fixed order: the format marker, the per-transaction id, the binding slot that ties the record to one payment, and then the parties' content, byte for byte as given. It returns the bytes and H.

Save this as assemble.ts and run node assemble.ts:

import {  } from "@integraledger/lcp";

const  = new ().('{"text":"One market report for 10000 base units of USDC."}');
const  = await ("0f8fad5b-d9cb-469f-a165-70867728950e", ["x402", { : [] }], [["terms", ]]);
if ("refused" in ) throw new (.);

.(new ().(.));
.(.);
{"atrVersion":"1","id":"0f8fad5b-d9cb-469f-a165-70867728950e","x402":{"accepts":[]},"terms":{"text":"One market report for 10000 base units of USDC."}}
0x505084d776e5fe9e674b530133325906510c09f6cab2ac5e94b416c0f0917189

This page fixes the id so your output matches. For a real transaction, pass newAtrId(), a random RFC 9562 UUID: the fresh id makes every H unique, even for identical terms.

Every function in the package returns its result or a refusal, a value with a code, so check for refused before you use a result.

2. Compare the bytes with H

The buyer never trusts a record it has not hashed. It fetches the bytes the seller serves, hashes them, and compares the result with the H the seller advertised. One changed byte, here the final . of the terms turned into !, gives another hash:

import { , ,  } from "@integraledger/lcp";

const  = new ().('{"text":"One market report for 10000 base units of USDC."}');
const  = await ("0f8fad5b-d9cb-469f-a165-70867728950e", ["x402", { : [] }], [["terms", ]]);
if ("refused" in ) throw new (.);

const  = ..();
.("as served:", (await (), .));

[. - 4] = 0x21;
.("one byte changed:", (await (), .));
as served: true
one byte changed: false

hashEquals compares the 32 bytes the two strings decode to, in either case. This comparison is the buyer gate: if it fails, the buyer signs nothing.

3. Carry H through one x402 payment

The whole exchange, on x402/exact/eip155/eip3009: USDC on Base Sepolia, paid with an EIP-3009 authorization. Save this as pay.ts and run node pay.ts:

import { , , ,  } from "@integraledger/lcp";
import {
  ,
  ,
  ,
  type ,
  type ,
} from "@integraledger/lcp/x402";
import type {  } from "viem";
import { ,  } from "viem/accounts";

// Seller: the x402 challenge for GET /v1/report, with one option.
const :  = {
  : "exact",
  : "eip155:84532",
  : "10000",
  : "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  : "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
  : 60,
  : { : "USDC", : "2" },
};
const :  = {
  : 2,
  : { : "https://api.seller.example/v1/report" },
  : [],
};

// Seller, a: assemble the ATR for this request. Its binding slot records the options and the request.
const  = await ({ : "GET", : "/v1/report", : new () });
if ("refused" in ) throw new (.);
const  = new ().('{"text":"One market report for 10000 base units of USDC."}');
const  = await ((), (., ), [["terms", ]]);
if ("refused" in ) throw new (.);

// Seller, b: store the bytes at an https link before the challenge goes out.
const  = `https://atr.seller.example/${.}`;
const  = new ([[, .]]);

// Seller, c: advertise H and the link in the challenge's legalContext extension.
const  = .(, ., , );
if ("refused" in ) throw new (.);
const  = .?.["legalContext"]?. as { ?: string } | undefined;
.("1. challenge carries H:", ?. === .);

// Buyer, d: read H and the link, fetch the bytes, and compare.
const  = .();
if ("refused" in ) throw new (.);
const  = .(.);
if ( ===  || !(await (), .)) throw new ("decline: hash-mismatch");
.("2. the served bytes hash to H");

// Buyer, e: build the authorization with H as its nonce, sign it, and complete the payment.
const  = (());
const  = await .(
  { : , : ..[0]!, : ., : .(.() / 1000) },
  .,
);
if ("refused" in ) throw new (.);
const { ,  } = .;
.("3. the buyer signs", , "with nonce H:", . === .);
const  = .(await .(. as ));
if ("refused" in ) throw new (.);

// Seller, f: read H back from what the buyer signed, and match it to the H it issued.
const  = await .();
if (typeof  !== "string") throw new (.);
.("4. the payment is bound to the ATR:", (, .));
1. challenge carries H: true
2. the served bytes hash to H
3. the buyer signs TransferWithAuthorization with nonce H: true
4. the payment is bound to the ATR: true

What each step did:

StepSideCallWhat it gives
asellerrequestCommitment, tie, assembleThe ATR's bytes and H, with the challenge's options and the request in the binding slot.
bselleryour storageThe bytes at an https link, before any buyer can ask for them.
cselleradvertiseThe challenge with extensions.legalContext set to H and the link.
dbuyerread, hash, hashEqualsH and the link from the challenge, and the check that the served bytes hash to H.
ebuyerbuild, completeEIP-712 typed data whose nonce is H, and the x402 payment around the buyer's signature.
fsellerboundH read back from the signed authorization.

On a live network, the buyer sends payment in x402's PAYMENT-SIGNATURE header, the seller's facilitator verifies and settles it, and the token contract's AuthorizationUsed event carries H on chain. The seller guide continues from step f to the settlement read.

Next

Edit on GitHub

Last updated on

On this page