pay402 / source

Source

One file. Read it before you trust it.

The whole client is a single Python file, rendered below exactly as PyPI installs it. A tool that spends money should be small enough to read in a sitting, and this one is.

Releases

VersionDateWhat changedDownload
0.3.0current 2026‑08‑15 The receipt announces itself on success; refund_to rides the payment onto the receipt; --refund-to and $PAY402_REFUND_TO on the CLI. .tar.gz · .whl · PyPI
0.2.1 2026‑08‑10 Buyer inputs (--param, POST bodies), binary goods arrive as bytes, receipts state the buyer-paid total. PyPI
0.1.0 2026‑08‑03 First release: pay() and quote() on Base and Solana, spend cap required, no gas either rail. PyPI

The current release's files are served from this domain; every version is also on PyPI's release history, where the hashes are published.

The file

pay402.py 749 lines · 32 KB · MIT · Python 3.10+ Raw sdist
"""pay402 — buy anything behind a 402 with one call.

    from pay402 import pay
    goods = pay("https://checkout402.com/c/chk_abc", private_key=os.environ["AGENT_KEY"])

That's the whole buyer story: no account, no API key, no browser. The helper
does what an agent would otherwise hand-roll — request the URL, read the x402
challenge, pick a network it can pay on, sign an EIP-3009 authorization, and
retry with `X-PAYMENT` — and hands back the goods plus the receipt.

Why this exists in the repo: an agent that has to implement x402 correctly to
buy one thing will not buy the thing. The npm package (AGENTS.md M2.3) is the
same flow for the TypeScript ecosystem; `clients/pay402.js` is its reference
implementation.

Guardrails, because this spends real money:
- `max_usd` is REQUIRED for live payments. An agent looping on a paid endpoint
  with no ceiling is the failure mode that matters, so the default is refusal
  rather than a permissive default someone forgets to override.
- The fee is read from the challenge and shown in `quote()`, so an agent can
  decide before it commits.
- Nothing is signed until the amount has passed the cap check.
- The receipt URL is printed on success (stderr): it is the buyer's only
  durable proof, and it must survive in the transcript even when the caller
  drops the return value.

The agent-friendly shape is the CLI: the human sets $PAY402_KEY in the
environment, the agent runs `pay402 <url> --max-usd 5` and never sees,
reads or forwards the key. Signing happens in this process, policy is the
flags, and the conversation stays free of secrets — transcripts persist,
keys do not belong in them.

Depends only on `httpx` and `eth-account`.
"""

from __future__ import annotations

import base64
import json
from dataclasses import dataclass
from decimal import Decimal
from typing import Any

import httpx

X402_VERSION = 1
_ATOMIC_DECIMALS = {"eip155": 6, "solana": 6}  # USDC, both families


class Pay402Error(RuntimeError):
    """Anything that stops the purchase. `.detail` is safe to show a user."""

    def __init__(self, detail: str, *, response: httpx.Response | None = None):
        super().__init__(detail)
        self.detail = detail
        self.response = response


class PriceTooHigh(Pay402Error):
    """The quoted total exceeded `max_usd`. Nothing was signed."""


@dataclass(frozen=True, slots=True)
class Quote:
    """What paying would cost, read from the 402 before committing to it."""

    network: str
    price_usd: Decimal  # what the seller receives
    fee_usd: Decimal  # protocol fee, charged on top
    total_usd: Decimal  # what the buyer pays
    pay_to: str  # on-chain recipient (the splitter, not the seller)
    seller_wallet: str
    description: str
    requirement: dict

    def __str__(self) -> str:
        return (
            f"{self.description}: ${self.total_usd} "
            f"(${self.price_usd} + ${self.fee_usd} fee) on {self.network}"
        )


@dataclass(frozen=True, slots=True)
class Purchase:
    goods: Any
    receipt: dict
    quote: Quote


