Metadata-Version: 2.4
Name: x402-xrpl
Version: 0.4.0b1
Summary: XRPL implementation of x402 payments (exact, upto and batch-settlement Check sessions) with client + FastAPI/Starlette helpers
Author: Facilitator Team
Project-URL: Homepage, https://xrpl-ai.org/build
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi<0.116,>=0.115
Requires-Dist: uvicorn[standard]<0.31,>=0.30
Requires-Dist: xrpl-py>=4.1.0
Requires-Dist: pydantic<3,>=2
Requires-Dist: python-dotenv<2,>=1
Requires-Dist: requests<3,>=2
Requires-Dist: prometheus-client<0.22,>=0.21
Requires-Dist: redis<6,>=5
Requires-Dist: psycopg[binary,pool]<4,>=3.2
Requires-Dist: google-cloud-storage<3,>=2.19
Requires-Dist: httpx>=0.28.1
Requires-Dist: cryptography>=45.0.6
Requires-Dist: openai<2,>=1
Dynamic: license-file

# x402 XRPL Python SDK

This package provides a small, spec‑aligned SDK for working with x402 payments over XRPL. It is designed to be reusable by both **buyer clients** (creating `PAYMENT-SIGNATURE` headers) and **seller/resource servers** (verifying and settling via a facilitator).

It covers three schemes:

- `exact`: one **presigned `Payment` tx blob** per request.
- `upto`: one `CheckCreate` per request, cashed for the metered amount.
- `batch-settlement` (beta): a Check session, in which one Check pays for many signed requests and
  is cashed once when the session closes. See "Check sessions" below.

Overview and links: <https://xrpl-ai.org/build>.

---

## Install (PyPI)

Requires Python 3.11+.

```bash
pip install x402-xrpl
```

Note: the PyPI name uses a hyphen, but the import name uses an underscore:

```python
import x402_xrpl
```

---

## Quickstart: Buyer Client (requests-style)

If you want the UX of `x402_requests(...)` (auto-handle 402), use `x402_xrpl.clients.requests`:

Buyer clients call the protected resource URL and use XRPL RPC plus optional filters. They do not take a facilitator URL.

```python
import requests

from x402_xrpl.clients.requests import x402_requests
from x402_xrpl.clients.base import decode_payment_response
from x402_xrpl.xrpl_wallet import wallet_from_seed

XRPL_RPC = "https://s.altnet.rippletest.net:51234/"
RESOURCE_URL = "http://127.0.0.1:8080/xrpl-demo/resource"

# Not xrpl-py's Wallet.from_seed(seed): it reads a secp256k1 seed (`s…`, not `sEd…`) as Ed25519,
# which gives another key pair and address. wallet_from_seed uses the key type the seed encodes.
wallet = wallet_from_seed("…demo seed…")

session: requests.Session = x402_requests(
    wallet,
    rpc_url=XRPL_RPC,
    # Optional filters so most users don't write a custom selector:
    network_filter="xrpl:1",
    scheme_filter="exact",
)

resp = session.get(RESOURCE_URL, timeout=180)
print(resp.status_code, resp.text)

if "PAYMENT-RESPONSE" in resp.headers:
    settlement = decode_payment_response(resp.headers["PAYMENT-RESPONSE"])
    print("settled tx:", settlement.get("transaction"))
```

### Safe retries for verify-stage outages

When the resource server returns a typed transient response with HTTP 503,
`retryable=true`, `phase=verify`, and `settlementAttempted=false`, the buyer session
reuses the same invoice and `PAYMENT-SIGNATURE` for at most two retries while the
payment requirement remains within `maxTimeoutSeconds`. `Retry-After` controls the
delay. It never retries a response where settlement may have been attempted.

FastAPI/Starlette resource servers using `require_payment` propagate facilitator
verify-stage 408, 429, 5xx, and transport timeouts as this typed 503 response. The
response includes `retryAfter`, `correlationId`, and the two safety fields above. This
is fail-closed: the protected handler and `/settle` are not called. Policy denials and
other 4xx responses remain non-retryable.

## Cross-currency payments (opt-in)

By default the payer funds the exact quoted asset: an XRP quote is paid in XRP (no `SendMax`),
and an IOU quote is paid with a same-asset `SendMax` equal to `Amount`. When the merchant
advertises `extra["crossCurrency"] = True`, you may fund from a **different** asset by passing
`pay_with` to `prepare_payment` as an XRP drops string or an `IssuedCurrencyAmount`:

```python
# XRP -> IOU: fund a USD quote with XRP (a drops-string SendMax)
payer.prepare_payment(req, pay_with="1000000")

# IOU -> XRP, or IOU -> a different IOU: fund with an issued-currency SendMax
from xrpl.models.amounts import IssuedCurrencyAmount
payer.prepare_payment(req, pay_with=IssuedCurrencyAmount(currency="USD", issuer="rISSUER", value="5"))
```

