Python
The Python buyer gate, integraledger-terms — the same rules and the same vectors as the TypeScript gate, with Python names and dataclass results.
integraledger-terms is the buyer gate in Python. It is a second implementation of the same rules, not a wrapper: it
carries its own bindings, one module-level constant per pairing, and runs the same shared vectors as the TypeScript
gate, so for the same inputs the two build the same payments.
pip install integraledger-termsPython >=3.11. It depends on httpx and cryptography, and is fully typed.
Differences from the TypeScript gate
| TypeScript | Python | |
|---|---|---|
| Binding | an export of @integraledger/lcp, such as exactEip3009 | a constant of integraledger_terms, such as X402_EXACT_EIP155_EIP3009 |
| HTTP client | fetch, WHATWG's call shape | an httpx.AsyncClient |
| Result | an object, or { decline: { code, detail }, moved? } | a dataclass (Transacted, Confirmed, Finished, …), or Declined(code, detail, moved) |
| ATR bytes | bytes: Uint8Array | atr_bytes: bytes |
transact's options | { inputs, agreementSigner, approved, signal } | keyword arguments inputs=, agreement_signer=, approved=, signal= |
| Ending the agreement exchange | signal, an AbortSignal | signal=, an asyncio.Event that you set |
| The agreement payment to approve | { approve, bytes, h } | ToApprove(approve, atr_bytes, h) |
| Byte strings in a signing request | Uint8Array, as built | 0x and lower-case hex |
| Channel functions | openChannel, within, recordCharge | open_channel, within, record_charge |
The HTTP clients also differ on some hosts that both gates accept in an https link. httpx refuses 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. In
the Python gate, 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.
Everything else is the same: the order of the steps, the bounds of the fetch, the identity-only content coding, the twelve decline codes, the agreement exchange and its timings, the channel hold's members.
A signer
Any object with an account string (CAIP-10) and an async def sign(self, request) method is a signer. It receives
the request as a dict, with every byte string as 0x and lower-case hex, and returns its answer as JSON-ready
values:
from collections.abc import Mapping
from typing import Any
from eth_account import Account
from eth_account.messages import encode_typed_data
class Wallet:
"""Signs EIP-712 requests with one key, and refuses every other kind."""
def __init__(self, key: str, account: str) -> None:
self._key = key
self.account = account
async def sign(self, request: Mapping[str, Any]) -> str:
if request["kind"] != "eip712":
raise ValueError(f"this wallet does not sign {request['kind']}")
signed = Account.sign_message(encode_typed_data(full_message=dict(request["typedData"])), self._key)
return "0x" + bytes(signed.signature).hex()A signer that raises is a Declined("signer-failed", …).
The flows
- One call:
await transact(doc, binding, signer, client, inputs=…, agreement_signer=…). Where the pairing needs an agreement payment, the call returnsToApproveand signs nothing; once your agent approvesresult.approve, call again withapproved=result.approve. - Two calls:
await confirm(doc, binding, account, client, inputs), then your signer, thenfinish(atr_bytes, chosen, signature, binding). - Later:
check(atr_bytes, presented, binding, landed=…). The caller passes aslandedonly the receipt its own reader read for the payment, asfinishortransactreturned it, never a value another party sent. - Agreement:
await agree(atr_bytes, url, signer, client)returnsToApprove, signing nothing; once your agent approves it,await agree(atr_bytes, url, signer, client, approved=…)pays it and returnsAgreed. - Channels:
open_channel(atr_bytes, opened, binding, landed=…),await within(doc, hold, binding, signer),record_charge(hold, charged). The caller passes aslandedonly the receipt its own reader read for the opening, astransactreturned it, never a value another party sent.
The Python README runs each of them against the shared vectors, and the Python reference lists every export.
Running the tests
From a checkout, after pnpm install (the tests read the shared vectors from the installed @integraledger/lcp):
cd agentic-terms-py
uv run --locked --python 3.11 pytest -q
uv run --locked --python 3.14 pytest -q
uv run --locked --python 3.14 mypy --strict src testsLast updated on