def _atomic_to_usd(atomic: str | int, network: str) -> Decimal:
    decimals = _ATOMIC_DECIMALS.get(network.split(":")[0], 6)
    return Decimal(int(atomic)) / (Decimal(10) ** decimals)


def _challenge(response: httpx.Response) -> dict:
    try:
        body = response.json()
    except ValueError:
        raise Pay402Error("402 response was not JSON", response=response) from None
    if body.get("x402Version") != X402_VERSION:
        raise Pay402Error(
            f"unsupported x402 version: {body.get('x402Version')}", response=response
        )
    if not body.get("accepts"):
        raise Pay402Error("402 challenge offered no payment options", response=response)
    return body


def quote(url: str, *, network: str | None = None, client: httpx.Client | None = None) -> Quote:
    """Fetch the 402 and report the cost. Signs nothing, spends nothing."""
    owned = client is None
    client = client or httpx.Client(timeout=30, follow_redirects=True)
    try:
        response = client.get(url, headers={"Accept": "application/json"})
    finally:
        if owned:
            client.close()

    if response.status_code != 402:
        raise Pay402Error(
            f"expected a 402 payment challenge, got {response.status_code}",
            response=response,
        )
    body = _challenge(response)
    accepts = body["accepts"]
    if network:
        chosen = next((a for a in accepts if a["network"] == network), None)
    else:
        # accepts[0] was picked blind. A server that lists Solana first would
        # hand an EVM-only caller a route it cannot sign, and the failure came
        # out as a signing error rather than "you have no key for that chain".
        chosen = next((a for a in accepts if _can_sign(a["network"])), None)
        if chosen is None and accepts:
            chosen = accepts[0]
    if chosen is None:
        offered = ", ".join(a["network"] for a in accepts)
        raise Pay402Error(f"network {network!r} not offered; this URL accepts: {offered}")

    extra = chosen.get("extra", {})
    net = chosen["network"]
    endpoint = body.get("endpoint") or {}
    # A platform-fee checkout advertises the seller's leg and the platform's
    # cut separately; the seller's PRICE is their sum. Showing only the
    # seller's leg here would make price + fee != total, and the cap check in
    # `pay` reads total anyway — this is display honesty, not money movement.
    price_atomic = int(extra.get("providerAmount", 0)) + int(
        extra.get("platformAmount", 0) or 0
    )
    return Quote(
        network=net,
        price_usd=_atomic_to_usd(price_atomic, net),
        fee_usd=_atomic_to_usd(extra.get("feeAmount", 0), net),
        total_usd=_atomic_to_usd(chosen["maxAmountRequired"], net),
        pay_to=chosen["payTo"],
        seller_wallet=extra.get("providerWallet", ""),
        description=endpoint.get("title") or chosen.get("description") or url,
        requirement=chosen,
    )


