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;
assemblein@integraledger/lcpwrites them as one JSON object. - ATR hash (H): SHA-256 over the ATR's exact bytes,
0xand 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
- Read. The pairing's binding reads the seller's document: H, the link, and the payment options.
- Compare. The gate fetches the link once, with your
httpx.AsyncClient, and hashes the bytes it received. No match: aDeclined, and no signer call. - Build. On a match, the gate builds the payment with the hash it computed, not the string the seller advertised.
- 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:
transactfetches the agreement URL's payment request and returnsToApprove(approve, atr_bytes, h), signing nothing.approve.optionis the option it would pay: itsamount,asset,payToandnetwork.approve.urlis the agreement URL, andapprove.requiredthe payment request as served.- Your agent approves it as it approves any payment. To pay it, call
transactagain with the same arguments andapproved=approve. - The second call signs exactly that payment, sends it, and signs the main payment only after the agreement URL
answers
200with 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
maxTimeoutSecondsis a JSON number with an integral value:60and60.0are the same value. - The agreement URL's
402must 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,5xxor timeout, within the paid option'smaxTimeoutSecondsplus 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-pendingwhen it may settle yet, andagreement-failedwhen the agreement URL answered otherwise. Either way, it has left your wallet. To collect the receipt, call again withoutapproved: the gate asks the agreement URL unpaid and goes on once it answers200with the receipt, or declinesagreement-pendingagain while it answers202, signing nothing. Never call again withapprovedfor the same agreement: every call withapprovedsigns and sends a new agreement payment. - Setting
signalends the exchange sooner: before the payment is sent,agreement-failed, and nothing is sent (a payment signed in that window is discarded); after,agreement-pendingwith the payment asmoved. Cancelling the task raisesasyncio.CancelledError, and a payment already sent is not returned: set the signal to get it back asmoved. - The receipt is the agreement URL's statement: the gate checks its form and that its
atrHashis H, and does not look up the transaction it names. A200to 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. Withconfirmandfinish, 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.
finishreturns 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,transactsigns the payment only after the agreement's receipt for that hash.confirmandfinishneither pay nor check the agreement: a caller that drives them signsrequestonly with the receiptagreereturns. - 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
GETof anhttpslink, 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 a200whoseContent-Encodingnames 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.
confirmandfinishneither pay nor check the agreement; onlytransactandagreedo. Withconfirmandfinish, whenconfirmed.chosen.agreementis set, callagreeand signconfirmed.requestonly with theAgreedreceipt 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
atrHashis H, and does not look up the transaction it names. A200to 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. httpxandcryptography, 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)
| File | Size | Uploaded | |
|---|---|---|---|
| integraledger_terms-0.3.0.tar.gz | 298.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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