Skip to content
Integra Protocol

Agentic checkouts

The ATR hash on ACP, UCP, AP2 and ACK checkouts, on card networks, and in A2A task metadata.

Not every agent pays through a 402 challenge. Many buy through a checkout: the seller opens a checkout or a payment request, the buyer's agent completes it, and a payment service provider, a card network or a credential provider moves the money. This package binds H into each of these checkouts in the field its protocol provides, and says so plainly where the protocol gives the buyer nothing to sign.

The members are the ones every pairing has. What differs is the document the seller advertises into and what the buyer presents:

ProtocolEntry pointPairingsWhere H ridesBuyer signs H
ACP@integraledger/lcp/acpacp/checkout/delegated, acp/checkout/undelegatedThe checkout session's id, and the delegate-payment allowance's checkout_session_id where the handler requires one.no
UCP@integraledger/lcp/ucpucp/checkout/ap2-mandate, ucp/checkout/unsigned, ucp/booking/ap2-mandate, ucp/booking/unsignedA legal_context entry in the checkout's links[], before the business signs the checkout.with an AP2 mandate
AP2@integraledger/lcp/ap2ap2/checkout-mandateThe legalContext member of the merchant-signed checkout_jwt, which the buyer's Checkout Mandate commits to.yes
ACK@integraledger/lcp/ackack/payment-requestThe id of the seller-signed Payment Request, as H's LCP string.no
Card networks@integraledger/lcp/cardcard/visa-tap, card/mastercard-vi/immediate, card/mastercard-vi/autonomous, card/seller-referenceA covered lcp-hash field of the agent's message signature, the checkout_jwt a Verifiable Intent mandate signs, or the seller's processor reference.Visa TAP and Verifiable Intent
A2A@integraledger/lcp/a2anoneThe Task's metadata, under the extension's URI. Delivery only.no

The "Buyer signs H" column is each pairing's pattern.buyerSigns. The pairings reference gives the sentence each pairing's record states about what a payment through it proves, including which signatures this package verifies and which it leaves to the network or the provider.

Where the buyer's approval does not sign H, the seller may advertise an agreement URL beside the link. Every checkout pairing's advertise takes it as its last argument and writes it where the protocol keeps the link, and its read returns it as agreement.

ACP

In the Agentic Commerce Protocol, the seller creates a checkout session and the buyer's agent completes it with a payment handler. This package makes H the session's id, and puts the link beside it in the session's metadata.legal_context, in snake case:

{
  "id": "0x1be27e75ea3728b55834ec54ff6709669427b11c9ba47a6d9f182b0be46513be",
  "metadata": {
    "legal_context": {
      "type": "sha256",
      "value": "0x1be27e75ea3728b55834ec54ff6709669427b11c9ba47a6d9f182b0be46513be",
      "legal_context_url": "https://atr.seller.example/0x1be27e75ea3728b55834ec54ff6709669427b11c9ba47a6d9f182b0be46513be"
    }
  }
}

The ATR's binding slot, tie(options), holds one option per kind of payment handler the session offers, each ACP's own { "requires_delegate_payment": boolean }, and nothing from the checkout. The two pairings differ in that flag:

  • acp/checkout/delegated: the handler requires delegate_payment. build returns the allowance the agent's delegate_payment request carries: ACP's six members, with reason "one_time" and checkout_session_id set to H. The agent signs its request; complete(request) returns the request only when its allowance is exactly the one built, and refuses acp/allowance-changed otherwise. bound reads checkout_session_id back. It verifies no signature.
  • acp/checkout/undelegated: nothing the buyer signs names the session. build refuses acp/nothing-to-sign and bound refuses acp/not-buyer-signed.
import { , , ,  } from "@integraledger/lcp";
import { , , type , type  } from "@integraledger/lcp/acp";