def pay(
    url: str,
    *,
    private_key: str | None = None,
    max_usd: str | Decimal | None = None,
    network: str | None = None,
    test_payer: str | None = None,
    params: dict[str, str] | None = None,
    refund_to: str | None = None,
    client: httpx.Client | None = None,
    announce: bool = True,
) -> Purchase:
    """Pay the checkout at `url` and return the goods.

    `max_usd` is the spend ceiling and is REQUIRED unless `test_payer` is used —
    an unbounded paid call in an agent loop is the failure this guards against.
    `test_payer` rehearses on the checkout's test face with no key and no funds.

    `params` are the buyer's inputs, string values: the checkout's
    `input_schema` fields, `tool`/`arguments` on an MCP checkout, `body` (a
    JSON string) on a POST/PUT/PATCH origin. They are folded into what you
    SIGN, so one payment buys one exact query — the server rejects any drift
    between the params you signed and the params you present.

    `refund_to` is a wallet YOU control, recorded on the receipt: the paying
    wallet is usually throwaway, so this is where a seller-issued goodwill
    refund can actually land. Optional, never required, moves no money.

    `announce=True` prints the receipt URL to stderr on success. That is
    deliberate for a library: the receipt is the buyer's only durable proof
    of purchase, and printing it means it survives in the transcript or log
    of whatever ran the purchase, even when the caller drops the return
    value. Pass False to stay silent.
    """
    owned = client is None
    client = client or httpx.Client(timeout=30, follow_redirects=True)
    try:
        q = quote(url, network=network, client=client)

        if test_payer is None:
            if private_key is None:
                raise Pay402Error("private_key is required (or pass test_payer for test mode)")
            if max_usd is None:
                raise Pay402Error(
                    "max_usd is required for a live payment — set the most you are "
                    "willing to spend, e.g. max_usd='5.00'"
                )
        if max_usd is not None and q.total_usd > Decimal(str(max_usd)):
            raise PriceTooHigh(
                f"total ${q.total_usd} exceeds max_usd ${Decimal(str(max_usd))} — "
                f"nothing was signed"
            )

        if test_payer is not None:
            payload = {"payer": test_payer}
        elif q.network.startswith("solana:"):
            payload = _sign_svm(url, q, private_key, client, params)  # type: ignore[arg-type]
        else:
            payload = _sign_evm(q, private_key, params)  # type: ignore[arg-type]
        header = (
            base64.urlsafe_b64encode(
                json.dumps({"network": q.network, "payload": payload,
                            **({"params": params} if params else {}),
                            **({"refund_to": refund_to} if refund_to else {}),
                            }).encode()
            )
            .decode()
            .rstrip("=")
        )
        response = client.post(f"{url.rstrip('/')}/pay", headers={"X-PAYMENT": header})
        if response.status_code != 200:
            raise Pay402Error(_explain(response), response=response)
        body = response.json()
        ful = body.get("fulfillment") or {}
        goods = ful.get("payload")
        if goods is None and ful.get("payload_base64") is not None:
            # Binary-safe deliveries (relay checkouts, e.g. gitbuyer's
            # tar.gz) arrive base64-wrapped; a buyer who PAID must get
            # bytes, not None — the first real sale had to hook raw HTTP
            # responses to salvage its own purchase.
            goods = base64.b64decode(ful["payload_base64"])
        receipt = body.get("receipt", {})
        if announce and receipt.get("url"):
            # The receipt is the buyer's only durable proof; stderr so it
            # lands in the transcript/log without polluting stdout goods.
            import sys

            print(f"pay402: receipt {receipt['url']}", file=sys.stderr)
        return Purchase(
            goods=goods,
            receipt=receipt,
            quote=q,
        )
    finally:
        if owned:
            client.close()


def _family(network: str) -> str:
    return network.split(":", 1)[0]


def _can_sign(network: str) -> bool:
    """Whether this machine has the library for that chain. Checked before a
    route is chosen so a missing dependency is reported as such."""
    fam = _family(network)
    try:
        if fam == "eip155":
            import eth_account  # noqa: F401
            return True
        if fam == "solana":
            import solders  # noqa: F401
            return True
    except ImportError:
        return False
    return False


