Skip to content
Integra Protocol

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 H

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

Edit on GitHub

Last updated on

On this page