// Seller: the session offers a handler that requires delegate_payment.
const :  = { : true };
const  = new ().('{"line_items":[{"id":"li_1","quantity":1}],"total":19900}');
const  = await ((), ([]), [["order", ]]);
if ("refused" in ) throw new (.);
const  = `https://atr.seller.example/${.}`;
const :  = { : "usd", : { : "A-100" } };
const  = .(, ., , );
if ("refused" in ) throw new ((.));
.("session id is H:", . === ., .(. ?? {}));

// Buyer: read H and the link, fetch and compare, then build the allowance.
const  = .();
if ("refused" in ) throw new (.);
if (!(await (.), .)) throw new ("decline: hash-mismatch");
const  = await .(
  { , : 19900, : "usd", : "acme", : "2026-10-01T00:00:00Z" },
  .,
);
if ("refused" in ) throw new (.);
const { , ... } = .;
.("checkout_session_id is H:",  === ., );

// Buyer: the agent's own stack builds and signs its delegate_payment request around the allowance.
// complete returns the request only when the allowance in it is the one built.
const  = .({ : ., : { : "card" } });
if ("refused" in ) throw new ((.));
const  = .({ : { ...., : 99900 } });
.("refused" in  ? . : "accepted");

// Seller: H back out of what the agent signed.
const  = await .();
.("bound to H:", typeof  === "string" && (, .));
session id is H: true [ 'order_ref', 'legal_context' ]
checkout_session_id is H: true {
  reason: 'one_time',
  max_amount: 19900,
  currency: 'usd',
  merchant_id: 'acme',
  expires_at: '2026-10-01T00:00:00Z'
}
acp/allowance-changed
bound to H: true

advertise keeps every other member and metadata key, sets the session id only when it is absent or already H (acp/id-conflict otherwise), and refuses a session larger than 1 MiB as JSON. read takes H from id alone and refuses a metadata.legal_context whose value is another hash (acp/legal-context-conflict). The LCP profile acp/checkout/session-id states the rules.

UCP

In the Universal Commerce Protocol, the business returns a checkout, and, under UCP's AP2 Mandates extension, signs it (ap2.merchant_authorization). This package appends one entry to the checkout's links[] before the business signs it:

{ "type": "legal_context", "url": "https://atr.seller.example/0x…", "title": "lcp:sha256:0x…" }

and, with an agreement URL, a second entry of type legal_context_agreement. The option names the checkout or the booking by its own id ({ "checkout": "…" } or { "booking": "…" }), and tie(options) records those ids. advertise refuses a checkout the business has already signed (ucp/already-signed) and one whose id is not the option's (ucp/option-not-this-checkout).

PairingBuyer sidebound
ucp/checkout/ap2-mandate, ucp/booking/ap2-mandateread requires the business's ap2.merchant_authorization (ucp/ap2-not-active otherwise). build returns the checkout unchanged for the buyer's mandate issuer, once its link carries H, and complete(checkout_mandate) gives what the buyer presents.Checks AP2's checkout_hash against the checkout inside the buyer's mandate, then reads H from that checkout's legal_context link. It verifies no signature.
ucp/checkout/unsigned, ucp/booking/unsignedread gives H and the link. build refuses ucp/nothing-to-sign: UCP without the AP2 Mandates extension defines no buyer signature.Refuses ucp/not-buyer-signed.

legalContextLink(checkout) reads the one legal_context entry of any checkout: two such entries are ucp/legal-context-conflict. The LCP profile ucp/checkout/legal-context states the rules.

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

const  = await (new ().("the ATR's bytes"));
const  = `https://atr.seller.example/${}`;
const :  = {
  : "chk_123",
  : "ready_for_complete",
  : [{ : "terms_of_service", : "https://seller.example/tos" }],
};

const  = .(, , , { : "chk_123" });
if ("refused" in ) throw new ((.));
.(.);