def _sign_svm(url: str, q: Quote, private_key: str, client: httpx.Client,
              params: dict[str, str] | None = None) -> dict:
    """Sign the Solana transaction the SERVER built.

    Deliberately not built here. `verify_payment` allow-lists one exact
    instruction sequence — two ATA creations then two transfer_checked, with the
    relayer as fee payer at signer slot 0 — and a second implementation of that
    in the client is a second thing to keep in step with it. `/prepare` returns
    the unsigned transaction; this fills the buyer's slot and nothing else.

    It also needs a blockhash that is minutes old at most, which is why this
    cannot be derived from the 402 alone.
    """
    try:
        from solders.keypair import Keypair
        from solders.message import to_bytes_versioned
        from solders.transaction import VersionedTransaction
    except ImportError as exc:  # pragma: no cover — depends on the install
        raise Pay402Error(
            'paying on Solana requires the solana extra — pip install "pay402[solana]"'
        ) from exc

    payer = Keypair.from_base58_string(private_key)
    prep = client.post(
        f"{url.rstrip('/')}/prepare",
        json={"network": q.network, "payer": str(payer.pubkey()),
              **({"params": params} if params else {})},
    )
    if prep.status_code != 200:
        raise Pay402Error(_explain(prep), response=prep)
    prepared = prep.json()

    tx = VersionedTransaction.from_bytes(base64.b64decode(prepared["transaction"]))

    # NEVER sign this unverified. On EVM the buyer signs typed data whose
    # amounts and recipients are legible in the struct; here the payee hands
    # back an opaque transaction and asks for a signature on it. "Non-custodial"
    # would then mean only that they cannot hold your funds — not that they
    # cannot ask you to sign them away. The server allow-lists this exact
    # instruction set in `verify_payment`; this is the mirror image, and the
    # side that matters more, because it is the side holding the key.
    _verify_svm_tx(tx, q, str(payer.pubkey()))

    # Fill OUR slot only. Slot 0 is the relayer's and stays empty until settle;
    # signing it here would be signing for someone else's account.
    sigs = list(tx.signatures)
    sigs[int(prepared["payer_signer_index"])] = payer.sign_message(
        to_bytes_versioned(tx.message)
    )
    signed = VersionedTransaction.populate(tx.message, sigs)
    return {
        "transaction": base64.b64encode(bytes(signed)).decode(),
        "payer": str(payer.pubkey()),
    }


_TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
_ATA_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
_TRANSFER_CHECKED = 12          # SPL Token instruction discriminator


def _ata(owner: str, mint: str) -> str:
    from solders.pubkey import Pubkey

    addr, _ = Pubkey.find_program_address(
        [bytes(Pubkey.from_string(owner)),
         bytes(Pubkey.from_string(_TOKEN_PROGRAM)),
         bytes(Pubkey.from_string(mint))],
        Pubkey.from_string(_ATA_PROGRAM),
    )
    return str(addr)


def _verify_svm_tx(tx, q: Quote, payer: str) -> None:
    """Refuse to sign anything that is not exactly the quoted payment.

    Checks, against the 402 the buyer already saw rather than against anything
    the prepare step said:

      every instruction belongs to the token or ATA program — no third program
      exactly the quoted value transfers, and they are transfer_checked
      they move providerAmount and feeAmount — plus platformAmount when the
        402 advertised a platform cut — to the advertised wallets' ATAs
      they move it FROM the buyer's own ATA, so no other account is drained
      the buyer signs nothing beyond their own slot
    """
    from solders.pubkey import Pubkey

    extra = q.requirement.get("extra", {})
    mint = q.requirement["asset"]
    keys = list(tx.message.account_keys)

    def key(i: int) -> str:
        if i >= len(keys):
            raise Pay402Error("prepared transaction references an unknown account "
                              "(address-table lookups are not accepted)")
        return str(keys[i])

    # Address lookup tables would let accounts be swapped after this check.
    if getattr(tx.message, "address_table_lookups", None):
        raise Pay402Error("prepared transaction uses address lookup tables; refusing to sign")

    want = {
        _ata(extra["providerWallet"], mint): int(extra["providerAmount"]),
        _ata(extra["feeRecipient"], mint): int(extra["feeAmount"]),
    }
    legs = 2
    if extra.get("platformWallet") is not None:
        # The platform's cut is a third leg of the SAME quoted total — it was
        # visible in the 402 the buyer read, so requiring it here signs
        # nothing the quote did not already say. A missing platformAmount is
        # a malformed quote, refused rather than guessed at.
        if extra.get("platformAmount") is None:
            raise Pay402Error("quote advertises platformWallet without "
                              "platformAmount; refusing to sign")
        want[_ata(extra["platformWallet"], mint)] = int(extra["platformAmount"])
        legs = 3
    if len(want) != legs:
        raise Pay402Error("payment legs share an account; the split "
                          "cannot be verified")
    source = _ata(payer, mint)
    seen: dict[str, int] = {}

    for ix in tx.message.instructions:
        program = key(ix.program_id_index)
        if program == _ATA_PROGRAM:
            continue                      # creating a destination account moves no value
        if program != _TOKEN_PROGRAM:
            raise Pay402Error(f"prepared transaction calls an unexpected program {program}")
        data = bytes(ix.data)
        if not data or data[0] != _TRANSFER_CHECKED:
            raise Pay402Error("prepared transaction contains a non-transfer token "
                              "instruction; refusing to sign")
        # transfer_checked accounts: [source, mint, destination, authority]
        src, _mint_ix, dest, authority = (key(i) for i in list(ix.accounts)[:4])
        if src != source:
            raise Pay402Error(f"transfer spends from {src}, not your token account")
        if authority != payer:
            raise Pay402Error("transfer is authorised by someone other than you")
        amount = int.from_bytes(data[1:9], "little")
        seen[dest] = seen.get(dest, 0) + amount

    if seen != want:
        raise Pay402Error(
            "prepared transaction does not match the quote — it moves "
            f"{sorted(seen.values())} to {sorted(seen)}, the 402 said "
            f"{sorted(want.values())} to {sorted(want)}"
        )
    total = sum(seen.values())
    if total != int(q.requirement["maxAmountRequired"]):
        raise Pay402Error(f"transaction moves {total}, the quote said "
                          f"{q.requirement['maxAmountRequired']}")


