Skip to main content

integraledger-terms

The buyer gate for agents that pay, in Python: before your wallet signs anything, it confirms that the record a seller serves hashes to the value the payment will carry, and builds the payment with that hash.

pip install integraledger-terms

What it is

A seller that follows the Legal Context Protocol publishes the agreement's record, an Agentic Transaction Record (ATR), at an https link, and advertises its hash beside the payment request. Where the payment your agent approves carries that hash, that approval is the buyer's acceptance of that exact record; where it does not, the pairing's statement of what it proves says so.

That only holds if the hash in the payment is the hash of the record you can read. This package is the check that makes sure it is. It fetches the record, computes its SHA-256, compares it with what the seller advertised, and only on a match hands your signer a request built with that hash. After your signer answers, the pairing reads the hash back from the completed payment, and the payment is returned only if it is the hash of the bytes you received, or, where the proof is the agreement payment, without that reading.

It is the Python implementation of the same gate as the TypeScript package @integraledger/terms. Both run the same rules and pass the same shared vectors, pairing for pairing. It holds no keys and never reads the record's content: what the record says is for you and your principal to judge. The one payment it sends is the agreement payment your agent approved, to the agreement URL.

Key concepts

  • Agentic Transaction Record (ATR): the agreement's record: the exact bytes the seller serves; assemble in @integraledger/lcp writes them as one JSON object.
  • ATR hash (H): SHA-256 over the ATR's exact bytes, 0x and 64 hex digits. It is computed over the bytes as received, never over a parsed and re-serialised copy.
  • Legal Context Protocol (LCP): the pattern this package implements: where the payment the buyer approves carries H, that approval is the buyer's acceptance of that exact record; where it does not, the pairing's statement of what it proves says so.
  • Pairing: a payment protocol, scheme and rail combination, such as x402/exact/eip155/eip3009.
  • Binding: how H rides in a pairing's payment: the field of that payment, as the protocol's own specification defines the payment, that carries H. This package exports one binding per pairing, named after its id: X402_EXACT_EIP155_EIP3009.
  • Buyer gate: this package: the buyer-side check that compares the served bytes with H before anything is signed.
  • Seller: the party serving the resource.
  • Facilitator: the x402 role that verifies and settles. The gate never talks to one. It returns the payment to you, except the approved agreement payment, which it sends to the agreement URL.
  • Vectors: the shared test cases that fix the rules byte for byte across languages.

Install

pip install integraledger-terms

Python >=3.11. The package depends on httpx and cryptography, and is fully typed (py.typed).

The examples below sign with eth-account (pip install eth-account). The gate itself never signs; any wallet that can sign the request works.

Quickstart

The seller answered with an x402 402 Payment Required whose PAYMENT-REQUIRED document advertises H and the link. This example uses the ATR, the document and the payer key of the shared vectors, and an httpx.MockTransport as a local stand-in for the network, so it runs offline and prints the same values every time.

import asyncio
import uuid
from collections.abc import Mapping
from typing import Any

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. The gate hashes these bytes and never parses them.
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()}"

# The seller's x402 payment request, advertising H and the link.
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: Mapping[str, Any]) -> str:
        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   138

The nonce your wallet signed is the hash of the 138 bytes the link served. Keep result.atr_bytes with the payment: they are your copy of the record. To send the payment over x402's HTTP transport, put base64 of its JSON in the PAYMENT-SIGNATURE header.

If the link had served one byte more, less or different, transact would have returned Declined(code="hash-mismatch", …) and never called the signer.

How it works

  1. Read. The pairing's binding reads the seller's document: H, the link, and the payment options.
  2. Compare. The gate fetches the link once, with your httpx.AsyncClient, and hashes the bytes it received. No match: a Declined, and no signer call.
  3. Build. On a match, the gate builds the payment with the hash it computed, not the string the seller advertised.
  4. Finish. The gate joins your signer's answer and returns the payment only when the pairing reads H back from the completed payment as the hash of the compared bytes, or, where the proof is the agreement payment, without that reading.