const  = ();
.("refused" in  ? . : . === );
const  = .(, , , { : "chk_999" });
.("refused" in  ? . : "placed");
.("refused" in (await .({ :  }, )));
[
  { type: 'terms_of_service', url: 'https://seller.example/tos' },
  {
    type: 'legal_context',
    url: 'https://atr.seller.example/0x3283f4fc90a2f6d2782b0345c3b274acbe0218ec84ce2d8c3a1fdbe6d1cff263',
    title: 'lcp:sha256:0x3283f4fc90a2f6d2782b0345c3b274acbe0218ec84ce2d8c3a1fdbe6d1cff263'
  }
]
true
ucp/option-not-this-checkout
true

AP2

In AP2, the merchant signs a checkout_jwt and the buyer's closed Checkout Mandate commits to it through checkout_hash, the SHA-256 of that JWT. This package puts H in the JWT's payload:

  • Seller. advertise(payload, h, link, { checkout: id }) appends a legalContext member, {"type":"sha256","value":H,"legalContextUrl":link}, to the checkout payload whose id is the option's. The seller's own stack signs the result as its checkout_jwt.
  • Buyer. read(checkoutJwt) decodes the JWT's payload and returns H, the link and the checkout. build(offer, h) returns the Checkout Mandate's required claims: vct "mandate.checkout.1", the checkout_jwt, and checkout_hash, the unpadded base64url of SHA-256 over the JWT. The buyer's mandate signer issues the mandate as an SD-JWT, and complete(checkout_mandate) gives what the buyer presents: the mandate and the JWT.
  • Seller, on payment. bound(presented) finds the one closed Checkout Mandate in the SD-JWT, checks that its checkout_hash is the hash of the presented checkout_jwt (ap2/checkout-not-latest otherwise), and reads H from that JWT's payload. It verifies no signature.

jwsPayload, readMandate and checkoutBinding expose those steps one by one. The LCP profile ap2/checkout-mandate states the rules.

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

const  = await (new ().("the ATR's bytes"));
const  = { : "chk_42", : { : "USD", : "19.90" } };
const  = .(, , `https://atr.seller.example/${}`, { : "chk_42" });
if ("refused" in ) throw new ((.));
.(.());

// The seller's stack signs the payload. A placeholder signature stands in: the buyer's side verifies none.
const  = (: object) => .(.()).("base64url");
const  = .("signature").("base64url");
const  = `${({ : "ES256", : "JWT" })}.${()}.${}`;

const  = .();
if ("refused" in ) throw new (.);
const  = await .(., .);
if ("refused" in ) throw new (.);
.(.., ...);
[ 'id', 'total', 'legalContext' ]
mandate.checkout.1 43

ACK

In the payment flow of the Agent Commerce Kit (ACK), the seller answers with a 402 whose body carries a signed Payment Request token, and a receipt issued after settlement embeds that token. This package makes H the Payment Request's id, in LCP string form, and puts the link beside the request in the 402 body:

  • Seller. advertise({}, h, link, option) returns the values the seller's stack places: paymentRequestId, lcp:sha256:0x…, for the token it signs, and legalContext, {type, value, legalContextUrl}, for the body beside it. tie(options) records the Payment Request's options exactly as issued.
  • Buyer. read(body) takes H from the signed token's id, which must equal the body's legalContext (ack/legal-context-conflict otherwise), and returns the token's payment options. The body's unsigned paymentRequest copy is never read. The token's signature is not verified here.
  • After settlement. fromReceipt(credentialSubject) reads H from the Payment Request token a receipt embeds. It verifies nothing: call it on a receipt you have verified.

ACK defines no payer signature, so there is no place for H in what the buyer signs. build and bound refuse ack/no-signed-place. The LCP profile ack/payment-request states the rules.

Card networks

