This release is a pre-release and may not be stable for production use.
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 presignedPaymenttx blob per request.upto: oneCheckCreateper 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+.
pip install x402-xrpl
Note: the PyPI name uses a hyphen, but the import name uses an underscore:
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.
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:
# 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:
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:
app.middleware("http")(
require_payment(
# ...
extra={"sourceTag": 123},
)
)
IOU notes (non-XRP assets):
- Set
reqs.assetto the XRPL currency code (MUST be 3 chars or 40-hex). - Provide the issuer as
reqs.extra["issuer"](classic address). - Set
reqs.amountto the XRPL issued-currencyvaluestring (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:
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.
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)
AsyncSessionClientis the same client overhttpx.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 akeyyou can derive again and callclient.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 apayer_service.RemotePayeraspayer=. 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.AsyncSessionFacilitatoris the gateway's client for the facilitator.pricing.PriceTableis 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_serviceis the only process with the merchant's signing key, best a RegularKey (python -m x402_xrpl.session.custodymoves signing to one), andpython -m x402_xrpl.session.settlement_workercashes the sessions that closed. The docstrings ofsigner_from_env,worker_from_envandpayer_from_envlist 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:
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_dropson the payer options,x402_requests, orx402_purchaseto choose your application's fee ceiling. The default is10000drops (0.01 XRP), checked after autofill and before signing. ExistingsignedTxBlobpayloads and defaultbothinvoice 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. isValidandsuccessmust be JSON booleans.x402_purchasereturnssettle_failedfor a validsuccess: false. A malformed settlement header or unavailable paid-request response returnssettlement_unknown, withretryable=Falseand 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.
Metadata
Release files for x402-xrpl 0.4.0b1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| x402_xrpl-0.4.0b1.tar.gz | 111.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| x402_xrpl-0.4.0b1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 234.1 kB
Release files / x402_xrpl-0.4.0b1.tar.gz
| Download URL | x402_xrpl-0.4.0b1.tar.gz |
|---|---|
| Size | 111.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bca500e7564e8b1e9fe0894f04c7cb45a5cc6f00fe68cda1ffded8ebd40b9f0f
|
|
BLAKE2b-256 checksum How to use checksums |
e75fec57e9c5a04255cb0eb12b7261d4032ae7208f84d332e69351bca1924261
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.9
|
Release files / x402_xrpl-0.4.0b1-py3-none-any.whl
| Download URL | x402_xrpl-0.4.0b1-py3-none-any.whl |
|---|---|
| Size | 122.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
23908912538ee6d86f439d1ef8305a31133615beb369b28f8a42c69c72a51b9d
|
|
BLAKE2b-256 checksum How to use checksums |
5582c9c10868481cd49f909efe991e51cceace83e8cf2a79d85c97e60166e2cf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.9
|
Security for x402-xrpl 0.4.0b1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.