def _explain(response: httpx.Response) -> str:
    """Turn a failure into something an agent can act on — notably the
    paid-but-no-data case, where a receipt exists and must not be lost."""
    try:
        detail = response.json().get("detail")
    except ValueError:
        return f"payment failed with HTTP {response.status_code}"
    if isinstance(detail, dict):
        if detail.get("receipt_id"):
            return (
                f"{detail.get('message', 'delivery failed')}: {detail.get('reason', '')} "
                f"— you PAID and hold receipt {detail['receipt_id']} "
                f"(tx {detail.get('tx_hash')}). {detail.get('next_step', '')}"
            ).strip()
        # Server-side recovery hints (help block) — surface the actionable line
        # rather than dumping JSON at the model.
        msg = detail.get("message") or f"payment failed with HTTP {response.status_code}"
        help_block = detail.get("help") or {}
        parts = [msg]
        if help_block.get("fix"):
            parts.append(f"fix: {help_block['fix']}")
        if help_block.get("accepted_networks"):
            parts.append("accepted networks: " + ", ".join(help_block["accepted_networks"]))
        return " — ".join(parts)
    return str(detail or f"payment failed with HTTP {response.status_code}")


def _resource_with_params(resource: str, params: dict[str, str] | None) -> str:
    """The exact string the server binds into the nonce: the quote's resource
    plus the buyer's params in the server's own canonical form (stringified,
    sorted by key — see `canonical_params` server-side). Byte parity matters:
    a different encoding here is a signature the splitter rejects."""
    if not params:
        return resource
    from urllib.parse import urlencode

    canon = {str(k): str(v) for k, v in sorted(params.items(), key=lambda kv: str(kv[0]))}
    return f"{resource}?{urlencode(canon)}"