The card entry point covers three ways to pay a card checkout. The option is the seller's own { scheme, checkout }, where scheme is "visa-tap", "mastercard-vi" or "seller-reference" and checkout is the seller's checkout id; pairingsOf(option) names the pairings that pay it. advertise gives the same values for every card pairing: the legalContext the seller shows before payment, and reference, H's LCP string, for the seller to place in its processor reference.

  • Visa Trusted Agent Protocol. The agent sends H in an lcp-hash field and lists that field among the covered components of its agent-payer-auth message signature (RFC 9421). build returns the field for the agent's signer to add: { field: "lcp-hash", value: H, component: "lcp-hash" }. bound reads the one lcp-hash line when an agent-payer-auth signature lists "lcp-hash" without parameters. It verifies no signature, key, window or nonce. The LCP profile card/visa-tap states the rules.
  • Mastercard Verifiable Intent. H rides in the merchant's checkout_jwt as its legalContext, and a mandate signs that JWT's SHA-256 as checkout_hash. build returns the checkout mandate to sign and the payment mandate's transactionId, both that digest. card/mastercard-vi/immediate reads H from the checkout mandate the user's L2 mandate references, and checks the checkout_hash and any disclosed transaction_id; it verifies no signature. card/mastercard-vi/autonomous reads H from the agent's L3b checkout mandate, and verifies the ES256 signatures of L2 and L3b and their sd_hash links, but not the L1 issuer's signature. The LCP profile card/mastercard-vi states the rules.
  • card/seller-reference. A plain card checkout: nothing the buyer signs carries H. The seller shows H and the link before payment and places reference in its processor reference. build and bound refuse card/no-signed-place.
import {  } from "@integraledger/lcp";
import { , , type CardOption } from "@integraledger/lcp/card";

const  = await (new ().("the ATR's bytes"));
const : CardOption = { : "visa-tap", : "order-7731" };
.((), ({ : "mastercard-vi", : "order-7731" }));

const  = .({}, , `https://atr.seller.example/${}`, );
if ("refused" in ) throw new (.);
.(. === `lcp:sha256:${}`);

const  = await .(, );
if ("refused" in ) throw new (.);
.(., ., . === );
[ 'card/visa-tap' ] [ 'card/mastercard-vi/immediate', 'card/mastercard-vi/autonomous' ]
true
lcp-hash lcp-hash true

A2A

The Agent2Agent protocol signs nothing per transaction, so it has no place for H in a payment. What it can do is deliver H and the link with the task. This package defines an A2A extension for that, and exports no pairing: binding is the refusal a2a/no-signed-place, and the payment made for the task uses its own pairing, whose record states what it proves.

ExportWhat it does
A2A_EXTENSION_URISThe extension's URI: https://integraledger.com/lcp/a2a/legal-context/v1.
agentExtension({ required? })The Agent Card capabilities.extensions[] entry that declares it.
requested(header)Whether a request's A2A-Extensions header value names the extension.
place(task, h, link, header?)A copy of the Task with {type, value, legalContextUrl} in its metadata, under the extension's URI. Other metadata is kept.
read(task)H and the link from the Task's metadata.
delivery.provesWhat the delivery shows: nothing about the payment.
import {  } from "@integraledger/lcp";
import { , , , , type A2aTask } from "@integraledger/lcp/a2a";

.(()[0]?.);

const  = await (new ().("the ATR's bytes"));
const  = "https://integraledger.com/lcp/a2a/legal-context/v1";
.(());

const : A2aTask = { : "task-1", : "ctx-1", : { : "input-required" }, : "task" };
const  = (, , `https://atr.seller.example/${}`, );
if ("refused" in ) throw new ((.));
const  = ();
.("refused" in  ? . : . ===  && ..());
.(());
https://integraledger.com/lcp/a2a/legal-context/v1
true
true
{ refused: true, code: 'a2a/no-legal-context' }

The LCP profile a2a-legal-context-v1 states the extension's rules.

Next

  • Buyer: the gate every checkout on this page runs before the buyer approves.
  • Pairings reference: each checkout pairing's pattern and what its record proves.
Edit on GitHub

Last updated on

On this page