Metadata-Version: 2.5
Name: integraledger-terms
Version: 0.3.1
Summary: Buyer gate: compare the ATR hash with the served bytes, and build the payment with that hash, before signing.
Project-URL: Homepage, https://github.com/IntegraLedger/integra-agentic-terms/tree/main/agentic-terms-py#readme
Project-URL: Repository, https://github.com/IntegraLedger/integra-agentic-terms
Project-URL: Issues, https://github.com/IntegraLedger/integra-agentic-terms/issues
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agentic-commerce,ai-agents,atr,buyer,lcp,legal-context-protocol,mpp,payments,sha256,x402
Requires-Python: >=3.11
Requires-Dist: cryptography==50.0.2
Requires-Dist: httpx<0.29,>=0.28.1
Description-Content-Type: text/markdown

# 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.

```sh
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`](https://www.npmjs.com/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

```sh
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](https://github.com/ethereum/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.

```python
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())
```

```text output
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](#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](https://github.com/IntegraLedger/integra-agentic-terms/blob/main/docs/guides/rails.md)
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`).

```python
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())
```

```text output
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](#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](#sign-elsewhere-confirm-then-finish).

### 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:

```python
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())
```

```text output
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, landed=None)` 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`, pass it as `landed`, beside the payment: a
`"landed"` member written into the payment is not read. The caller passes as `landed` only the receipt its own reader
read for the payment, as `finish` or `transact` returned it, never a value another party sent.

### 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`, `broadcaster`. |
| `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 binding's `broadcaster(presented)` names who puts the presented payment on its rail: `"payer"`, which sent it before
presenting it, or `"payee"`, the seller, or the facilitator or processor acting for it, which sends or executes it. It
answers only for a payment of a type and kind the pairing takes, within the shape gate `bound` applies first, and gives
any other payment the `Refusal` `bound` gives that fault. Where a payment has a second fault that `bound` finds first,
in H or the challenge, the two codes differ.

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](https://github.com/IntegraLedger/integra-agentic-terms/blob/main/docs/guides/signers.md)
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.

<!-- pairings:python:start -->
| 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 |
<!-- pairings:python:end -->

## 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](https://github.com/IntegraLedger/integra-agentic-terms#readme) and
[CONTRIBUTING.md](https://github.com/IntegraLedger/integra-agentic-terms/blob/main/CONTRIBUTING.md).

## License

Apache-2.0. See [LICENSE](https://github.com/IntegraLedger/integra-agentic-terms/blob/main/agentic-terms-py/LICENSE).