Every function returns a value and never raises for a protocol outcome: either its result, or a Declined.

Guides

Pay with transact

await transact(doc, binding, signer, fetch, *, inputs=None, agreement_signer=None, approved=None, signal=None) runs the whole order: compare, pay the agreement your agent approved where the pairing needs one, sign, finish. It returns Transacted:

Field What it is
signed The payment to send, in the protocol's own form. None when the pairing gives the buyer nothing to sign, except for x402/batch-settlement/cloudflare, whose build is itself the payment.
atr_bytes The ATR's exact bytes: your copy of the record.
h Their SHA-256.
agreement The agreement's AgreementReceipt, where the pairing needs an agreement payment.
landed A landed receipt to keep beside the payment, for the few pairings that return one.

Where the pairing needs an agreement payment that is not yet recorded, it returns ToApprove(approve, atr_bytes, h) instead, and nothing has been signed: see Pay the agreement first.

inputs are the buyer's own values a build needs, such as a recent blockhash; agreement_signer pays the agreement where a different wallet should (without it, signer pays both); approved is the agreement payment your agent approved, exactly as a previous call returned it in ToApprove.approve; setting signal, an asyncio.Event, ends the agreement exchange. See the rails guide for each pairing's inputs.

Sign elsewhere: confirm, then finish

When the key lives in a hardware wallet or a signing service, split the order. Confirmed.chosen is a Chosen dataclass whose fields are plain JSON. Send dataclasses.asdict(confirmed.chosen) with the bytes, and rebuild it with Chosen(**data) before finish. finish declines anything that is not a Chosen (offer-unreadable).

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, Finished, confirm, finish

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": {}}},
}
KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"


def seller(request: httpx.Request) -> httpx.Response:
    return httpx.Response(200, content=ATR) if str(request.url) == LINK else httpx.Response(404)


async def main() -> None:
    # 1. Compare, and build the request with H.
    async with httpx.AsyncClient(transport=httpx.MockTransport(seller)) as client:
        confirmed = await confirm(
            OFFER, X402_EXACT_EIP155_EIP3009, "eip155:84532:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", client
        )
    if isinstance(confirmed, Declined) or confirmed.request is None:
        raise SystemExit(f"not confirmed: {confirmed}")

    # 2. Sign exactly the request, elsewhere.
    typed = encode_typed_data(full_message=dict(confirmed.request["typedData"]))
    signature = "0x" + bytes(Account.sign_message(typed, KEY).signature).hex()

    # 3. Finish from the bytes and chosen. finish recomputes H from the bytes; it trusts nothing it did not rebuild.
    done = finish(confirmed.atr_bytes, confirmed.chosen, signature, X402_EXACT_EIP155_EIP3009)
    if not isinstance(done, Finished):
        raise SystemExit(f"not finished: {done}")
    print("nonce is H:", done.signed["payload"]["authorization"]["nonce"] == H)


asyncio.run(main())
nonce is H: True

A pairing signed in two steps (a channel funding, then its first voucher) makes finish return Next: sign next.next and call finish again with every answer so far, in order, as a list. transact does this for you.

confirm and finish neither pay nor check the agreement; only transact and agree do. When confirmed.chosen.agreement is set, the pairing's payment is not itself a public proof of H: do not sign yet. Call to_approve = await agree(confirmed.atr_bytes, confirmed.chosen.agreement, signer, fetch), have your agent approve to_approve.approve, call agree again with approved=to_approve.approve, and sign confirmed.request only with the Agreed receipt in hand. See Pay the agreement first.

Where request is None, there is nothing to sign. For x402/batch-settlement/cloudflare the build is itself the payment: with the agreement's receipt in hand, finish(confirmed.atr_bytes, confirmed.chosen, None, binding) returns it. For every other such pairing, the comparison and, where the pairing needs one, the agreement payment are the whole of the buyer's step.

Pay the agreement first