The destination `Amount` is always pinned to the quote (cross-currency only relaxes the
`SendMax` asset). The signed `SendMax` is your own spend ceiling. Before signing, the client
refuses: a `pay_with` when the merchant did not opt in, a drops-string `pay_with` for an XRP
destination (XRP→XRP is not cross-currency), and any non-positive or malformed `SendMax` —
matching the facilitator's verifier so a bad value never wastes a signed transaction.

## Quickstart: Protecting a FastAPI Route with `require_payment` (recommended)

To protect a route (e.g. `/ai-news`) with XRPL x402 payments using an ergonomic wrapper:

```python
from fastapi import FastAPI

from x402_xrpl.server import require_payment

app = FastAPI()

app.middleware("http")(
    require_payment(
        path="/ai-news",
        price="1000",  # XRP drops; for IOUs use the XRPL value string (e.g. "1.25")
        pay_to_address="rhaDe3NBxgUSLL12N5Sxpii2xy8vSyXNG6",
        network="xrpl:1",
        asset="XRP",
        facilitator_url="http://127.0.0.1:8011",
        resource="demo:ai-news",
        description="AI news feed (paid)",
    )
)
```

### XRPL SourceTag (analytics)

By default, this SDK issues `PaymentRequirements.extra["sourceTag"] = 804681468` and buyer clients will sign XRPL `Payment` transactions with `SourceTag = 804681468` (so XRPL can query/aggregate these payments on-ledger).

To override the tag for your app, pass your own value in `extra`:

```python
app.middleware("http")(
    require_payment(
        # ...
        extra={"sourceTag": 123},
    )
)
```

---

**IOU notes** (non-XRP assets):

- Set `reqs.asset` to the XRPL currency code (**MUST** be 3 chars or 40-hex).
- Provide the issuer as `reqs.extra["issuer"]` (classic address).
- Set `reqs.amount` to the XRPL issued-currency `value` string (e.g. `"1"`, `"1.25"`).

For RLUSD, raw payment requirements should use `524C555344000000000000000000000000000000` as `reqs.asset` and include the
issuer in `reqs.extra["issuer"]`.

If you want a human-friendly display string (or an opt-in conversion of a symbol like `"RLUSD"` into a 40-hex code), use the currency helpers:

```python
from x402_xrpl.xrpl_currency import display_currency_code, resolve_currency_code

asset = "524C555344000000000000000000000000000000"
print(display_currency_code(asset))  # "RLUSD" (best-effort)

# Opt-in convenience (only use if your app has a trusted mapping/intent):
asset_hex = resolve_currency_code("RLUSD", allow_utf8_symbol=True)
```

## Check sessions (`batch-settlement`, beta)

A Check session pays for many requests with one XRPL Check. The payer signs one `CheckCreate` up
to a ceiling, signs each request with a session key (HTTP Message Signatures, RFC 9421), and the
merchant cashes the Check once, for what the requests used, when the session closes. In x402 terms
this is the `batch-settlement` scheme with the XRPL binding `xrpl-check-session`, version 1, in
`x402_xrpl.session`. It is a beta: its API may still change before 0.4.0.

### Paying with sessions

A client talks only to the resource server. It needs the payer's wallet and an XRPL JSON-RPC node.

```python
from x402_xrpl.session.client import SessionClient
from x402_xrpl.session.ledger import HttpJsonRpc
from x402_xrpl.session.wire import decode_b64_json
from x402_xrpl.xrpl_wallet import wallet_from_seed

RESOURCE_URL = "https://rs.example/v1/chat/completions"
body = {"model": "…", "messages": [{"role": "user", "content": "Hello"}]}

client = SessionClient(
    wallet=wallet_from_seed("…payer seed…"),
    rpc=HttpJsonRpc("https://s.altnet.rippletest.net:51234/"),
)

# The unpaid request's 402 carries the session offer.
offer = client.discover(RESOURCE_URL, json_body=body)

# One CheckCreate for up to 1 unit of the offer's asset. The ceiling must lie between
# offer.min_ceiling and offer.max_ceiling, the duration within offer.max_session_seconds.
session = client.open(offer, ceiling="1", session_seconds=3600)

# A paid request is charged at most max_amount; its receipt is in PAYMENT-RESPONSE
# (session.receipts keeps them too).
response = client.request(session, "POST", RESOURCE_URL, max_amount="0.05", json_body=body)
receipt = decode_b64_json(response.headers["PAYMENT-RESPONSE"], reason="payment_response")

# After the close, the merchant cashes the Check once, for the sum of the receipts.
client.close(session)
```