def _recompute_evm_nonce(q: Quote, stub: dict,
                         params: dict[str, str] | None) -> str:
    """Recompute the splitter nonce from the QUOTE the buyer approved, using
    the v1 preimage — or the v2 preimage when the requirement carries the
    platform leg (`extra.platformWallet`/`platformAmount`, the same fields the
    SVM rail advertises). Every routed field comes from the quote; only the
    per-payment freshness (salt, expiry, config version) comes from the
    prepared stub, because varying those cannot re-route a cent. Field order
    mirrors the contracts byte-for-byte:

      v1: domain, chainId, usdc, splitter, provider, providerAmount,
          feeAmount, resourceId, paymentConfigVersion, validBefore, payerSalt
      v2: same, with (platformRecipient, platformAmount) inserted after
          feeAmount and domain "endpoint.farm:v2"
    """
    from eth_abi import encode as abi_encode
    from eth_utils import keccak, to_checksum_address

    extra = q.requirement.get("extra", {})
    platform_wallet = extra.get("platformWallet")
    chain_id = int(q.network.split(":", 1)[1])
    resource = _resource_with_params(q.requirement["resource"], params)
    salt = bytes.fromhex(stub["payerSalt"].removeprefix("0x"))

    if platform_wallet is not None:
        types = ["string", "uint256", "address", "address", "address", "uint256",
                 "uint256", "address", "uint256", "bytes32", "uint256", "uint256",
                 "bytes32"]
        values = [
            "endpoint.farm:v2",
            chain_id,
            to_checksum_address(q.requirement["asset"]),
            to_checksum_address(q.requirement["payTo"]),
            to_checksum_address(extra["providerWallet"]),
            int(extra["providerAmount"]),
            int(extra["feeAmount"]),
            to_checksum_address(platform_wallet),
            int(extra["platformAmount"]),
            keccak(text=resource),
            int(stub["paymentConfigVersion"]),
            int(stub["validBefore"]),
            salt,
        ]
    else:
        types = ["string", "uint256", "address", "address", "address", "uint256",
                 "uint256", "bytes32", "uint256", "uint256", "bytes32"]
        values = [
            "endpoint.farm:v1",
            chain_id,
            to_checksum_address(q.requirement["asset"]),
            to_checksum_address(q.requirement["payTo"]),
            to_checksum_address(extra["providerWallet"]),
            int(extra["providerAmount"]),
            int(extra["feeAmount"]),
            keccak(text=resource),
            int(stub["paymentConfigVersion"]),
            int(stub["validBefore"]),
            salt,
        ]
    return "0x" + keccak(abi_encode(types, values)).hex()


def _check_prepared_evm(q: Quote, prep: dict,
                        params: dict[str, str] | None = None) -> None:
    """Refuse to sign a prepared payment that does not match the QUOTE.

    Mirror of `_verify_svm_tx` and its loud-failure philosophy: the wallet
    shows the user an opaque nonce, so the only party who can check that the
    typed data commits to the split the buyer approved — including the
    platform's cut when the requirement advertises one — is this client,
    before the signature exists. Everything is checked against the quote, not
    against the prepare response's own claims.
    """
    from eth_utils import to_checksum_address

    extra = q.requirement.get("extra", {})
    stub = prep.get("payload_stub", {})
    msg = prep.get("typed_data", {}).get("message", {})

    def want(field: str, got, expected) -> None:
        if got != expected:
            raise Pay402Error(
                f"prepared payment does not match the quote ({field}: "
                f"{got!r} != {expected!r}); refusing to sign"
            )

    domain = prep.get("typed_data", {}).get("domain", {})
    want("chainId", int(domain["chainId"]), int(q.network.split(":", 1)[1]))
    want("asset", to_checksum_address(domain["verifyingContract"]),
         to_checksum_address(q.requirement["asset"]))
    want("payTo", to_checksum_address(msg["to"]),
         to_checksum_address(q.requirement["payTo"]))
    want("total", str(msg["value"]), str(q.requirement["maxAmountRequired"]))
    want("total", str(stub["total"]), str(q.requirement["maxAmountRequired"]))
    want("provider", to_checksum_address(stub["provider"]),
         to_checksum_address(extra["providerWallet"]))
    want("providerAmount", str(stub["providerAmount"]), str(extra["providerAmount"]))
    want("feeAmount", str(stub["feeAmount"]), str(extra["feeAmount"]))

    if extra.get("platformWallet") is not None:
        # A three-leg quote (the platform's cut rides as its own leg — same
        # fields the SVM rail uses). The prepared payload must carry the SAME
        # leg: a stub without it would sign a v1-shaped payment that either
        # fails at verify or, against a lying server, pays a different split
        # than the buyer was shown. A missing platformAmount in the quote
        # itself is half a leg and equally refused.
        if extra.get("platformAmount") is None:
            raise Pay402Error("quote advertises platformWallet without "
                              "platformAmount; refusing to sign")
        if "platformWallet" not in stub or "platformAmount" not in stub:
            raise Pay402Error(
                "quote advertises a platform leg but the prepared payment "
                "does not carry it; refusing to sign"
            )
        want("platformWallet", to_checksum_address(stub["platformWallet"]),
             to_checksum_address(extra["platformWallet"]))
        want("platformAmount", str(stub["platformAmount"]), str(extra["platformAmount"]))
    elif "platformWallet" in stub or "platformAmount" in stub:
        raise Pay402Error(
            "prepared payment carries a platform leg the quote never "
            "advertised; refusing to sign"
        )

    # The nonce is the commitment to the whole route. Recompute it from the
    # quote and refuse a typed-data nonce that differs — this is what binds
    # the v1/v2 preimage (and the platform leg inside it) to what was quoted.
    want("nonce", str(msg["nonce"]).lower(),
         _recompute_evm_nonce(q, stub, params).lower())