Some payments are not a public proof of H: a card charge, a Stripe payment, a Lightning invoice, a checkout in which the buyer signs nothing. For those pairings the seller's offer also names an agreement URL, an x402 resource for the same H whose payment is a public proof. The agreement payment is a payment like any other, so your agent approves it before anything is signed:

  1. transact fetches the agreement URL's payment request and returns ToApprove(approve, atr_bytes, h), signing nothing. approve.option is the option it would pay: its amount, asset, payTo and network. approve.url is the agreement URL, and approve.required the payment request as served.
  2. Your agent approves it as it approves any payment. To pay it, call transact again with the same arguments and approved=approve.
  3. The second call signs exactly that payment, sends it, and signs the main payment only after the agreement URL answers 200 with a receipt {"atrHash": H, "agreed": true, "network": …, "transaction": …}.
  • The option paid is the first, in document order, that your signer can pay with a pairing whose payment is itself a public proof: the same rule as for the main payment's options. Its maxTimeoutSeconds is a JSON number with an integral value: 60 and 60.0 are the same value.
  • The agreement URL's 402 must advertise the same H, or nothing is signed (hash-mismatch).
  • Once the agreement payment is sent, the gate sends the same payment again after each 202, 5xx or timeout, within the paid option's maxTimeoutSeconds plus 180 seconds. Within one call it never signs a second agreement payment.
  • Once the agreement payment is sent, every decline carries it as moved: agreement-pending when it may settle yet, and agreement-failed when the agreement URL answered otherwise. Either way, it has left your wallet. 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. Never call again with approved for the same agreement: every call with approved signs and sends a new agreement payment.
  • Setting signal ends the exchange sooner: before the payment is sent, agreement-failed, and nothing is sent (a payment signed in that window is discarded); after, agreement-pending with the payment as moved. Cancelling the task raises asyncio.CancelledError, and a payment already sent is not returned: set the signal to get it back as moved.
  • 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, and the first call goes straight on to the main payment.
  • A pairing that needs an agreement and whose offer names none is declined with agreement-not-offered.
  • await agree(atr_bytes, url, signer, fetch, approved=…, inputs=…, signal=…) runs the exchange on its own, in the same two calls. With confirm and finish, call it before you sign: see Sign elsewhere.

Channels and sessions

In an x402 batch-settlement channel or an MPP session, one ATR covers the whole channel. The gate compares it once, at the opening; you keep a hold (a JSON-ready dict) and sign every later voucher from it. This example opens a channel with the vectors' two development keys and pays two more requests in it:

import asyncio
import uuid
from collections.abc import Mapping
from typing import Any

import httpx
from eth_account import Account
from eth_account.messages import encode_typed_data

from integraledger_terms import X402_BATCH_SETTLEMENT_EIP155, Declined, open_channel, record_charge, transact, within

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()}"

# The seller's batch-settlement offer: 1000 units per request, into a channel.
OFFER = {
    "x402Version": 2,
    "resource": {"url": "https://api.seller.example/v1/quote"},
    "accepts": [
        {
            "scheme": "batch-settlement",
            "network": "eip155:84532",
            "amount": "1000",
            "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
            "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
            "maxTimeoutSeconds": 3600,
            "extra": {
                "receiverAuthorizer": "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
                "withdrawDelay": 900,
                "name": "USDC",
                "version": "2",
            },
        }
    ],
    "extensions": {"legalContext": {"info": {"type": "sha256", "value": H, "legalContextUrl": LINK}, "schema": {}}},
}

# Anvil development keys: the payer funds the channel, the payer authorizer signs vouchers. Never use them for real funds.
PAYER_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
AUTHORIZER_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
AUTHORIZER = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"


def sign_typed(typed_data: Mapping[str, Any], key: str) -> str:
    signed = Account.sign_message(encode_typed_data(full_message=dict(typed_data)), key)
    return "0x" + bytes(signed.signature).hex()


