Skip to content
Integra Agentic Terms

Agreement payments

For a payment that is not itself a public proof of H, the buyer's agent approves a small agreement payment that carries H, and transact signs the payment for the resource only after its receipt.

A card charge, a Stripe payment, a Lightning invoice, a checkout in which the buyer signs nothing: these payments do not put H anywhere public. For such a pairing, the proof of agreement is a second, small payment that does: the agreement payment. The seller names an agreement URL in its offer. The agreement payment is a payment like any other, so the buyer's agent approves it before anything is signed. transact then pays it, waits until it is recorded, and only then signs the payment for the resource. confirm and finish do none of this; see Driving the steps yourself.

The pairings reference marks each pairing whose payment is not a public proof. In the registry, it is pattern.publicProof === false.

The exchange

Your signerAgreement URLSellerBuyer gateYour agentYour signerAgreement URLSellerBuyer gateYour agenttransact(offer)GET the ATR link, compare with HGET (no payment)402, PAYMENT-REQUIRED for the same Happrove: the agreement payment (nothing signed)transact(offer, { approved })GET the ATR link, compare with Hsign the approved agreement payment (it carries H)signatureGET with PAYMENT-SIGNATURE202, Retry-After (still settling)the same payment again200, the receiptsign the payment for the resource
  1. Compare. The gate fetches and compares the ATR as for any pairing. A mismatch ends here.

  2. Ask. The gate sends one GET to the agreement URL, with no payment, no redirect and a 10-second deadline. This request and every paid one ask for Accept-Encoding: identity; a 200 or a 402 with any other Content-Encoding is agreement-failed, its body unread.

    • 200: taken as an agreement already recorded. The body must be the receipt for this H, and the gate goes on to step 7.
    • 202: another agreement payment for this ATR is settling. The gate declines agreement-pending and signs nothing.
    • 402: the PAYMENT-REQUIRED header, padded standard base64 (RFC 4648 section 4) of an x402 document, is the agreement's payment request.
    • anything else: agreement-failed.
  3. Choose the option. The gate reads the request's options in document order and takes the first one your signer can pay with a pairing whose payment is itself a public proof, the same rule as for the payment's own options. An option is payable only if its maxTimeoutSeconds is a JSON number with an integral value from 1 to 253 − 1 (60 and 60.0 are the same value): the gate passes any other over, and declines no-payable-option when it finds no option it can pay. The request's legal context must advertise the same H: another H is hash-mismatch, and nothing is signed.

  4. Hand it over for approval. The call returns approve, the agreement payment, and signs nothing:

    MemberWhat it is
    urlThe agreement URL.
    optionThe option the gate pays: its amount (in the asset's atomic units), asset, payTo and network are the terms the agent approves.
    requiredThe agreement URL's payment request, as it served it.

    Your agent approves it as it approves any payment, or does not. To approve it, call again with approved set to approve, unchanged.

  5. Pay. With approved, the gate first reads its required whole, as step 3 reads the agreement URL's request: a request the protocol package's read refuses as a whole, for its version, its legal context or agreement URL, or an accepts that is not a list of at most 32 entries holding an option, is declined offer-unreadable with the read's code, as the unpaid request declines it, and nothing is fetched or signed. Its option must then be one of required.accepts, compared as the protocol package compares JSON values, by RFC 8785 form: member order does not count, and 60.0 is 60. Any other option is declined offer-unreadable with x402/option-not-in-document, and nothing is fetched or signed. The gate then builds exactly that payment with H and hands it to your signer (or to agreementSigner, where you gave one). It sends it in the PAYMENT-SIGNATURE header, with no second unpaid request. A signed payment nested past 64 levels, or one JSON cannot write, is declined signed-not-bound, "The agreement payment is not plain JSON.", and is not sent.

  6. Wait for the receipt. After a 202, the gate waits the Retry-After seconds (one to nine digits, surrounding spaces, tabs, CRs and LFs aside, at least 1; 2 when absent or in any other form, an HTTP-date included) and sends the same payment again. After a 5xx or a timeout it does the same after 2 seconds. Each paid request has a deadline of the option's maxTimeoutSeconds (at most 120) plus 70 seconds; the whole exchange ends at maxTimeoutSeconds plus 180 seconds, with agreement-pending. Any other answer, a redirect included, is agreement-failed. Within one call the gate never signs a second agreement payment; every call with approved signs a new one (see Once the agreement payment is sent). A 200 is the receipt:

{
  "atrHash": "0x8b1e122580ae3f6a8c3d36a24294e1260a87bde70f597279b973e39310072938",
  "agreed": true,
  "network": "eip155:84532",
  "transaction": "0xabababababababababababababababababababababababababababababababab"
}

atrHash must be this H, agreed must be true, and network and transaction must be strings. Anything else is agreement-failed, and the payment for the resource is never signed. The receipt is the agreement URL's statement: the gate checks its form and that its atrHash is H, and does not look up the transaction it names. A 200 to the first, unpaid request is taken as an agreement already recorded. The public proof is that transaction on network, which anyone can read.

  1. Pay the resource. Only then does the gate sign the payment the offer asked for, which it built, unsigned, at the comparison: a build that refuses declines before any agreement is asked. transact returns the receipt beside the payment as agreement.

Every decline after the agreement payment is sent carries it as moved: keep it, and call again without approved to learn whether it is recorded (see Once the agreement payment is sent).

Ending the exchange

transact and agree take a signal, an AbortSignal, in their options. Aborting it ends the agreement exchange at its next step: a request in flight is abandoned and a wait is cut short.

  • Before the agreement payment is sent, the call declines agreement-failed. Nothing is sent; the signer may have signed it already, and that signature is discarded.
  • After it is sent, the call declines agreement-pending, with the payment as moved.

The signal ends the agreement exchange only. Once the receipt is in hand, the payment for the resource is signed as usual.

Once the agreement payment is sent

Once the agreement payment is sent, a decline carries it as moved. A decline agreement-pending means the agreement URL has not recorded the payment in time, or your signal ended the wait, and it may settle yet. To collect the receipt, call again without approved: the gate asks the agreement URL unpaid and goes on once it answers 200 with the receipt, or declines agreement-pending again while it answers 202, signing nothing. A 402 returns the agreement payment for approval again, and whether to pay a second time is your agent's decision. Never call again with approved for the same agreement: every call with approved signs and sends a new agreement payment.

Example

The seller offers an ERC-7710 payment, whose signed form does not carry H, and names an agreement URL beside the link. The seller builds the agreement URL from a random token, as it builds the link, not from H: unpaid, the agreement URL answers with a challenge that advertises the link. A local stand-in plays the seller and the agreement URL:

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

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/${.()}`;
const  = `https://api.seller.example/agreement/${.()}`;
const  = {
  : "exact",
  : "eip155:84532",
  : "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  : "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
  : 60,
};
const  = { : { : "sha256", : , :  }, : {} };

// The resource's offer: an ERC-7710 payment, with the agreement URL beside the link.
const  = {
  : 2,
  : { : "https://api.seller.example/v1/quote" },
  : [{ ..., : "10000", : { : "USDC", : "2", : "erc7710" } }],
  : {
    : { ..., : { ...., :  } },
  },
};
// The agreement URL's own payment request: one unit, paid with an EIP-3009 authorization whose nonce is H.
const  = {
  : 2,
  : { :  },
  : [{ ..., : "1", : { : "USDC", : "2" } }],
  : {  },
};

const : string[] = [];
const :  = async (, ) => {
  if ( === ) return new ();
  if ( !== ) return new (null, { : 404 });
  if (.?.["PAYMENT-SIGNATURE"] === ) {
    .("agreement URL: 402");
    return new (null, { : 402, : { "PAYMENT-REQUIRED": (.()) } });
  }
  .("agreement URL: paid, 200");
  return .({ : , : true, : "eip155:84532", : `0x${"ab".(32)}` });
};

// The published Anvil development key. Never use it for real funds.
const  = ("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
const : Signer = {
  : `eip155:84532:${.}`,
  async (: ): <> {
    .(`signer: ${.}`);
    if (. === "eip712") {
      return .(. as <typeof .>[0]);
    }
    if (. === "erc7710") {
      // A delegation from the wallet's own ERC-7710 tooling; placeholder values for this stand-in.
      return { : `0x${"11".(20)}`, : "0x1234", : . };
    }
    throw new (`unexpected ${.}`);
  },
};

// The first call returns the agreement payment for approval, and signs nothing.
const  = await (, , , );
if ("decline" in ) throw new (`${..}: ${..}`);
if (!("approve" in )) throw new ("this pairing pays an agreement first");
const { , , ,  } = ..;
.(`approve: ${} of ${} to ${} on ${}`);

// Your agent approves it like any other payment; the second call carries it back unchanged.
const  = await (, , , , { : . });
if ("decline" in ) throw new (`${..}: ${..}`);
if ("approve" in ) throw new ("the approved agreement payment is paid in this call");
.(.("\n"));
.("receipt for H:", .?. === );
agreement URL: 402
approve: 1 of 0x036CbD53842c5426634e7929541eC2318f3dCF7e to 0x209693Bc6afc0C5328bA36FaF03C514EF312287C on eip155:84532
signer: eip712
agreement URL: paid, 200
signer: erc7710
receipt for H: true

The first call asked the agreement URL for its payment request and returned it, with no signer call. The second call signed the approved agreement payment (EIP-712, carrying H), and asked for the ERC-7710 delegation only after the receipt.

Driving the steps yourself

agree(bytes, url, signer, fetch, { approved, inputs, signal }) runs steps 2 to 6 on their own, for an agent that confirms with confirm and finishes with finish. confirm and finish neither pay nor check the agreement; only transact and agree do. When chosen.agreement is set, the pairing's payment is not itself a public proof of H: do not sign yet. Call agree(bytes, chosen.agreement, signer, fetch), have your agent approve the approve it returns, call agree again with { approved }, and sign request only with its receipt in hand:

  1. confirm returns chosen.agreement, the URL, beside the request. Do not sign the request yet.
  2. Call agree with confirmed.bytes, that URL and your signer. It returns { approve, bytes, h } and signs nothing, or { receipt } where the agreement is already recorded.
  3. Once your agent approves it, call agree again with approved: approve. It returns { receipt }.
  4. Only with the receipt in hand, sign confirmed.request and call finish. finish takes no receipt: it completes the payment whether or not the agreement is paid.

For x402/batch-settlement/cloudflare, confirm returns request: null, but its build is the payment: transact returns it as signed, and after confirm you get it with finish(bytes, chosen, null, binding), once the receipt is in hand.

The MCP tools keep the same order: atr_confirm returns no request until you pass a receipt for H, and atr_agree returns the agreement payment to approve before it signs anything. As with transact, the receipt is the agreement URL's statement: atr_confirm checks its form and that its atrHash is H, and does not look up the transaction it names.

When the offer names no agreement URL

A pairing whose payment is not a public proof, in an offer with no agreement URL, is declined with agreement-not-offered right after the comparison. Nothing is signed.

Edit on GitHub

Last updated on

On this page