def _sign_evm(q: Quote, private_key: str,
              params: dict[str, str] | None = None) -> dict:
    """Sign the EIP-3009 authorization for `q`.

    Uses the server's `/prepare` step for the typed data — but never blindly:
    `_check_prepared_evm` recomputes the nonce commitment (v1, or v2 when the
    quote advertises a platform leg) and refuses to sign a prepared payment
    that differs from the quote in any routed field. The wallet-free
    equivalent of what the browser does in app/static/pay.js.
    """
    try:
        from eth_account import Account
        from eth_account.messages import encode_typed_data
    except ImportError:  # pragma: no cover - dependency hint
        raise Pay402Error('signing on Base requires the evm extra — pip install "pay402[evm]"') from None

    account = Account.from_key(private_key)
    resource = q.requirement["resource"]
    # follow_redirects like every other call here: the server's resource URL
    # should be canonical, but a signpost (e.g. a legacy checkout URL) must
    # not strand the one live-rail path that had no redirect tolerance.
    with httpx.Client(timeout=30, follow_redirects=True) as client:
        prep_response = client.post(
            f"{resource.rstrip('/')}/prepare",
            json={"payer": account.address, "network": q.network,
                  **({"params": params} if params else {})},
        )
    if prep_response.status_code != 200:
        raise Pay402Error(f"could not prepare payment: {_explain(prep_response)}")
    prep = prep_response.json()
    _check_prepared_evm(q, prep, params)

    signed = account.sign_message(encode_typed_data(full_message=prep["typed_data"]))
    payload = dict(prep["payload_stub"])
    payload["signature"] = {
        "r": "0x" + signed.r.to_bytes(32, "big").hex(),
        "s": "0x" + signed.s.to_bytes(32, "big").hex(),
        "v": signed.v if signed.v >= 27 else signed.v + 27,
    }
    return payload


__all__ = ["pay", "quote", "Quote", "Purchase", "Pay402Error", "PriceTooHigh"]


# --------------------------------------------------------------------- CLI