class Wallet:
    account = "eip155:84532:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"

    async def sign(self, request: Mapping[str, Any]) -> list[str]:
        if request["kind"] != "batch":
            raise ValueError(f"unexpected {request['kind']}")
        answers = []
        for each in request["requests"]:
            typed = each["typedData"]
            answers.append(sign_typed(typed, AUTHORIZER_KEY if typed["primaryType"] == "Voucher" else PAYER_KEY))
        return answers


def seller(request: httpx.Request) -> httpx.Response:
    return httpx.Response(200, content=ATR) if str(request.url) == LINK else httpx.Response(404)


async def main() -> None:
    wallet = Wallet()
    # Open: compare the ATR once, sign the deposit and the first voucher.
    async with httpx.AsyncClient(transport=httpx.MockTransport(seller)) as client:
        opened = await transact(
            OFFER, X402_BATCH_SETTLEMENT_EIP155, wallet, client, inputs={"payerAuthorizer": AUTHORIZER, "deposit": "100000"}
        )
    if isinstance(opened, Declined) or opened.signed is None:
        raise SystemExit(f"the channel did not open: {opened}")
    hold = open_channel(opened.atr_bytes, opened.signed, X402_BATCH_SETTLEMENT_EIP155)
    if isinstance(hold, Declined):
        raise SystemExit(hold.code)
    print("channel ", hold["channel"])

    # Each later request: the seller's new offer must advertise the held H.
    for charged in ("1000", "2000"):
        paid = await within(OFFER, hold, X402_BATCH_SETTLEMENT_EIP155, wallet)
        if isinstance(paid, Declined):
            raise SystemExit(paid.code)
        print("voucher up to", paid.hold["signedMax"])
        # The seller reports its cumulative charge; it may not exceed what was signed.
        recorded = record_charge(paid.hold, charged)
        if isinstance(recorded, Declined):
            raise SystemExit(recorded.code)
        hold = recorded


asyncio.run(main())
channel  0x9c2d7031768ecb3fe688ae307c45f2e3a3d98b0d9ce1a6afcd27131fabb67e56
voucher up to 1000
voucher up to 2000

The channel id is the one the TypeScript gate computes for the same inputs: both languages build the same payment.

The hold is a plain dict: store it, and pass the latest one to each call. within takes the bytes, H and the channel from the hold, and the amount from the new challenge. On x402's batch channels it signs only when that challenge advertises the held H and the payment it builds names the held channel. On an MPP session it declines a challenge that names another channel or another H, and signs one with no legal context under the held H.

Confirm a payment later: check

check(atr_bytes, presented, binding) returns Checked(h) when the ATR hash the pairing reads from a payment you hold is the SHA-256 of the bytes you kept. A payment from which the pairing reads no ATR hash is declined signed-not-bound. Where finish or transact returned landed, put it back into the payment under "landed" first.

Declines and moved

A decline is Declined(code, detail, moved). code is one of twelve values; detail is a sentence or the protocol's refusal code. Where your signer moved the payment itself before the gate declined (a Lightning node paid the invoice, a signer broadcast a transaction), moved is Moved(signed, atr_bytes, h). A decline that follows a sent agreement payment carries that payment as moved too. Keep it: present a payment your signer moved again rather than signing a new one, and for an agreement payment, call again without approved to collect its receipt, never with approved.