- `AsyncSessionClient` is the same client over `httpx.AsyncClient`, for many requests in flight
  on one session.
- A client that lost a request's answer looks it up with `client.lookup(session, request_id)`. To
  find a session again after a restart, open it with a `key` you can derive again and call
  `client.restore(offer, session_id, key)`.
- A platform whose clients must not hold the payer's key runs the payer service
  (`python -m x402_xrpl.session.payer_service`) and gives each client a
  `payer_service.RemotePayer` as `payer=`. The service can sign through a Privy server wallet
  (`privy.PrivySigner`).

### Selling with sessions

A resource server needs a tenant API key from its facilitator's operator. The tenant is bound to
the server's `payTo` accounts, origins and paths, decides which payers it admits and caps their
ceilings, live sessions and request rate; a payer it does not admit is refused
(`payer_not_allowed`) when it opens a session. The parts:

- `server.SessionGateway` (FastAPI) serves the session endpoints and the 402 offer, reserves each
  signed request and commits its metered usage (`SessionCharge`).
- `facilitator.AsyncSessionFacilitator` is the gateway's client for the facilitator.
- `pricing.PriceTable` is the price table each session's terms freeze.
- The merchant settles its own sessions: the facilitator never holds a key that can sign for it.
  `python -m x402_xrpl.session.signer_service` is the only process with the merchant's signing
  key, best a RegularKey (`python -m x402_xrpl.session.custody` moves signing to one), and
  `python -m x402_xrpl.session.settlement_worker` cashes the sessions that closed. The docstrings
  of `signer_from_env`, `worker_from_env` and `payer_from_env` list their settings.

Mainnet (`xrpl:0`) is opt-in wherever a service signs: `MerchantSigner(mainnet=True)` or
`MERCHANT_MAINNET_ENABLED=true` for the merchant's signer, `PAYER_MAINNET_ENABLED=true` for the
payer service, and `--mainnet` for `custody`.

## Optional X402 Secure / Verifiable Intent

Buyer clients can attach a Verifiable Intent L1-L3 chain through the `verifiable_intent_provider` seam. When the
hosted XRPL Facilitator receives `extensions.x402Secure`, it calls X402 Secure and Trustline before settlement. Plain
XRPL x402 payments without the extension remain on the normal path.

Use `RemoteIssuerProvider` when the agent cannot hold the Trustline issuer secret. The remote endpoint only issues L1;
the SDK still signs L2 with the owner key and L3 with the agent key locally:

```python
from x402_xrpl.vi import RemoteIssuerProvider, RemoteIssuerSource

provider = RemoteIssuerProvider(
    issuer=RemoteIssuerSource(
        endpoint="https://agent.example/api/vi/issue-l1",
        headers=lambda: {"authorization": f"Bearer {short_lived_issuer_token}"},
    ),
    issue_request={
        "subject": owner_account_ref,
        "ownerPublicJwk": owner_public_jwk,
        "allowedChains": ["xrpl"],
        "allowedAssets": ["XRP"],
        "spendingCeiling": "0.001",
        "validitySeconds": 3600,
        "ownerProof": owner_proof,
    },
    owner_private_jwk=owner_private_jwk,
    owner_kid=owner_kid,
    agent_private_jwk=agent_private_jwk,
    agent_public_jwk=agent_public_jwk,
    agent_kid=agent_kid,
    constraints={"allowed_chains": ["xrpl"], "allowed_assets": ["XRP"], "per_transaction_max": "0.001"},
)
```

Do not put Trustline API keys, issuer secrets, wallet seeds, owner private keys, or agent private keys into the x402
payment payload.

## Payment safety

- Before signing, the payer requires its configured network, quote network, RPC
  `server_info.info.network_id`, and any autofilled network ID to agree. Missing RPC network
  information is rejected. Explicit invoice IDs must match the quote when both are present.
- Set `max_fee_drops` on the payer options, `x402_requests`, or `x402_purchase` to choose your
  application's fee ceiling. The default is `10000` drops (0.01 XRP), checked after autofill and
  before signing. Existing `signedTxBlob` payloads and default `both` invoice binding are preserved.
- Header helpers and payer methods accept optional `resource`; high-level clients forward the
  advertised resource. Existing custom header factories without this parameter still work.
- `isValid` and `success` must be JSON booleans. `x402_purchase` returns `settle_failed` for a
  valid `success: false`. A malformed settlement header or unavailable paid-request response
  returns `settlement_unknown`, with `retryable=False` and original transaction recovery metadata
  when available. Query the original transaction before attempting another payment; this lookup
  is the application's responsibility. A true flag alone is not independent ledger proof.
- Facilitator HTTP exception messages contain status and a validated request ID instead of
  the response body or request URL. The original response is still available explicitly.

---