def main(argv: list[str] | None = None) -> int:
    """`pay402 <url> --key …` — the command every checkout page prints.

    It printed it for a while before this existed, alongside `pip install
    pay402` for a package that was never published, so the one instruction a
    buyer is given failed at both steps. The library was importable and the
    command was not.
    """
    import argparse
    import os

    ap = argparse.ArgumentParser(
        prog="pay402",
        description="Buy anything behind an HTTP 402. No account, no API key.",
    )
    ap.add_argument("url", help="the checkout URL")
    ap.add_argument("--key", default=os.environ.get("PAY402_KEY"),
                    help="buyer private key (EVM 0x… or Solana base58). "
                         "Defaults to $PAY402_KEY, which keeps it out of shell history.")
    ap.add_argument("--max-usd", dest="max_usd",
                    help="spend ceiling. Required to pay; nothing is signed above it.")
    ap.add_argument("--network", help="force a chain, e.g. eip155:8453")
    ap.add_argument("--param", action="append", default=[], metavar="KEY=VALUE",
                    help="buyer input, repeatable — schema params, or "
                         "tool=/arguments= for MCP checkouts. Folded into "
                         "what you sign: one payment, one exact query.")
    ap.add_argument("--body", help="request body for a POST/PUT/PATCH origin "
                                   "— a JSON string; shorthand for "
                                   "--param body='…'")
    ap.add_argument("--quote", action="store_true",
                    help="print the price and exit. Signs nothing.")
    ap.add_argument("--test-payer", dest="test_payer",
                    help="pay a TEST-mode checkout as this address; moves no money")
    ap.add_argument("--refund-to", dest="refund_to",
                    default=os.environ.get("PAY402_REFUND_TO"),
                    help="a wallet YOU control, recorded on the receipt as "
                         "where a seller-issued refund goes — the paying "
                         "wallet is usually throwaway. Defaults to "
                         "$PAY402_REFUND_TO.")
    ap.add_argument("--json", action="store_true", help="machine-readable output")
    args = ap.parse_args(argv)

    try:
        if args.quote:
            q = quote(args.url, network=args.network)
            if args.json:
                print(json.dumps({
                    "network": q.network, "price_usd": str(q.price_usd),
                    "fee_usd": str(q.fee_usd), "total_usd": str(q.total_usd),
                    "pay_to": q.pay_to, "description": q.description,
                }, indent=2))
            else:
                print(f"{q.description}")
                print(f"  price  ${q.price_usd}")
                print(f"  fee    ${q.fee_usd}")
                print(f"  total  ${q.total_usd}  on {q.network}")
            return 0

        if not args.key and not args.test_payer:
            ap.error("--key is required (or --test-payer for a test checkout)")

        params: dict[str, str] = {}
        for pair in args.param:
            key_, sep, value = pair.partition("=")
            if not sep or not key_:
                ap.error(f"--param wants KEY=VALUE, got {pair!r}")
            params[key_] = value
        if args.body is not None:
            params["body"] = args.body

        out = pay(args.url, private_key=args.key, max_usd=args.max_usd,
                  network=args.network, test_payer=args.test_payer,
                  params=params or None, refund_to=args.refund_to,
                  announce=False)  # the CLI prints its own receipt line
        if args.json:
            print(json.dumps({"goods": out.goods, "receipt": out.receipt}, indent=2,
                             default=str))
        else:
            r = out.receipt or {}
            print(f"paid ${out.quote.total_usd} on {out.quote.network}")
            if r.get("tx_hash"):
                print(f"  tx      {r['tx_hash']}")
            if r.get("url"):
                print(f"  receipt {r['url']}")
            print()
            print(out.goods if isinstance(out.goods, str)
                  else json.dumps(out.goods, indent=2, default=str))
        return 0
    except PriceTooHigh as exc:
        # Distinct exit code: an agent retrying a too-expensive call should not
        # be told the same thing as an agent whose key was wrong.
        print(f"pay402: {exc}", file=__import__("sys").stderr)
        return 3
    except Pay402Error as exc:
        print(f"pay402: {exc}", file=__import__("sys").stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())

What you read here is the file the current release installs. Line links work like you expect: #L1 is the first line.