Code Meaning
pairing-not-supported The binding names no pairing the gate serves; chosen or a hold belongs to another pairing; or the pairing has no channel.
offer-unreadable The seller's document, the chosen option or a build could not be read, or a recorded charge is out of range. detail carries the reason.
no-payable-option No option is payable by this account (for the agreement, with a public-proof pairing), an input the build needs is missing or malformed, the arguments of transact or agree are malformed, or a later channel challenge asks for a payment in a channel the hold did not open.
link-not-https The ATR link or the agreement URL is not an https URL. Nothing was fetched.
atr-unfetchable The link did not answer 200 with the bytes within 10 seconds (a redirect counts as a failure), or answered 200 with a Content-Encoding other than identity.
atr-too-large The ATR is larger than 1 MiB (1,048,576 bytes).
hash-mismatch The served bytes do not hash to the advertised H; the agreement URL or a later channel challenge advertises another H; or a hold's bytes do not hash to its H. Nothing was signed.
signer-failed Your signer raised.
signed-not-bound The pairing does not read the hash of the compared bytes from the payment. The payment is not returned.
agreement-not-offered The pairing's payment is not a public proof, and the offer names no agreement URL.
agreement-pending The agreement payment is sent and not yet recorded, or the agreement URL answered 202 to the unpaid request: another is already settling.
agreement-failed The unpaid request: the agreement URL could not be reached, or answered a status other than 200, 202 or 402, or a 402 without a readable PAYMENT-REQUIRED. The paid request: any answer but 200, 202 or 5xx, a 402 included. Either request: a 200 or 402 with a Content-Encoding other than identity, or a 200 that is not a receipt for this H (a receipt over 64 KiB included). Also: the approved payment names another agreement URL, or signal ended the exchange before the payment is sent.

API reference

Everything below is exported from integraledger_terms.

Functions

Export Signature What it does
transact async (doc, binding, signer, fetch, *, inputs=None, agreement_signer=None, approved=None, signal=None) -> Transacted | ToApprove | Declined Compare, pay the approved agreement first where needed, sign, finish.
confirm async (doc, binding, account, fetch, inputs=None) -> Confirmed | Declined Read, fetch, compare, and build the signing request with H. request is None when there is nothing to sign; chosen.agreement is the agreement URL, where the pairing needs one.
finish (atr_bytes, chosen, signature, binding) -> Finished | Next | Declined Rebuild from chosen and the bytes, join the signature, and return the payment only when the pairing reads H from the completed payment, or, where the proof is the agreement payment, without that reading.
check (atr_bytes, presented, binding) -> Checked | Declined Confirm a held payment carries the hash of the kept bytes.
agree async (atr_bytes, url, signer, fetch, *, approved=None, inputs=None, signal=None) -> ToApprove | Agreed | Declined Without approved, return the agreement payment to approve, signing nothing, or the receipt where the agreement is already recorded; with it, pay that payment and return the receipt.
open_channel (atr_bytes, opened, binding, landed=None) -> ChannelHold | Declined Hold a channel opened for the compared ATR. Pass landed where transact returned one.
within async (doc, hold, binding, signer, refund=None, inputs=None) -> Within | Declined Sign a later voucher, or a refund with refund, in a held channel.
record_charge (hold, charged_cumulative_amount) -> ChannelHold | Declined Record the seller's cumulative charge, between the last recorded and the most signed.
atr_hash (data: bytes) -> AtrHash SHA-256 of the bytes, 0x and 64 lower-case hex digits.
hash_equals (a: str, b: str) -> bool Compares two hashes as 32 decoded bytes, in either case; False when either is malformed.

fetch is an httpx.AsyncClient. The gate asks it for one GET of the link, with Accept-Encoding: identity and no redirect, and reads the body as sent, stopping at the size bound. A 200 whose Content-Encoding names any coding is declined with its body unread, so nothing is decompressed and the bytes hashed are the bytes sent. The agreement URL is asked the same way, and its receipt read as sent, at most 64 KiB.

httpx refuses some hosts the gate accepts in an https link: an xn-- label that is not valid Punycode, such as xn--zz, and a label IDNA 2008 does not allow, such as xn--a or xn--ls8h. An ATR link on such a host is declined atr-unfetchable, and an agreement URL on one is declined agreement-failed; nothing is fetched. The TypeScript gate passes such a link to its fetch.

Results and types

Export What it is
Transacted signed, atr_bytes, h, agreement, landed.
Confirmed chosen, request (a dict, or None), atr_bytes, h.
Finished signed, h, landed.
Next next (the next request) and h, for a payment signed in steps.
Checked h.
ToApprove approve (an AgreementPayment), atr_bytes, h: the agreement payment to approve.
AgreementPayment url, option (the x402 option paid: amount, asset, payTo, network), required (the agreement URL's payment request as served).
Agreed receipt.
AgreementReceipt atr_hash, agreed, network, transaction.
Within signed and the updated hold.
ChannelHold A dict: pairing, network, channel, h, atr (base64), opening, charged, signedMax.
Declined code, detail, moved.
Moved signed, atr_bytes, h: the payment the signer moved before a decline, to keep and present again.
DeclineCode The twelve codes in the table above.
Chosen A dataclass: pairing, choice, ref, agreement, whose values are plain JSON; the agreement URL where the pairing needs one.
Signer A protocol: an account string (CAIP-10) and async def sign(self, request) -> Signature.
Binding A protocol: id, public_proof, read, build, bound.
Advertised, Step, Refusal, Unsigned, BatchUnsigned, ChannelRef The values a binding and the gate exchange, for code that implements a binding.
AtrHash, Json, Inputs, Signature Aliases: str, Mapping[str, Any], Mapping[str, Any], Any.
MAX_ATR_BYTES 1048576.

A signer receives each request with every byte string as 0x and lower-case hex, and answers with byte strings in the form the signers guide names for its kind: 0x hex, except where the guide names base58, base64, or hex digits without 0x. The guide lists every request kind and the answer it takes.

Supported pairings

Each pairing is a module-level binding constant. The list is generated from the TypeScript registry and checked against this package's exports, so both languages serve the same pairings. "Agreement payment first" marks the pairings whose payment is not itself a public proof of H.

Rail Pairing Python binding Buyer signs H Agreement payment first
EVM mpp/charge/evm/authorization MPP_CHARGE_EVM_AUTHORIZATION yes no
EVM mpp/charge/evm/hash MPP_CHARGE_EVM_HASH no yes
EVM mpp/charge/evm/permit2 MPP_CHARGE_EVM_PERMIT2 yes no
EVM mpp/charge/evm/transaction MPP_CHARGE_EVM_TRANSACTION no yes
EVM mpp/charge/usdc/evm MPP_CHARGE_USDC_EVM yes no
EVM mpp/charge/usdc/gateway MPP_CHARGE_USDC_GATEWAY yes no
EVM mpp/session/evm MPP_SESSION_EVM yes no
EVM x402/auth-capture/eip155/eip3009 X402_AUTH_CAPTURE_EIP155_EIP3009 yes no
EVM x402/auth-capture/eip155/permit2 X402_AUTH_CAPTURE_EIP155_PERMIT2 yes no
EVM x402/batch-settlement/eip155 X402_BATCH_SETTLEMENT_EIP155 yes no
EVM x402/exact/eip155/eip3009 X402_EXACT_EIP155_EIP3009 yes no
EVM x402/exact/eip155/erc7710 X402_EXACT_EIP155_ERC7710 no yes
EVM x402/exact/eip155/erc7710-salt X402_EXACT_EIP155_ERC7710_SALT yes no
EVM x402/exact/eip155/permit2 X402_EXACT_EIP155_PERMIT2 yes no
EVM x402/upto/eip155/permit2 X402_UPTO_EIP155_PERMIT2 yes no
Tempo mpp/charge/tempo/memo MPP_CHARGE_TEMPO_MEMO no no
Tempo mpp/charge/tempo/push MPP_CHARGE_TEMPO_PUSH no no
Tempo mpp/session/tempo MPP_SESSION_TEMPO yes no
Tempo mpp/subscription/tempo MPP_SUBSCRIPTION_TEMPO yes no
Solana mpp/charge/solana MPP_CHARGE_SOLANA yes no
Solana mpp/charge/usdc/solana MPP_CHARGE_USDC_SOLANA yes no
Solana mpp/session/solana MPP_SESSION_SOLANA no no
Solana x402/batch-settlement/solana X402_BATCH_SETTLEMENT_SOLANA yes no
Solana x402/exact/solana X402_EXACT_SOLANA yes no
Solana x402/upto/solana X402_UPTO_SOLANA yes no
Stellar mpp/charge/stellar MPP_CHARGE_STELLAR no no
Stellar x402/exact/stellar X402_EXACT_STELLAR no no
XRP Ledger mpp/charge/xrpl MPP_CHARGE_XRPL yes no
XRP Ledger mpp/session/xrpl MPP_SESSION_XRPL yes no
XRP Ledger x402/exact/xrpl X402_EXACT_XRPL yes no
Hedera mpp/charge/hedera MPP_CHARGE_HEDERA no no
Hedera mpp/session/hedera MPP_SESSION_HEDERA yes no
Hedera x402/exact/hedera X402_EXACT_HEDERA yes no
Hedera x402/exact/hedera/transfer-executor X402_EXACT_HEDERA_TRANSFER_EXECUTOR no yes
Algorand x402/exact/algorand X402_EXACT_ALGORAND yes no
Aptos x402/exact/aptos X402_EXACT_APTOS no yes
Sui x402/exact/sui X402_EXACT_SUI yes no
NEAR mpp/charge/nearintents MPP_CHARGE_NEARINTENTS no yes
NEAR x402/exact/near X402_EXACT_NEAR yes no
Starknet x402/exact/starknet X402_EXACT_STARKNET yes no
Polkadot x402/exact/polkadot/lcp-assets-remark X402_EXACT_POLKADOT_LCP_ASSETS_REMARK yes no
TRON x402/exact/tron/lcp-trc20-memo X402_EXACT_TRON_LCP_TRC20_MEMO yes no
TON x402/exact/tvm X402_EXACT_TVM yes no
Cardano x402/exact/cardano X402_EXACT_CARDANO yes no
Casper x402/exact/casper X402_EXACT_CASPER yes no
Concordium x402/exact/ccd X402_EXACT_CCD yes no
Stacks mpp/charge/usdc/stacks MPP_CHARGE_USDC_STACKS yes no
Lightning mpp/charge/lightning MPP_CHARGE_LIGHTNING no yes
Lightning mpp/session/lightning MPP_SESSION_LIGHTNING no yes
Lightning x402/exact/lnbtc X402_EXACT_LNBTC no yes
Lightning x402/exact/lnbtc/invoice-named X402_EXACT_LNBTC_INVOICE_NAMED no yes
Cloudflare x402/batch-settlement/cloudflare X402_BATCH_SETTLEMENT_CLOUDFLARE yes yes
Card card/mastercard-vi/autonomous CARD_MASTERCARD_VI_AUTONOMOUS yes yes
Card card/mastercard-vi/immediate CARD_MASTERCARD_VI_IMMEDIATE yes yes
Card card/seller-reference CARD_SELLER_REFERENCE no yes
Card card/visa-tap CARD_VISA_TAP yes yes
Card mpp/charge/card MPP_CHARGE_CARD no yes
Stripe mpp/charge/stripe MPP_CHARGE_STRIPE no yes
Stripe mpp/subscription/stripe MPP_SUBSCRIPTION_STRIPE no yes
Mandate ap2/checkout-mandate AP2_CHECKOUT_MANDATE yes yes
Mandate ucp/booking/ap2-mandate UCP_BOOKING_AP2_MANDATE yes yes
Mandate ucp/checkout/ap2-mandate UCP_CHECKOUT_AP2_MANDATE yes yes
Checkout ack/payment-request ACK_PAYMENT_REQUEST no yes
Checkout acp/checkout/delegated ACP_CHECKOUT_DELEGATED no yes
Checkout acp/checkout/undelegated ACP_CHECKOUT_UNDELEGATED no yes
Checkout ucp/booking/unsigned UCP_BOOKING_UNSIGNED no yes
Checkout ucp/checkout/unsigned UCP_CHECKOUT_UNSIGNED no yes

Security model

What the gate guarantees, in the code:

  • Nothing is signed before the comparison. The signer is called only after SHA-256 of the served bytes equals the advertised H, compared as 32 decoded bytes.
  • The payment carries the computed hash. The build takes the hash the gate computed, not the advertised string.
  • The hash is read back from the payment. finish returns a payment only when the binding reads, from the completed payment, the hash of the compared bytes, or, where the proof is the agreement payment, without that reading. For a pairing whose payment is not a public proof, transact signs the payment only after the agreement's receipt for that hash. confirm and finish neither pay nor check the agreement: a caller that drives them signs request only with the receipt agree returns.
  • Every payment is shown before it is signed. The agreement payment is returned for your agent's approval, with its amount, asset, payee and network, and is signed only when a second call carries it back.
  • One bounded fetch. One GET of an https link, no redirect, one 10-second deadline over headers and body, at most 1 MiB of decoded body.
  • Exact bytes. The ATR is hashed as received and returned as received. It is never parsed. Every request asks for Accept-Encoding: identity, and a 200 whose Content-Encoding names any coding is declined unread, so nothing is decompressed: the bytes hashed are the bytes sent.
  • No keys. The gate hands requests to your signer and holds nothing that could sign.

What it does not do:

  • It does not read or judge the ATR's content.
  • It does not check amount, payee, asset, timing or payer against the ATR, and applies no spending limit. A discrepancy is between the parties; what to approve is your agent's decision.
  • confirm and finish neither pay nor check the agreement; only transact and agree do. With confirm and finish, when confirmed.chosen.agreement is set, call agree and sign confirmed.request only with the Agreed receipt in hand.
  • It does not authenticate the agreement's receipt. 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 the transaction the receipt names, on its network.
  • It does not bound the amount a channel voucher adds: that is the amount the seller's new challenge asks.
  • It carries no business or legal logic.
  • It does not store the ATR, talk to a facilitator, or settle a payment.

Test vectors and conformance

The rules are fixed by the shared vectors that the TypeScript package @integraledger/lcp ships in its vectors/ directory: the buyer rows of buyer.json (the hash, a record changed by one byte, the fetch bounds, the agreement exchange) and each pairing's file (the request it builds and the payment it completes). This package's test suite runs every one on Python 3.11 and 3.14, and the TypeScript gate runs the same files, so both compute the same hashes, build the same requests and give the same declines. The tests read the vectors from @integraledger/lcp as pnpm install places it in a checkout, so they run from a checkout after pnpm install, not from the published package.

Requirements

  • Python >=3.11.
  • httpx and cryptography, installed with the package.
  • A signer for the pairings you pay.

Contributing

See the repository README and CONTRIBUTING.md.

License

Apache-2.0. See LICENSE.

Metadata

Release files for integraledger-terms 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for integraledger-terms 0.3.0
File Size Uploaded
integraledger_terms-0.3.0.tar.gz 298.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for integraledger-terms 0.3.0
File Interpreter ABI Platform
integraledger_terms-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 692.5 kB

Release files / integraledger_terms-0.3.0.tar.gz

Download URL integraledger_terms-0.3.0.tar.gz
Size 298.3 kB
Tags Source
SHA-256 checksum
How to use checksums
0df8358476759bb19cff924fbe82338890d81665fc2d5bb6388ba520179a6546
BLAKE2b-256 checksum
How to use checksums
d014a57010a46f1b2f22867ddb59c0199e304d641af90340905dfd32e751d2bf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / integraledger_terms-0.3.0-py3-none-any.whl

Download URL integraledger_terms-0.3.0-py3-none-any.whl
Size 394.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1ffed08e02a4fce804d99ecb8e582372219cc2df92f75e5312821ecb353157d6
BLAKE2b-256 checksum
How to use checksums
707320c0f574f56c94b9078510d2612a4ad453b6008ad0e03bde17a07d144386
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.1

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page