Python SDK for integrating x402 cryptocurrency payments via the Ultravioleta DAO facilitator.
Accept gasless stablecoin payments across 25 blockchain networks with a single integration. The SDK handles signature verification, on-chain settlement, and all the complexity of multi-chain payments.
New in v0.26.0: Robinhood Chain support (robinhood / robinhood-testnet, chain IDs 4663 / 46630), settling in Paxos USDG (EIP-712 domain Global Dollar version 1, sent via extra since version() reverts on-chain).
- 25 Networks: EVM chains (15 including Robinhood, Scroll, SKALE), SVM chains (Solana, Fogo), NEAR, Stellar, Algorand, Sui, and XRPL (native XRP)
- 6 Stablecoins: USDC, EURC, AUSD, PYUSD, USDT, USDG (EVM chains); XRPL settles in native XRP
- x402 v1 & v2: Full support for both protocol versions with auto-detection
- Framework Integrations: Flask, FastAPI, Django, AWS Lambda
- Gasless Payments: Users sign EIP-712/EIP-3009 authorizations, facilitator pays all network fees
- Simple API: Decorators and middleware for quick integration
- Type Safety: Full Pydantic models and type hints
- Extensible: Register custom networks and tokens easily
- ERC-8004 Trustless Agents: On-chain reputation and identity for AI agents (20 networks: 18 EVM + Solana + Solana-devnet)
- Escrow & Refunds: Hold payments in escrow with dispute resolution (11 EVM chains + SKALE via CREATE3)
- Commerce Scheme: Supports
"exact","escrow", and"commerce"schemes (facilitator v1.43.0+) - Server-Side Signing:
connect_with_private_key()for backend EIP-3009 signing without browser wallet /acceptsNegotiation: Discover facilitator capabilities before constructing payments- Bazaar Discovery: Register and discover paid resources across the x402 network
- x402 v2 Envelopes:
build_verify_request_v2/build_settle_request_v2for facilitators advertising CAIP-2 networks - Live Traffic Stream: Subscribe to
GET /events(SSE) for settlements as they happen — lossy live hint, not a ledger - Facilitator Info: Query version, supported networks, blacklist, and health
- WalletAdapter: Abstract protocol for wallet signing (EnvKeyAdapter, OWSWalletAdapter)
- ERC-8128 Signed HTTP Requests: RFC 9421 request signing with any WalletAdapter — authenticate against wallet-signed APIs like Execution Market
- Escrow Pre-Auth Builder:
build_escrow_pre_auth()/compute_escrow_nonce()— sign the ADR-002 sign-on-assignment escrow lock (X-Payment-Authheader) with any WalletAdapter, no web3 required
from decimal import Decimal
from uvd_x402_sdk import X402Client
client = X402Client(recipient_address="0xYourWallet...")
result = client.process_payment(request.headers["X-PAYMENT"], Decimal("10.00"))
print(f"Paid by {result.payer_address}, tx: {result.transaction_hash}")Here's a complete example showing the full x402 payment flow - returning a 402 response when no payment is provided, and processing the payment when it arrives:
from decimal import Decimal
from uvd_x402_sdk import (
X402Client,
X402Config,
create_402_response,
PaymentRequiredError,
)
# 1. Configure the client with your recipient addresses
config = X402Config(
recipient_evm="0xYourEVMWallet...", # For Base, Ethereum, etc.
recipient_solana="YourSolanaAddress...", # For Solana, Fogo
recipient_near="your-account.near", # For NEAR
recipient_stellar="G...YourStellarAddress", # For Stellar
recipient_algorand="YOUR_ALGO_ADDRESS...", # For Algorand
recipient_sui="0xYourSuiAddress...", # For Sui
recipient_xrpl="r...YourXRPLAddress", # For XRP Ledger (native XRP)
)
client = X402Client(config=config)
def handle_api_request(request):
"""
Handle an API request that requires payment.
"""
price = Decimal("1.00") # $1.00 USD
# Check if payment header exists
x_payment = request.headers.get("X-PAYMENT")
if not x_payment:
# No payment provided - return 402 Payment Required
return {
"status": 402,
"headers": {
"Content-Type": "application/json",
},
"body": create_402_response(
amount_usd=price,
config=config,
resource="/api/premium",
description="Premium API access",
)
}
# Payment provided - verify and settle
try:
result = client.process_payment(x_payment, price)
# Payment successful!
return {
"status": 200,
"body": {
"success": True,
"message": "Payment verified and settled!",
"payer": result.payer_address,
"network": result.network,
"transaction_hash": result.transaction_hash,
"amount_paid": str(result.amount),
}
}
except PaymentRequiredError as e:
# Payment verification failed
return {
"status": 402,
"body": {"error": str(e)}
}
# The 402 response includes payment options for all configured networks:
# {
# "x402Version": 1,
# "accepts": [
# {"network": "base", "asset": "0x833589fCD...", "amount": "1000000", "payTo": "0xYour..."},
# {"network": "solana", "asset": "EPjFWdd5...", "amount": "1000000", "payTo": "Your..."},
# {"network": "near", "asset": "17208628...", "amount": "1000000", "payTo": "your.near"},
# {"network": "stellar", "asset": "CCW67Q...", "amount": "10000000", "payTo": "G..."},
# {"network": "algorand", "asset": "31566704", "amount": "1000000", "payTo": "YOUR..."},
# {"network": "sui", "asset": "0xdba346...", "amount": "1000000", "payTo": "0xYour..."},
# ],
# "payTo": "0xYour...",
# "maxAmountRequired": "1000000",
# "resource": "/api/premium",
# "description": "Premium API access"
# }For even simpler integration, use the @require_payment decorator:
from decimal import Decimal
from uvd_x402_sdk import require_payment, configure_x402
# Configure once at startup
configure_x402(
recipient_address="0xYourEVMWallet...",
recipient_solana="YourSolanaAddress...",
)
@require_payment(amount_usd=Decimal("1.00"))
def premium_endpoint(payment_result):
"""
This function only runs if payment is verified.
Returns 402 automatically if no valid payment.
"""
return {
"data": "Premium content here!",
"paid_by": payment_result.payer_address,
"tx": payment_result.transaction_hash,
}| Network | Type | Chain ID | CAIP-2 | Status |
|---|---|---|---|---|
| Base | EVM | 8453 | eip155:8453 |
Active |
| Ethereum | EVM | 1 | eip155:1 |
Active |
| Polygon | EVM | 137 | eip155:137 |
Active |
| Arbitrum | EVM | 42161 | eip155:42161 |
Active |
| Optimism | EVM | 10 | eip155:10 |
Active |
| Avalanche | EVM | 43114 | eip155:43114 |
Active |
| Celo | EVM | 42220 | eip155:42220 |
Active |
| HyperEVM | EVM | 999 | eip155:999 |
Active |
| Unichain | EVM | 130 | eip155:130 |
Active |
| Monad | EVM | 143 | eip155:143 |
Active |
| Scroll | EVM | 534352 | eip155:534352 |
Active |
| SKALE | EVM | 1187947933 | eip155:1187947933 |
Active |
| SKALE Testnet | EVM | 324705682 | eip155:324705682 |
Active |
| Robinhood | EVM | 4663 | eip155:4663 |
Active |
| Robinhood Testnet | EVM | 46630 | eip155:46630 |
Active |
| Solana | SVM | - | solana:5eykt... |
Active |
| Fogo | SVM | - | solana:fogo |
Active |
| NEAR | NEAR | - | near:mainnet |
Active |
| Stellar | Stellar | - | stellar:pubnet |
Active |
| Algorand | Algorand | - | algorand:mainnet |
Active |
| Algorand Testnet | Algorand | - | algorand:testnet |
Active |
| Sui | Sui | - | sui:mainnet |
Active |
| Sui Testnet | Sui | - | sui:testnet |
Active |
| XRP Ledger | XRPL | - | (no CAIP-2) xrpl-mainnet |
Active |
| XRP Ledger Testnet | XRPL | - | (no CAIP-2) xrpl-testnet |
Active |
| Token | Networks | Decimals |
|---|---|---|
| USDC | All networks except Robinhood | 6 |
| EURC | Ethereum, Base, Avalanche | 6 |
| AUSD | Ethereum, Arbitrum, Avalanche, Polygon, Monad, Sui | 6 |
| PYUSD | Ethereum | 6 |
| USDT | Ethereum, Arbitrum, Optimism, Avalanche, Polygon | 6 |
| USDG | Robinhood, Robinhood Testnet | 6 |
Robinhood Chain settles in Paxos USDG, not USDC. USDG's on-chain
version()getter reverts, so clients MUST send the EIP-712 domain{"name": "Global Dollar", "version": "1"}inPaymentRequirements.extra. The SDK carries this automatically for therobinhood/robinhood-testnetnetworks.
# Core SDK (minimal dependencies)
pip install uvd-x402-sdk
# With wallet signing support (EIP-3009)
pip install uvd-x402-sdk[wallet] # WalletAdapter + EnvKeyAdapter
pip install uvd-x402-sdk[signer] # Alias for wallet (backward compat)
# With framework support
pip install uvd-x402-sdk[flask] # Flask integration
pip install uvd-x402-sdk[fastapi] # FastAPI/Starlette integration
pip install uvd-x402-sdk[django] # Django integration
pip install uvd-x402-sdk[aws] # AWS Lambda helpers
pip install uvd-x402-sdk[algorand] # Algorand atomic group helpers
# All integrations
pip install uvd-x402-sdk[all]from decimal import Decimal
from flask import Flask, g, jsonify
from uvd_x402_sdk.integrations import FlaskX402
app = Flask(__name__)
x402 = FlaskX402(
app,
recipient_address="0xYourEVMWallet...",
recipient_solana="YourSolanaAddress...",
recipient_near="your-account.near",
recipient_stellar="G...YourStellarAddress",
)
@app.route("/api/premium")
@x402.require_payment(amount_usd=Decimal("5.00"))
def premium():
return jsonify({
"message": "Premium content!",
"payer": g.payment_result.payer_address,
"tx": g.payment_result.transaction_hash,
"network": g.payment_result.network,
})
@app.route("/api/basic")
@x402.require_payment(amount_usd=Decimal("0.10"))
def basic():
return jsonify({"data": "Basic tier data"})
if __name__ == "__main__":
app.run(debug=True)from decimal import Decimal
from fastapi import FastAPI, Depends
from uvd_x402_sdk.config import X402Config
from uvd_x402_sdk.models import PaymentResult
from uvd_x402_sdk.integrations import FastAPIX402
app = FastAPI()
x402 = FastAPIX402(
app,
recipient_address="0xYourEVMWallet...",
recipient_solana="YourSolanaAddress...",
recipient_near="your-account.near",
recipient_stellar="G...YourStellarAddress",
)
@app.get("/api/premium")
async def premium(
payment: PaymentResult = Depends(x402.require_payment(amount_usd="5.00"))
):
return {
"message": "Premium content!",
"payer": payment.payer_address,
"network": payment.network,
}
@app.post("/api/generate")
async def generate(
body: dict,
payment: PaymentResult = Depends(x402.require_payment(amount_usd="1.00"))
):
# Dynamic processing based on request
return {"result": "generated", "payer": payment.payer_address}# settings.py
X402_FACILITATOR_URL = "https://facilitator.ultravioletadao.xyz"
X402_RECIPIENT_EVM = "0xYourEVMWallet..."
X402_RECIPIENT_SOLANA = "YourSolanaAddress..."
X402_RECIPIENT_NEAR = "your-account.near"
X402_RECIPIENT_STELLAR = "G...YourStellarAddress"
X402_PROTECTED_PATHS = {
"/api/premium/": "5.00",
"/api/basic/": "1.00",
}
MIDDLEWARE = [
# ...other middleware...
"uvd_x402_sdk.integrations.django_integration.DjangoX402Middleware",
]
# views.py
from django.http import JsonResponse
from uvd_x402_sdk.integrations import django_x402_required
@django_x402_required(amount_usd="5.00")
def premium_view(request):
payment = request.payment_result
return JsonResponse({
"message": "Premium content!",
"payer": payment.payer_address,
})import json
from decimal import Decimal
from uvd_x402_sdk.config import X402Config
from uvd_x402_sdk.integrations import LambdaX402
config = X402Config(
recipient_evm="0xYourEVMWallet...",
recipient_solana="YourSolanaAddress...",
recipient_near="your-account.near",
recipient_stellar="G...YourStellarAddress",
)
x402 = LambdaX402(config=config)
def handler(event, context):
# Calculate price based on request
body = json.loads(event.get("body", "{}"))
quantity = body.get("quantity", 1)
price = Decimal(str(quantity * 0.01))
# Process payment or return 402
result = x402.process_or_require(event, price)
# If 402 response, return it
if isinstance(result, dict) and "statusCode" in result:
return result
# Payment verified!
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"success": True,
"payer": result.payer_address,
"tx": result.transaction_hash,
"network": result.network,
"quantity": quantity,
})
}EVM chains use ERC-3009 TransferWithAuthorization with EIP-712 signatures.
from uvd_x402_sdk import X402Client, X402Config
# Accept payments on Base and Ethereum only
config = X402Config(
recipient_evm="0xYourEVMWallet...",
supported_networks=["base", "ethereum"],
)
client = X402Client(config=config)
result = client.process_payment(x_payment_header, Decimal("10.00"))
# The payload contains EIP-712 signature + authorization
payload = client.extract_payload(x_payment_header)
evm_data = payload.get_evm_payload()
print(f"From: {evm_data.authorization.from_address}")
print(f"To: {evm_data.authorization.to}")
print(f"Value: {evm_data.authorization.value}")SVM chains use partially-signed VersionedTransactions with SPL token transfers.
from uvd_x402_sdk import X402Client, X402Config
# Accept payments on Solana and Fogo
config = X402Config(
recipient_solana="YourSolanaAddress...",
supported_networks=["solana", "fogo"],
)
client = X402Client(config=config)
result = client.process_payment(x_payment_header, Decimal("5.00"))
# The payload contains a base64-encoded VersionedTransaction
payload = client.extract_payload(x_payment_header)
svm_data = payload.get_svm_payload()
print(f"Transaction: {svm_data.transaction[:50]}...")
# Fogo has ultra-fast finality (~400ms)
if result.network == "fogo":
print("Payment confirmed in ~400ms!")Stellar uses Soroban Authorization Entries with fee-bump transactions.
from uvd_x402_sdk import X402Client, X402Config
config = X402Config(
recipient_stellar="G...YourStellarAddress",
supported_networks=["stellar"],
)
client = X402Client(config=config)
result = client.process_payment(x_payment_header, Decimal("1.00"))
# Stellar uses 7 decimals (stroops)
payload = client.extract_payload(x_payment_header)
stellar_data = payload.get_stellar_payload()
print(f"From: {stellar_data.from_address}")
print(f"Amount (stroops): {stellar_data.amount}")
print(f"Token Contract: {stellar_data.tokenContract}")NEAR uses NEP-366 meta-transactions with Borsh serialization.
from uvd_x402_sdk import X402Client, X402Config
config = X402Config(
recipient_near="your-recipient.near",
supported_networks=["near"],
)
client = X402Client(config=config)
result = client.process_payment(x_payment_header, Decimal("2.00"))
# NEAR payload contains a SignedDelegateAction
payload = client.extract_payload(x_payment_header)
near_data = payload.get_near_payload()
print(f"SignedDelegateAction: {near_data.signedDelegateAction[:50]}...")
# Validate NEAR payload structure
from uvd_x402_sdk.networks.near import validate_near_payload
validate_near_payload(payload.payload) # Raises ValueError if invalidAlgorand uses atomic groups with ASA (Algorand Standard Assets) transfers.
from uvd_x402_sdk import X402Client, X402Config
config = X402Config(
recipient_algorand="NCDSNUQ2QLXDMJXRALAW4CRUSSKG4IS37MVOFDQQPC45SE4EBZO42U6ZX4",
supported_networks=["algorand"],
)
client = X402Client(config=config)
result = client.process_payment(x_payment_header, Decimal("1.00"))
# Algorand uses atomic groups: [fee_tx, payment_tx]
from uvd_x402_sdk.networks.algorand import (
validate_algorand_payload,
get_algorand_fee_payer,
build_atomic_group,
)
# Get the facilitator fee payer address
fee_payer = get_algorand_fee_payer("algorand")
print(f"Fee payer: {fee_payer}") # KIMS5H6Q...
# Validate payload structure
payload = client.extract_payload(x_payment_header)
validate_algorand_payload(payload.payload) # Raises ValueError if invalidfrom uvd_x402_sdk.networks.algorand import (
build_atomic_group,
build_x402_payment_request,
get_algorand_fee_payer,
)
from algosdk.v2client import algod
# Connect to Algorand node
client = algod.AlgodClient("", "https://mainnet-api.algonode.cloud")
# Build atomic group
payload = build_atomic_group(
sender_address="YOUR_ADDRESS...",
recipient_address="MERCHANT_ADDRESS...",
amount=1000000, # 1 USDC (6 decimals)
asset_id=31566704, # USDC ASA ID on mainnet
facilitator_address=get_algorand_fee_payer("algorand"),
sign_transaction=lambda txn: txn.sign(private_key),
algod_client=client,
)
# Build x402 payment request
request = build_x402_payment_request(payload, network="algorand")Sui uses sponsored transactions with Move-based programmable transaction blocks.
from uvd_x402_sdk import X402Client, X402Config
config = X402Config(
recipient_sui="0xYourSuiAddress...",
supported_networks=["sui"],
)
client = X402Client(config=config)
result = client.process_payment(x_payment_header, Decimal("1.00"))
# Sui payload contains a user-signed PTB that the facilitator sponsors
payload = client.extract_payload(x_payment_header)
sui_data = payload.get_sui_payload()
print(f"From: {sui_data.from_address}")
print(f"To: {sui_data.to}")
print(f"Amount: {sui_data.amount}")
print(f"Coin Object ID: {sui_data.coinObjectId}")
print(f"Transaction Bytes: {sui_data.transactionBytes[:50]}...")
# Sui uses sponsored transactions (user pays ZERO SUI for gas)
# Facilitator adds sponsor signature and pays gas feesfrom uvd_x402_sdk.networks.sui import (
validate_sui_payload,
is_valid_sui_address,
is_valid_sui_coin_type,
get_sui_fee_payer,
get_sui_usdc_coin_type,
get_sui_ausd_coin_type,
SUI_FEE_PAYER_MAINNET,
SUI_USDC_COIN_TYPE_MAINNET,
SUI_AUSD_COIN_TYPE_MAINNET,
)
# Validate Sui addresses (0x + 64 hex chars)
assert is_valid_sui_address("0xe7bbf2b13f7d72714760aa16e024fa1b35a978793f9893d0568a4fbf356a764a")
# Validate coin types (package::module::type format)
assert is_valid_sui_coin_type(SUI_USDC_COIN_TYPE_MAINNET)
# Get fee payer (sponsor) address
fee_payer = get_sui_fee_payer("sui") # Returns mainnet sponsor
print(f"Sui sponsor: {fee_payer}")
# Get USDC coin type
usdc_type = get_sui_usdc_coin_type("sui")
# '0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC'
# Get AUSD coin type (mainnet only)
ausd_type = get_sui_ausd_coin_type("sui")
# '0x2053d08c1e2bd02791056171aab0fd12bd7cd7efad2ab8f6b9c8902f14df2ff2::ausd::AUSD'
# Validate Sui payment payload
payload = client.extract_payload(x_payment_header)
validate_sui_payload(payload.payload) # Raises ValueError if invalidXRPL uses the t54 scheme. The user signs a Payment transaction sending native XRP to the merchant; the facilitator submits it to the ledger and pays the network fee. Payment details (payTo, amount, asset) travel in PaymentRequirements/extra, so the payload carries only the signed transaction blob.
from uvd_x402_sdk import X402Client, X402Config
config = X402Config(
recipient_xrpl="r...YourXRPLAddress",
supported_networks=["xrpl-mainnet"],
)
client = X402Client(config=config)
result = client.process_payment(x_payment_header, Decimal("1.00"))
# XRPL payload carries only the signed Payment transaction blob (hex)
payload = client.extract_payload(x_payment_header)
xrpl_data = payload.get_xrpl_payload()
print(f"Signed tx blob: {xrpl_data.signed_tx_blob[:50]}...")from uvd_x402_sdk.networks.xrpl import (
drops_to_xrp,
xrp_to_drops,
is_valid_xrpl_address,
get_xrpl_fee_payer,
)
from uvd_x402_sdk import XRPL_FEE_PAYER_MAINNET, XRPL_FEE_PAYER_TESTNET
# Native XRP uses 6 decimals (drops): 1 XRP = 1,000,000 drops
assert xrp_to_drops(1.5) == 1_500_000
assert drops_to_xrp(1_000_000) == 1.0
# Validate classic addresses (start with 'r', base58, 25-35 chars)
assert is_valid_xrpl_address("rfADKkVXBNqK3z72tVSS3LVzAR3psYkonp")
# Get the facilitator fee payer (submits the tx + pays the network fee)
fee_payer = get_xrpl_fee_payer("xrpl-mainnet")
print(f"XRPL fee payer: {fee_payer}") # rfADKkVXBNqK3z72tVSS3LVzAR3psYkonpThe SDK supports both x402 protocol versions with automatic detection.
| Aspect | v1 | v2 |
|---|---|---|
| Network ID | String ("base") |
CAIP-2 ("eip155:8453") |
| Payment delivery | JSON body | PAYMENT-REQUIRED header |
| Multiple options | Limited | accepts array |
| Discovery | Implicit | Optional extension |
from uvd_x402_sdk import PaymentPayload
# The SDK auto-detects based on network format
payload = PaymentPayload(
x402Version=1,
scheme="exact",
network="base", # v1 format
payload={"signature": "...", "authorization": {...}}
)
print(payload.is_v2()) # False
payload_v2 = PaymentPayload(
x402Version=2,
scheme="exact",
network="eip155:8453", # v2 CAIP-2 format
payload={"signature": "...", "authorization": {...}}
)
print(payload_v2.is_v2()) # True
# Both work the same way
print(payload.get_normalized_network()) # "base"
print(payload_v2.get_normalized_network()) # "base"from uvd_x402_sdk import X402Config, create_402_response_v2, Payment402BuilderV2
config = X402Config(
recipient_evm="0xYourEVM...",
recipient_solana="YourSolana...",
recipient_near="your.near",
recipient_stellar="G...Stellar",
)
# Simple v2 response
response = create_402_response_v2(
amount_usd=Decimal("5.00"),
config=config,
resource="/api/premium",
description="Premium API access",
)
# Returns:
# {
# "x402Version": 2,
# "scheme": "exact",
# "resource": "/api/premium",
# "accepts": [
# {"network": "eip155:8453", "asset": "0x833...", "amount": "5000000", "payTo": "0xYour..."},
# {"network": "solana:5eykt...", "asset": "EPjF...", "amount": "5000000", "payTo": "Your..."},
# {"network": "near:mainnet", "asset": "1720...", "amount": "5000000", "payTo": "your.near"},
# ...
# ]
# }
# Builder pattern for more control
response = (
Payment402BuilderV2(config)
.amount(Decimal("10.00"))
.resource("/api/generate")
.description("AI generation credits")
.networks(["base", "solana", "near"]) # Limit to specific networks
.build()
)Each network type has specific payload validation:
from uvd_x402_sdk.models import EVMPayloadContent, EVMAuthorization
# Parse and validate EVM payload
payload = client.extract_payload(x_payment_header)
evm_data = payload.get_evm_payload()
# Validate authorization fields
auth = evm_data.authorization
assert auth.from_address.startswith("0x")
assert auth.to.startswith("0x")
assert int(auth.value) > 0
assert int(auth.validBefore) > int(auth.validAfter)from uvd_x402_sdk.networks.solana import validate_svm_payload, is_valid_solana_address
# Validate SVM payload
payload = client.extract_payload(x_payment_header)
validate_svm_payload(payload.payload) # Raises ValueError if invalid
# Validate Solana addresses
assert is_valid_solana_address("YourSolanaAddress...")from uvd_x402_sdk.networks.near import (
validate_near_payload,
is_valid_near_account_id,
BorshSerializer,
)
# Validate NEAR payload
payload = client.extract_payload(x_payment_header)
validate_near_payload(payload.payload) # Raises ValueError if invalid
# Validate NEAR account IDs
assert is_valid_near_account_id("your-account.near")
assert is_valid_near_account_id("0xultravioleta.near")from uvd_x402_sdk.networks.stellar import (
is_valid_stellar_address,
is_valid_contract_address,
stroops_to_usd,
)
# Validate Stellar addresses
assert is_valid_stellar_address("G...YourStellarAddress") # G...
assert is_valid_contract_address("C...USDCContract") # C...
# Convert stroops to USD (7 decimals)
usd = stroops_to_usd(50000000) # Returns 5.0# Core configuration
X402_FACILITATOR_URL=https://facilitator.ultravioletadao.xyz
X402_VERIFY_TIMEOUT=30
X402_SETTLE_TIMEOUT=55
# Recipient addresses (at least one required)
X402_RECIPIENT_EVM=0xYourEVMWallet
X402_RECIPIENT_SOLANA=YourSolanaAddress
X402_RECIPIENT_NEAR=your-account.near
X402_RECIPIENT_STELLAR=G...YourStellarAddress
X402_RECIPIENT_XRPL=r...YourXRPLAddress
# Optional
X402_FACILITATOR_SOLANA=F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq
X402_RESOURCE_URL=https://api.example.com
X402_DESCRIPTION=API access paymentfrom uvd_x402_sdk import X402Config, MultiPaymentConfig
# Full configuration
config = X402Config(
facilitator_url="https://facilitator.ultravioletadao.xyz",
# Recipients
recipient_evm="0xYourEVMWallet",
recipient_solana="YourSolanaAddress",
recipient_near="your-account.near",
recipient_stellar="G...YourStellarAddress",
# Timeouts
verify_timeout=30.0,
settle_timeout=55.0,
# Limit to specific networks
supported_networks=["base", "solana", "near", "stellar"],
# Metadata
resource_url="https://api.example.com/premium",
description="Premium API access",
# Protocol version (1, 2, or "auto")
x402_version="auto",
)
# From environment
config = X402Config.from_env()Some deployments cannot settle every chain through the same facilitator — Coinbase's CDP facilitator, for example, does not settle Avalanche. Map the networks that need a different one; everything else is unchanged.
from uvd_x402_sdk import X402Client
client = X402Client(
recipient_address="0xYourEVMWallet",
supported_networks=["base", "avalanche"],
facilitator_by_network={
"base": "https://api.cdp.coinbase.com/platform/v2/x402",
"avalanche": "https://facilitator.ultravioletadao.xyz",
},
)
client.facilitator_url_for("base") # -> the CDP URL
client.facilitator_url_for("eip155:43114") # -> the UVD URL (CAIP-2 works too)Three rules make this safe to run in production:
- Not configured = nothing changes. Without
facilitator_by_networkevery network resolves tofacilitator_url, exactly as before. - It never guesses. A network that is not in the table raises
ConfigurationErrorinstead of falling back tofacilitator_url— silently settling on a facilitator that does not support that chain is a money bug. Declare a fallback explicitly with the reserved"*"key:{"base": CDP, "*": UVD}. - It fails at boot, not at the first payment. An enabled network with no
route raises in the constructor. Either route every network in
supported_networks, narrow that list, or add"*". Passverify_facilitator_support=Trueto also probe each facilitator'sGET /supportedat construction and refuse a route the facilitator itself does not advertise (available on demand asclient.verify_routes()).
From the environment, as a JSON object:
X402_FACILITATOR_BY_NETWORK='{"base":"https://api.cdp.coinbase.com/platform/v2/x402","avalanche":"https://facilitator.ultravioletadao.xyz"}'/verify, /settle (including the post-timeout re-check, which always asks the
same facilitator the settle went to) and /accepts follow the table. The
endpoints that are not network-scoped — /version, /blacklist, /api/stats,
/transactions — use facilitator_url; get_supported() and health_check()
accept an optional network= to target one facilitator.
The SDK includes all facilitator addresses as embedded constants. You don't need to configure them manually.
Non-EVM chains require a fee payer address for gasless transactions:
from uvd_x402_sdk import (
# Algorand
ALGORAND_FEE_PAYER_MAINNET, # KIMS5H6QLCUDL65L5UBTOXDPWLMTS7N3AAC3I6B2NCONEI5QIVK7LH2C2I
ALGORAND_FEE_PAYER_TESTNET, # 5DPPDQNYUPCTXRZWRYSF3WPYU6RKAUR25F3YG4EKXQRHV5AUAI62H5GXL4
# Solana
SOLANA_FEE_PAYER_MAINNET, # F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq
SOLANA_FEE_PAYER_DEVNET, # 6xNPewUdKRbEZDReQdpyfNUdgNg8QRc8Mt263T5GZSRv
# Fogo
FOGO_FEE_PAYER_MAINNET, # F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq
FOGO_FEE_PAYER_TESTNET, # 6xNPewUdKRbEZDReQdpyfNUdgNg8QRc8Mt263T5GZSRv
# NEAR
NEAR_FEE_PAYER_MAINNET, # uvd-facilitator.near
NEAR_FEE_PAYER_TESTNET, # uvd-facilitator.testnet
# Stellar
STELLAR_FEE_PAYER_MAINNET, # GCHPGXJT2WFFRFCA5TV4G4E3PMMXLNIDUH27PKDYA4QJ2XGYZWGFZNHB
STELLAR_FEE_PAYER_TESTNET, # GBBFZMLUJEZVI32EN4XA2KPP445XIBTMTRBLYWFIL556RDTHS2OWFQ2Z
# Sui
SUI_FEE_PAYER_MAINNET, # 0xe7bbf2b13f7d72714760aa16e024fa1b35a978793f9893d0568a4fbf356a764a
SUI_FEE_PAYER_TESTNET, # 0xabbd16a2fab2a502c9cfe835195a6fc7d70bfc27cffb40b8b286b52a97006e67
# XRPL
XRPL_FEE_PAYER_MAINNET, # rfADKkVXBNqK3z72tVSS3LVzAR3psYkonp
XRPL_FEE_PAYER_TESTNET, # rGhTioKAFHe75KgVnQtacRiKFuPv28Wbwk
# Helper function
get_fee_payer, # Get fee payer for any network
)
# Get fee payer for any network
fee_payer = get_fee_payer("algorand") # Returns KIMS5H6Q...
fee_payer = get_fee_payer("solana") # Returns F742C4VfF...
fee_payer = get_fee_payer("sui") # Returns 0xe7bbf2b...
fee_payer = get_fee_payer("base") # Returns None (EVM doesn't need fee payer)EVM chains use EIP-3009 transferWithAuthorization (gasless by design), but the facilitator wallet addresses are available for reference:
from uvd_x402_sdk import (
EVM_FACILITATOR_MAINNET, # 0x103040545AC5031A11E8C03dd11324C7333a13C7
EVM_FACILITATOR_TESTNET, # 0x34033041a5944B8F10f8E4D8496Bfb84f1A293A8
)from uvd_x402_sdk import (
get_fee_payer, # Get fee payer address for a network
requires_fee_payer, # Check if network needs fee payer
get_all_fee_payers, # Get all registered fee payers
build_payment_info, # Build payment info with auto feePayer
DEFAULT_FACILITATOR_URL, # https://facilitator.ultravioletadao.xyz
)
# Check if network needs fee payer
requires_fee_payer("algorand") # True
requires_fee_payer("base") # False
# Build payment info with automatic fee payer
info = build_payment_info(
network="algorand",
pay_to="MERCHANT_ADDRESS...",
max_amount_required="1000000",
description="API access"
)
# info = {
# 'network': 'algorand',
# 'payTo': 'MERCHANT_ADDRESS...',
# 'maxAmountRequired': '1000000',
# 'description': 'API access',
# 'asset': '31566704',
# 'extra': {
# 'token': 'usdc',
# 'feePayer': 'KIMS5H6QLCUDL65L5UBTOXDPWLMTS7N3AAC3I6B2NCONEI5QIVK7LH2C2I'
# }
# }from uvd_x402_sdk.networks import NetworkConfig, NetworkType, register_network
# Register a custom EVM network
custom_chain = NetworkConfig(
name="mychain",
display_name="My Custom Chain",
network_type=NetworkType.EVM,
chain_id=12345,
usdc_address="0xUSDCContractAddress",
usdc_decimals=6,
usdc_domain_name="USD Coin", # Check actual EIP-712 domain!
usdc_domain_version="2",
rpc_url="https://rpc.mychain.com",
enabled=True,
)
register_network(custom_chain)
# Now you can use it
config = X402Config(
recipient_evm="0xYourWallet...",
supported_networks=["base", "mychain"],
)The SDK supports 6 stablecoins on EVM chains. Use the token helper functions to query and work with different tokens.
from uvd_x402_sdk import (
TokenType,
get_token_config,
get_supported_tokens,
is_token_supported,
get_networks_by_token,
)
# Check which tokens a network supports
tokens = get_supported_tokens("ethereum")
print(tokens) # ['usdc', 'eurc', 'ausd', 'pyusd']
tokens = get_supported_tokens("base")
print(tokens) # ['usdc', 'eurc']
# Check if a specific token is supported
if is_token_supported("ethereum", "eurc"):
print("EURC is available on Ethereum!")
# Get token configuration
config = get_token_config("ethereum", "eurc")
if config:
print(f"EURC address: {config.address}")
print(f"Decimals: {config.decimals}")
print(f"EIP-712 name: {config.name}")
print(f"EIP-712 version: {config.version}")
# Find all networks that support a token
networks = get_networks_by_token("eurc")
for network in networks:
print(f"EURC available on: {network.display_name}")
# Output: EURC available on: Ethereum, Base, Avalanche C-ChainEach token has specific EIP-712 domain parameters required for signing:
from uvd_x402_sdk import TokenConfig, get_token_config
# TokenConfig structure
# - address: Contract address
# - decimals: Token decimals (6 for all supported stablecoins)
# - name: EIP-712 domain name (e.g., "USD Coin", "EURC", "Gho Token")
# - version: EIP-712 domain version
# Example: Get EURC config on Base
eurc = get_token_config("base", "eurc")
# TokenConfig(
# address="0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42",
# decimals=6,
# name="EURC",
# version="2"
# )
# Example: Get PYUSD config on Ethereum
pyusd = get_token_config("ethereum", "pyusd")
# TokenConfig(
# address="0x6c3ea9036406852006290770BEdFcAbA0e23A0e8",
# decimals=6,
# name="PayPal USD",
# version="1"
# )| Token | Description | Decimals | Issuer |
|---|---|---|---|
usdc |
USD Coin | 6 | Circle |
eurc |
Euro Coin | 6 | Circle |
ausd |
Agora USD | 6 | Agora Finance |
pyusd |
PayPal USD | 6 | PayPal/Paxos |
The same token may use different EIP-712 domain names on different chains. This affects signature verification.
| Token | Ethereum | Base | Avalanche |
|---|---|---|---|
| EURC | "Euro Coin" |
"EURC" |
"Euro Coin" |
| USDC | "USD Coin" |
"USD Coin" |
"USD Coin" |
| AUSD | "Agora Dollar" |
N/A | "Agora Dollar" |
| PYUSD | "PayPal USD" |
N/A | N/A |
Important: Always use get_token_config() to get the correct domain name. Never hardcode domain names.
# CORRECT: Use get_token_config for each chain
eurc_base = get_token_config("base", "eurc")
# TokenConfig(name="EURC", version="2", ...)
eurc_ethereum = get_token_config("ethereum", "eurc")
# TokenConfig(name="Euro Coin", version="2", ...)PYUSD uses the Paxos implementation which only supports the v,r,s signature variant of transferWithAuthorization. This is different from Circle's USDC/EURC which support both compact bytes and v,r,s variants.
Backend implications:
- The x402 facilitator (v1.9.0+) automatically handles this by detecting PYUSD and using
transferWithAuthorization_1(v,r,s)instead oftransferWithAuthorization_0(bytes signature) - If using a custom facilitator, ensure it supports the v,r,s variant for PYUSD
When using non-USDC tokens, your backend must pass the token info (including EIP-712 domain) to the facilitator. This is done via the extra field in paymentRequirements:
# When building payment requirements for the facilitator
payment_requirements = {
"asset": token_address, # Use actual token address, NOT hardcoded USDC
"extra": {
"name": token_config.name, # EIP-712 domain name
"version": token_config.version, # EIP-712 domain version
}
}Without this, the facilitator will use wrong EIP-712 domain and signature verification will fail with "invalid signature" error.
settle_payment(), verify_payment() and process_payment() accept asset and
eip712_domain overrides. By default they send the network's USDC address and the SDK
registry's domain — the overrides let YOUR token registry decide instead (required when
the token is not in the SDK registry, or when registries drift on domain names):
result = client.process_payment(
header,
Decimal("0.10"),
asset="0x01bFF41798a0BcF287b996046Ca68b395DbC1071", # the token you actually settle
eip712_domain={"name": "USD₮0", "version": "1"}, # the domain the token contract uses
)create_authorization() accepts the same eip712_domain override — there it changes the
signed digest (and the non-USDC token.eip712 block), so the signature verifies
against the domain the verifier resolves. A partial domain (missing name or version)
raises ValueError before anything is signed or sent.
response = client.settle_payment(payload, Decimal("0.10"), retry=True)Default False — a single attempt, exactly as before. With retry=True: up to 3 attempts
with exponential backoff (1s, 2s), retrying only transient transport errors and 5xx.
Never retried:
- 4xx — deterministic (bad request, auth, idempotency conflict); retrying amplifies it
success=falseinside a 200 — a business error, not a transient one- A 5xx whose body already carries a transaction hash — the facilitator broadcast the tx (e.g. a non-fatal post-settle hook failed); retrying would settle TWICE
result = client.try_settle_payment(payload, Decimal("0.10"), retry=True)
# {"success": True, "tx_hash": "0x...", "error": None}Same arguments as settle_payment(), but payment-flow errors come back as data instead of
exceptions. success=False with tx_hash set is the double-settle warning shape: the
facilitator returned an error status AFTER broadcasting — verify on-chain, do not re-send.
from uvd_x402_sdk.exceptions import (
X402Error,
PaymentRequiredError,
PaymentVerificationError,
PaymentSettlementError,
UnsupportedNetworkError,
InvalidPayloadError,
FacilitatorError,
X402TimeoutError,
)
try:
result = client.process_payment(header, amount)
except PaymentVerificationError as e:
# Signature invalid, amount mismatch, expired, etc.
print(f"Verification failed: {e.reason}")
print(f"Errors: {e.errors}")
except PaymentSettlementError as e:
# On-chain settlement failed (insufficient balance, nonce used, etc.)
print(f"Settlement failed on {e.network}: {e.message}")
except UnsupportedNetworkError as e:
# Network not recognized or disabled
print(f"Network {e.network} not supported")
print(f"Supported: {e.supported_networks}")
except InvalidPayloadError as e:
# Malformed X-PAYMENT header
print(f"Invalid payload: {e.message}")
except FacilitatorError as e:
# Facilitator returned error
print(f"Facilitator error: {e.status_code} - {e.response_body}")
except X402TimeoutError as e:
# Request timed out
print(f"{e.operation} timed out after {e.timeout_seconds}s")
except X402Error as e:
# Catch-all for x402 errors
print(f"Payment error: {e.message}")Build verifiable on-chain reputation for AI agents and services. Supports 20 networks (18 EVM + Solana + Solana devnet).
On EVM networks, agent IDs are sequential uint256 integers. On Solana, agent IDs are base58 pubkey strings (NFT asset addresses). The AgentId type (Union[int, str]) handles both.
from uvd_x402_sdk import Erc8004Client, AgentId
async with Erc8004Client() as client:
# EVM: agent_id is an integer
identity = await client.get_identity("ethereum", 42)
print(f"Agent URI: {identity.agent_uri}")
# Solana: agent_id is a base58 pubkey string
identity = await client.get_identity("solana", "8oo4dC4JvBLwy5...")
print(f"Agent URI: {identity.agent_uri}")
# Get agent reputation
reputation = await client.get_reputation("ethereum", 42)
print(f"Score: {reputation.summary.summary_value}")
# Submit feedback after payment
result = await client.submit_feedback(
network="ethereum",
agent_id=42,
value=95,
tag1="quality",
proof=settle_response.proof_of_payment,
)
# Respond to feedback (agents only)
# seal_hash is required for Solana, optional for EVM
await client.append_response(
network="ethereum",
agent_id=42,
feedback_index=1,
response_text="Thank you for your feedback!",
)Create signed EIP-3009 payment headers from your backend without a browser wallet. Useful for server-to-server x402 payments, automated agents, and testing.
pip install uvd-x402-sdk[signer]from decimal import Decimal
from uvd_x402_sdk import X402Client
client = X402Client(recipient_address="0xMerchant...")
# Connect with a private key (reads from env or direct param)
address = client.connect_with_private_key(
private_key="YOUR_PRIVATE_KEY", # or use os.environ
chain_name="base", # EVM chain to sign for
)
print(f"Connected: {address}")
# Create a signed X-PAYMENT header
x_payment = client.create_authorization(
pay_to="0xMerchant...",
amount_usd=Decimal("1.00"),
chain_name="base",
token_type="usdc",
valid_duration=3600, # 1 hour
)
# Use the header in requests
import requests
response = requests.get(
"https://api.example.com/premium",
headers={"X-PAYMENT": x_payment},
)Note: EVM-only. The SDK validates the chain is EVM type before signing.
Discover what the facilitator can settle before constructing payment authorizations. Used by Faremeter middleware and clients.
from uvd_x402_sdk import X402Client
client = X402Client(recipient_address="0xMerchant...")
# Ask facilitator what it can settle
enriched = client.negotiate_accepts([
{
"scheme": "exact",
"network": "base-mainnet",
"maxAmountRequired": "1000000",
"resource": "https://api.example.com/data",
"payTo": "0xMerchant...",
}
])
# enriched[0]["extra"] now has feePayer, tokens, escrow configHold payments in escrow with dispute resolution.
from uvd_x402_sdk import EscrowClient
async with EscrowClient() as client:
# Create escrow payment
escrow = await client.create_escrow(
payment_header=request.headers["X-PAYMENT"],
requirements=payment_requirements,
escrow_duration=86400, # 24 hours
)
# Release after service delivery
await client.release(escrow.id)
# Or request refund if service failed
await client.request_refund(
escrow_id=escrow.id,
reason="Service not delivered",
)Register and discover paid x402 resources across the network.
from uvd_x402_sdk import BazaarClient
async with BazaarClient() as bazaar:
# List available resources
resources = await bazaar.list_resources(category="finance", network="base-mainnet")
for r in resources.items:
print(f"{r.url} - {r.description}")
# Only the ones a probe actually reached, best-curated first
for r in (await bazaar.list_resources(limit=50, health="alive", tier="vip")).items:
print(r.url, r.health.status, r.health.latency_ms, r.curation.label)
# Free-text search. This runs server-side over the whole catalog, so
# `pagination.total` is the real number of matches -- filtering one page
# locally is not the same thing and will under-report.
hits = await bazaar.list_resources(q="logs")
print(hits.pagination.total)
# Register your own resource
await bazaar.register_resource(
url="https://api.example.com/data",
resource_type="http",
description="Premium data API",
accepts=[{
"scheme": "exact",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"payTo": "0xYourWallet...",
}],
metadata={"category": "finance", "tags": ["market-data"]},
)Abstract wallet interface for signing EIP-3009 authorizations. Use EnvKeyAdapter for raw private keys or OWSWalletAdapter for Open Wallet Standard (future).
pip install uvd-x402-sdk[wallet]from uvd_x402_sdk import EnvKeyAdapter
# Reads WALLET_PRIVATE_KEY or PRIVATE_KEY from env
wallet = EnvKeyAdapter()
print(wallet.get_address()) # 0x...
# Sign an EIP-3009 ReceiveWithAuthorization
auth = wallet.sign_eip3009({
"to": "0xRecipient...",
"amount_usdc": 0.10,
"network": "base",
})
print(auth["signature"]) # Use as X-PAYMENT header
# Sign arbitrary EIP-712 typed data
result = wallet.sign_typed_data({
"domain": {"name": "MyDapp", "version": "1", "chainId": 8453},
"types": {"Message": [{"name": "content", "type": "string"}]},
"message": {"content": "Hello"},
})
# Sign a personal message (EIP-191)
sig = wallet.sign_message("Hello, world!")Any class implementing the WalletAdapter protocol can be used:
from uvd_x402_sdk import WalletAdapter
class MyWallet:
def get_address(self) -> str: ...
def sign_message(self, message: str) -> str: ...
def sign_typed_data(self, typed_data: dict) -> "SignedTypedData": ...
def sign_eip3009(self, params: "EIP3009Params") -> "EIP3009Authorization": ...
assert isinstance(MyWallet(), WalletAdapter) # True (runtime_checkable)Sign HTTP requests per ERC-8128 (Signed HTTP
Requests with Ethereum, RFC 9421) with any WalletAdapter. This is how agents
authenticate against wallet-signed APIs — most notably Execution Market, where
API keys are rejected in production and only wallet signing is accepted. The
private key never leaves the adapter; only get_address() and
sign_message() (EIP-191 personal_sign) are used.
from uvd_x402_sdk import EnvKeyAdapter, fetch_nonce, sign_request
wallet = EnvKeyAdapter() # reads WALLET_PRIVATE_KEY from env
# 1. Fresh single-use nonce (5-minute TTL) — one per signed request,
# including retries: the server consumes it before verification.
nonce = await fetch_nonce("https://api.execution.market")
# 2. Sign the request. Body must be byte-identical to what goes on the wire.
headers = sign_request(
wallet,
method="POST",
url="https://api.execution.market/api/v1/tasks",
body='{"title": "test"}',
nonce=nonce,
)
# {"Signature": "eth=:...:", "Signature-Input": "eth=(...)", "Content-Digest": "sha-256=:...:"}
# 3. Merge into the request headers and send.The wire format is pinned and byte-tested against golden vectors
(tests/fixtures/erc8128.json): alg="eip191" always emitted, keyid always
lowercase (erc8128:{chain_id}:{address}), signature params in the order
created;expires;nonce;keyid;alg. Covered components: @method,
@authority, @path, plus @query when the URL has a query string and
content-digest (SHA-256, RFC 9530) when the request has a body.
Sign the escrow lock authorization for sign-on-assignment marketplaces
(Execution Market ADR-002) with any WalletAdapter. The EIP-3009 nonce is
AuthCaptureEscrow.getHash(paymentInfo) which includes the receiver — the
signature commits to the chosen worker, so it can only be created AT
ASSIGNMENT. Returns the raw JSON X-Payment-Auth header value the backend
relays verbatim to the Facilitator POST /settle.
import httpx
from uvd_x402_sdk import EnvKeyAdapter, build_escrow_pre_auth
# Full response of GET /api/v1/h2a/payment-config (never hardcode the domain)
config = httpx.get(
"https://api.execution.market/api/v1/h2a/payment-config"
).json()
payment_auth = build_escrow_pre_auth(
payment_config=config,
network="base",
payer="0xPublisher...", # must match the signing wallet
receiver="0xWorker...", # committed by the nonce
amount_usd=0.10,
deadline=task_deadline_epoch, # release window outlasts it
wallet=EnvKeyAdapter(),
)
# Send as the X-Payment-Auth header on the assignment request.Fail-loud: an unknown network or incomplete network config raises
ValueError — a silent domain fallback would sign a mismatched,
wallet-draining authorization. On-chain limits enforced client-side: bounty
<= $100 (AuthCaptureEscrow deposit condition) and the signed maxFeeBps must
cover the operator's 1300 bps static fee. compute_escrow_nonce() is the
standalone, dict-based equivalent of AdvancedEscrowClient._compute_nonce
(no web3 needed — only eth-abi/eth-utils, pulled in by eth-account).
The wrapper is byte-tested against golden vectors
(tests/fixtures/escrow-preauth.json, shared with the Execution Market web,
mobile and plugin-SDK suites).
If the 402 you received advertises CAIP-2 networks (eip155:8453), you are
speaking v2 and must send the v2 envelope. X402Client.verify_payment and
settle_payment emit the v1 one and cannot express v2:
from uvd_x402_sdk import (
AcceptedRequirementsV2, ResourceInfoV2, build_verify_request_v2,
)
body = build_verify_request_v2(
payload={"signature": "0x...", "authorization": {...}},
resource=ResourceInfoV2(
url="https://api.example.com/thing",
description="Thing",
mime_type="application/json",
),
accepted=AcceptedRequirementsV2(
scheme="exact",
network="eip155:8453",
asset="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
amount="100000",
pay_to="0xabc...",
max_timeout_seconds=300,
),
)
httpx.post(f"{facilitator_url}/verify", json=body)Plain dicts work too, which matters because the spec makes you echo the vendor's accept verbatim — you will usually have a dict, not a model.
| v1 envelope | v2 envelope | |
|---|---|---|
| top level | {x402Version, paymentPayload, paymentRequirements} |
{x402Version, paymentPayload, resource, accepted} |
| network | plain name — base |
CAIP-2 — eip155:8453 |
| amount field | maxAmountRequired |
amount |
resource |
URL string | object {url, description, mimeType} |
Do not mix levels. Each version demands its own network format and its own envelope. A v2 payload inside a v1 envelope — or a plain network name inside a v2 request — matches no variant at the facilitator and fails with
data did not match any variant of untagged enum VerifyRequestEnvelope, an error that names no field. If you see it, check the envelope shape first, not the fields inside it.
stats = client.get_stats()
for row in stats["byNetworkAndAsset"]:
# Use the row's OWN decimals. USDC is 6 nearly everywhere and 18 on BSC —
# scaling by a constant 6 overstates BSC volume by 10^12.
print(row["network"], row["settlesOk"], row["volumeAtomic"], row["decimals"])
recent = client.get_transactions(limit=20, network="base")An index, not a ledger. Records are written best-effort after settlement, so an outage loses rows while payments proceed — verify anything that matters against the transaction hash. Counting starts when the operator enabled the store, so earlier operations are unknown, not zero. And unless failure publishing is on, operations that error are not recorded at all, so a 100% success rate means "no failures were recorded".
get_transactionshas no pagination: it returns the newest N (capped at 200), walking back at most 30 days.
The facilitator emits one Server-Sent Event per operation it handles, so you can render or react to live traffic without polling.
from uvd_x402_sdk import TrafficEventStream
with TrafficEventStream() as stream:
for event in stream:
print(event.kind, event.network, event.ok, event.tx)
# Only settlements on the chains you care about. The facilitator has NO
# server-side filter by network, so this runs client-side.
with TrafficEventStream(networks=["base", "polygon"], kinds=["settle"]) as stream:
for event in stream:
print(event.tx, event.timestamp)
# Async, for an event loop
async with TrafficEventStream() as stream:
async for event in stream:
print(event.network)Three properties decide how you should use this:
It is lossy by design. The facilitator will never slow down or fail a payment to keep an observer in sync, so an event you were not connected for is gone. Treat it as a live hint and use the chain as the source of truth — and note that absence of events is not evidence that nothing happened. On a quiet rail the only thing on the wire for minutes is a keepalive.
Failed operations are not published. Only operations that resolved emit an
event, so ok=False means "resolved and came back negative", never "blew up". A
stream that looks healthy is not proof that the rail is.
Admission is bounded. /events is public and unauthenticated, so it sheds
with HTTP 503 + Retry-After at subscriber capacity, and returns 404 when the
operator disabled it. Both raise FacilitatorError with status_code intact.
Match the canonical network slug.
networkis the name/supporteduses, which is not always the alias you may send.skaleis accepted inbound, but events always sayskale-base. Keying on the alias silently drops every event for that chain.
| Field | Notes |
|---|---|
ts |
Unix epoch milliseconds (not seconds); event.timestamp gives a UTC datetime |
kind |
"verify" or "settle" |
network |
Canonical slug, same as /supported |
ok |
Resolved successfully? |
payer / amount / asset |
Omitted in minimal detail mode |
tx |
Present on settle, absent on verify — nothing settled yet |
Query the facilitator for version, supported networks, blacklist, and health.
from uvd_x402_sdk import X402Client
client = X402Client(recipient_address="0xYourWallet...")
# Check version
version = client.get_version()
print(f"Facilitator: v{version['version']}")
# List supported networks
supported = client.get_supported()
for kind in supported["kinds"]:
print(f" {kind['network']} - {kind['scheme']}")
# Check blacklist
bl = client.get_blacklist()
print(f"Blocked addresses: {bl['totalBlocked']}")
# Health check
is_healthy = client.health_check()Query on-chain escrow state without performing settlement.
from uvd_x402_sdk import EscrowClient
async with EscrowClient() as escrow:
state = await escrow.get_escrow_state(
network="base-mainnet",
payer="0xPayer...",
recipient="0xRecipient...",
nonce="0x1234...",
)
print(f"Status: {state['status']}, Balance: {state.get('balance')}")The x402 protocol enables gasless stablecoin payments (USDC, EURC, AUSD, PYUSD):
1. User Request --> Client sends request without payment
2. 402 Response <-- Server returns payment requirements
3. User Signs --> Wallet signs authorization (NO GAS!)
4. Frontend Sends --> X-PAYMENT header with signed payload
5. SDK Verifies --> Validates signature with facilitator
6. SDK Settles --> Facilitator executes on-chain transfer
7. Success <-- Payment confirmed, request processed
The facilitator (https://facilitator.ultravioletadao.xyz) handles all on-chain interactions and pays gas fees on behalf of users.
| Network Type | User Signs | Facilitator Does |
|---|---|---|
| EVM | EIP-712 message | Calls transferWithAuthorization() |
| SVM | Partial transaction | Co-signs + submits transaction |
| NEAR | DelegateAction (Borsh) | Wraps in Action::Delegate |
| Stellar | Auth entry (XDR) | Wraps in fee-bump transaction |
| Algorand | ASA transfer tx | Signs fee tx + submits atomic group |
| Sui | Programmable tx block | Sponsors gas + submits transaction |
| XRPL | Payment tx (signed blob) | Submits tx + pays network fee |
| Exception | Description |
|---|---|
PaymentRequiredError |
No payment header provided |
PaymentVerificationError |
Signature invalid, amount mismatch, expired |
PaymentSettlementError |
On-chain settlement failed |
UnsupportedNetworkError |
Network not recognized or disabled |
InvalidPayloadError |
Malformed X-PAYMENT header |
FacilitatorError |
Facilitator service error |
ConfigurationError |
Invalid SDK configuration |
X402TimeoutError |
Request timed out |
- Users NEVER pay gas or submit transactions directly
- EVM: Users sign EIP-712 structured messages for any supported stablecoin (USDC, EURC, AUSD, PYUSD)
- Solana/Fogo: Users sign partial transactions (facilitator co-signs and submits)
- Stellar: Users sign Soroban authorization entries only
- NEAR: Users sign NEP-366 meta-transactions (DelegateAction)
- Sui: Users sign programmable transaction blocks (facilitator sponsors gas)
- XRPL: Users sign a native-XRP Payment transaction (facilitator submits it and pays the ledger fee)
- The facilitator submits and pays for all on-chain transactions
- All signatures include expiration timestamps (
validBefore) for replay protection - Nonces prevent double-spending of authorizations
- Each token has verified contract addresses and EIP-712 domain parameters
"Unsupported network"
- Check that the network is in
supported_networks - Verify the network is enabled
- For v2, ensure CAIP-2 format is correct
"Payment verification failed"
- Amount mismatch between expected and signed
- Recipient address mismatch
- Authorization expired (
validBeforein the past) - Nonce already used (replay attack protection)
"Settlement timed out"
- Network congestion - increase
settle_timeout - Facilitator under load - retry after delay
"Invalid payload"
- Check base64 encoding of X-PAYMENT header
- Verify JSON structure matches expected format
- Ensure
x402Versionis 1 or 2
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("uvd_x402_sdk").setLevel(logging.DEBUG)# Clone and install
git clone https://github.com/UltravioletaDAO/uvd-x402-sdk-python
cd uvd-x402-sdk-python
pip install -e ".[dev]"
# Run tests
pytest
# Format code
black src tests
ruff check src tests
# Type checking
mypy srcMIT License - see LICENSE file.
- Added: per-network facilitator routing —
X402Config(facilitator_by_network={"base": CDP_URL, "avalanche": UVD_URL})(alsoX402Client(...)andconfigure_x402(...), and theX402_FACILITATOR_BY_NETWORKenv var as a JSON object).verify,settle, the post-timeout settle re-check and/acceptseach go to the facilitator that owns their network - The translation refuses to guess: a network that is neither in the table nor covered by the reserved
"*"fallback key raisesConfigurationError— it is NEVER routed tofacilitator_urlsilently. Settling on a facilitator that does not settle that chain is a money bug, not a config nit - Boot fails early: an ENABLED network left unrouted raises in the
X402Configconstructor, not on the first payment.X402Client(..., verify_facilitator_support=True)additionally probes each facilitator'sGET /supportedat construction and raises if one does not advertise a network routed to it (also available on demand asclient.verify_routes()) - Added
X402Config.facilitator_url_for(network),X402Config.facilitator_routes(),X402Client.facilitator_url_for()/verify_routes(), and optionalnetwork=onget_supported()/health_check()to target a single facilitator - Purely additive — with no
facilitator_by_networkthe resolution returnsfacilitator_urlfor every input, including unknown networks, and the wire is byte-identical.FACILITATOR_URLS(which maps ENVIRONMENT, not network) is untouched - Reported by NomiCheck, who route
basethrough Coinbase's CDP facilitator andavalanchethrough Ultravioleta's — CDP does not settle avalanche — and had to maintain a routing layer of their own on top of the SDK because a singlefacilitator_urlcould not express it
- Added:
assetandeip712_domainoverrides onsettle_payment(),verify_payment()andprocess_payment()— the caller's token registry (not the SDK's) decides which token contract is settled and which EIP-712 domain ({"name", "version"}) goes to the facilitator viaextra. Unblocks non-USDC settles where the token is not in the SDK registry or the registries drift - Added:
eip712_domainoverride oncreate_authorization()— injects the domain into the SIGNED digest (and into the non-USDCtoken.eip712block), so the signature verifies against the domain the verifier resolves. Partial domains raiseValueErrorbefore signing - Added: opt-in settle retry —
settle_payment(..., retry=True)(defaultFalse, single attempt exactly as before). Up to 3 attempts, exponential backoff (1s, 2s), retrying ONLY transient transport errors and 5xx; NEVER a 4xx, a business failure inside a 2xx, or a 5xx whose body already carries a transaction hash (anti-double-settle guard, ported from Execution Market's facilitator retry policy) - Added:
try_settle_payment()— non-raising settle returning{"success", "tx_hash", "error"};success=Falsewithtx_hashset is the double-settle warning shape (the facilitator broadcast the tx despite the error status — verify on-chain, do NOT re-send) - Purely additive — every new parameter defaults to the previous behavior; the default wire request is byte-identical
- Added:
uvd_x402_sdk.escrow_signing— escrow pre-auth builder for sign-on-assignment marketplaces (Execution Market ADR-002):build_escrow_pre_auth()signs theReceiveWithAuthorizationescrow lock and returns the raw JSONX-Payment-Authheader value;compute_escrow_nonce()mirrorsAuthCaptureEscrow.getHash(payer zeroed, receiver INCLUDED — the signature commits to the worker) - Wrapper and nonce pinned by golden vectors (
tests/fixtures/escrow-preauth.json, byte-identical copy of Execution Market's F0-1 shared fixture, also consumed by the EM web/mobile/plugin-SDK suites) - Purely additive — importable on a base install (eth-abi/eth-utils only required at signing time);
EnvKeyAdapter.sign_eip3009andAdvancedEscrowClient._compute_noncebehavior unchanged, their digest/nonce parity is now pinned by tests
- Added:
uvd_x402_sdk.erc8128— ERC-8128 Signed HTTP Requests (RFC 9421) with anyWalletAdapter:sign_request()buildsSignature/Signature-Input/Content-Digestheaders,fetch_nonce()gets the single-use server nonce - Wire format pinned by golden vectors (
tests/fixtures/erc8128.json, byte-identical copy of Execution Market's F3-1 conformance fixture):alg="eip191"emitted, keyid always lowercase, params in the ordercreated;expires;nonce;keyid;alg - Purely additive — importable on a base install (no
eth-accountrequired until you instantiate an adapter)
- Fixed:
BazaarClient.list_resources()raisedValidationErroragainst the live registry.firstSeen/lastSeenare epoch integers on the wire but the model declared themstr, so every call failed regardless oflimit. Timestamps are now epochintand coerce from int, float, numeric string, ISO-8601 string ordatetime - Added
first_seen_at/last_seen_at/last_updated_athelpers returning timezone-aware UTCdatetime - Added
lastUpdated, which the registry has always sent and the model dropped - Added:
health(DiscoveryHealth:status,last_checked,http_status,latency_ms) andcuration(DiscoveryCuration:tier,label). These are the fields that make the registry usable -- without them you cannot sort by reachable or by vetted -- andmodel_dump()was discarding them - Added
DiscoveryResource.is_aliveand.tiershortcuts list_resources()now exposes every server-side filter:provider,tag,source,source_facilitator,health,tierand free-textq.qis validated againstMAX_SEARCH_LEN(128) andhealth/tieragainstHEALTH_FILTERS/TIER_FILTERSbefore the request goes out- Discovery models now keep unmodelled server fields instead of silently dropping them
paginationis a typedDiscoveryPaginationand still supportspagination["total"]- Added
tests/test_discovery.py, a contract test pinned to a verbatim live registry page
Reported by KarmaCadabra against uvd-x402-sdk 0.26.0.
- Added Robinhood Chain support:
robinhood(chain ID 4663) androbinhood-testnet(chain ID 46630), Arbitrum Orbit L2s with ETH gas - Settlement stablecoin is Paxos USDG (Global Dollar), NOT USDC -- there is no USDC on Robinhood Chain
- New
usdgtoken type; USDG uses 6 decimals and EIP-712 domain{name: "Global Dollar", version: "1"} - USDG's on-chain
version()reverts, so the SDK always carries the domain viaextra(never resolved on-chain) - Added
default_tokenfield toNetworkConfigso a network can declare a primary settlement asset other than USDC - CAIP-2 mappings:
eip155:4663(mainnet) andeip155:46630(testnet) - Now supports 25 blockchains across 7 network families
- Added XRP Ledger support:
xrpl-mainnetandxrpl-testnet(native XRP, 6 decimals/drops) - New
NetworkType.XRPLandnetworks/xrpl.py(drops/XRP helpers, address validation) - XRPL settles via pre-signed Payment transaction blobs; the facilitator submits and pays the fee
- Added
recipient_xrpltoX402Configand theX402_RECIPIENT_XRPLenv var - Exported
XRPL_FEE_PAYER_MAINNET/XRPL_FEE_PAYER_TESTNET - XRPL has no CAIP-2 form; only the v1 network strings are recognized
- Now supports 23 blockchains across 7 network families
- Commerce Scheme:
PaymentPayload,PaymentRequirements, andPaymentRequirementsV2now acceptscheme: "exact" | "escrow" | "commerce"- Aligns with facilitator v1.43.0 which serves
"commerce"as alias for"escrow"(Execution Market / arbiter integrations) - Default scheme remains
"exact"-- no breaking changes - Updated scheme validator with clear error messages
- Aligns with facilitator v1.43.0 which serves
- AdvancedEscrowClient WalletAdapter:
AdvancedEscrowClientnow acceptsWalletAdapterprotocol for signing- Enables Open Wallet Standard (OWS) integration for escrow operations
- Backward compatible: still supports direct private key usage
- WalletAdapter Protocol: Abstract wallet interface for signing operations (
wallet.py)WalletAdapter- runtime-checkable Protocol for any wallet backendEnvKeyAdapter- raw private key from env var or direct paramOWSWalletAdapter- stub for Open Wallet Standard (not yet on PyPI)EIP3009Params,EIP3009Authorization,SignedTypedDataTypedDict types- Auto-detects USDC contract addresses and EIP-712 domain names per network
- Uses proven
encode_typed_data()+sign_message()signing pattern
- New
[wallet]install extra:pip install uvd-x402-sdk[wallet](eth-account>=0.11.0) - eth-account bumped to
>=0.11.0across all extras (signer,wallet,web3)
- ERC-8004: Added
get_identity_by_owner()method for looking up agent identity by owner address - ERC-8004: Added documentation for register idempotency behavior
- ERC-8004: Added SKALE to ERC-8004 networks (20 networks total: 18 EVM + Solana + Solana-devnet)
- Escrow: Added
OPERATOR_ABI_V2withbytes dataparameter for CREATE3 chains - Escrow: Updated SKALE default operator to EM-deployed address
- Server-Side Signing: New
X402Client.connect_with_private_key()for backend EIP-3009 signing- Creates an EVM signer from a private key without browser wallet
- Requires
pip install uvd-x402-sdk[signer](onlyeth-account, not fullweb3)
- EVM-only guard:
create_authorization()now validates chain is EVM type before signing
- SKALE Base Network: Added
skale-base(mainnet, chainId 1187947933) andskale-base-sepolia(testnet, chainId 324705682)- Gasless transactions (CREDIT gas token), legacy tx only (no EIP-1559)
- EIP-712 domain name:
"Bridged USDC (SKALE Bridge)"
- SKALE Base Escrow: Added escrow support via CREATE3 contracts
- Bazaar Discovery: New
BazaarClientfor resource registration and discoverylist_resources()with pagination, category, and network filteringregister_resource()for publishing paid resources to the BazaarDiscoveryResourceandDiscoveryResponsePydantic models
- Facilitator Info Endpoints: New methods on
X402Clientget_version()- query facilitator version (GET /version)get_supported()- list supported networks/schemes (GET /supported)get_blacklist()- check blocked/sanctioned addresses (GET /blacklist)health_check()- check facilitator availability (GET /health)
- Escrow State Queries: New
get_escrow_state()method onEscrowClient- Query on-chain escrow state via
POST /escrow/state - Read status, balance, timestamps without settlement
- Query on-chain escrow state via
- Bug Fix: Fixed
negotiate_accepts()referencing undefinedself.facilitator_urlandself.timeout- Now correctly uses
self.config.facilitator_urland the HTTP client pool
- Now correctly uses
- ERC-8004 Solana Support: Full integration with QuantuLabs 8004-solana Anchor program + ATOM Engine
AgentIdtype alias (Union[int, str]) for dual EVM/Solana agent IDs- Solana and Solana-devnet added to
Erc8004Network(18 networks total) - Solana program ID constants (
agent_registry_program,atom_engine_program) - All ERC-8004 methods now accept
AgentId(int for EVM, string for Solana) seal_hashparameter added torevoke_feedback()andappend_response()(SEAL v1 support)
/acceptsNegotiation: Newnegotiate_accepts()method onX402Client- Sends merchant payment requirements to facilitator POST
/accepts - Returns enriched requirements with feePayer, tokens, escrow config
- Faremeter middleware compatibility
- Sends merchant payment requirements to facilitator POST
- Solana Smart Wallet Support: Transparent CPI inner instruction scanning on server side
- Per-Network Settle Timeout: Different timeout per chain (L1 vs L2)
- On-Chain Fallback: Post-timeout on-chain state check before returning failure
- Escrow: Updated Ethereum mainnet contract addresses from Ali's redeploy
- Escrow: Fixed Optimism
token_collectorandprotocol_fee_configaddresses (corrected to CREATE2)
- Escrow: Dynamic EIP-712 domain resolution per-chain (was hardcoded "USD Coin"/"2")
- Escrow: Added Optimism (chain_id=10) to
ESCROW_CONTRACTS - Fix: HexBytes compatibility with web3 >= 6.x and eth_account >= 0.10.0
- ERC-8004 Trustless Agents: Full client for on-chain reputation system
Erc8004Clientclass with identity, reputation, and feedback methodsappend_response()method for agents to respond to feedbackProofOfPaymentmodel for reputation submission authorizationbuild_erc8004_payment_requirements()helper
- Escrow & Refund Support: Complete escrow payment flow
EscrowClientclass with create, release, refund, dispute methodsEscrowPayment,RefundRequest,Disputemodels- Helper functions:
can_release_escrow(),can_refund_escrow(), etc.
- New Networks: Scroll (534352) and SKALE (1187947933, testnet: 324705682)
- SKALE is gasless L3 with sFUEL
- Scroll is zkEVM Layer 2
- SDK now supports 21 blockchain networks
- Added
SuiPayloadContentPydantic model for Sui sponsored transactions - Added
coinObjectIdas required field (CRITICAL for facilitator deserialization) - Added
get_sui_payload()method toPaymentPayload - Updated
validate_sui_payload()to requirecoinObjectId
- Added AUSD (Agora USD) support for Sui mainnet
- Added
SUI_AUSD_COIN_TYPE_MAINNETconstant - Added
get_sui_ausd_coin_type()helper function
- Sui Blockchain Support: Added Sui mainnet and testnet networks
- Added
NetworkType.SUIfor Sui Move VM chains - Added
SUI_FEE_PAYER_MAINNETandSUI_FEE_PAYER_TESTNETsponsor addresses - Added CAIP-2 support for
sui:mainnetandsui:testnet - Added Sui-specific utilities:
validate_sui_payload(),is_valid_sui_address(),is_valid_sui_coin_type() - SDK now supports 18 blockchain networks
- Documentation updates for Algorand support
- Updated README with facilitator addresses and changelog
- Added EVM facilitator addresses for reference
EVM_FACILITATOR_MAINNET: 0x103040545AC5031A11E8C03dd11324C7333a13C7EVM_FACILITATOR_TESTNET: 0x34033041a5944B8F10f8E4D8496Bfb84f1A293A8
- Changed default Algorand mainnet network name from
algorand-mainnettoalgorand - Aligns with facilitator v1.9.5+ which now uses
algorandas the primary network identifier
- Facilitator Module: Added
facilitator.pywith all fee payer addresses embedded as constants - SDK users no longer need to configure facilitator addresses manually
- Added constants:
ALGORAND_FEE_PAYER_MAINNET,SOLANA_FEE_PAYER_MAINNET,NEAR_FEE_PAYER_MAINNET,STELLAR_FEE_PAYER_MAINNET, etc. - Added helper functions:
get_fee_payer(),requires_fee_payer(),build_payment_info() - Network-specific helpers:
get_algorand_fee_payer(),get_svm_fee_payer(),get_near_fee_payer(),get_stellar_fee_payer()
- Algorand Atomic Group Fix: Rewrote Algorand payload format to use GoPlausible x402-avm atomic group spec
- New
AlgorandPaymentPayloaddataclass withpaymentIndexandpaymentGroupfields - Added
build_atomic_group()helper for constructing two-transaction atomic groups - Added
validate_algorand_payload()for payload validation - Added
build_x402_payment_request()for building complete x402 requests
- Added AUSD (Agora USD) support on Solana using Token2022 program
- Added
TOKEN_2022_PROGRAM_IDconstant - Added
get_token_program_id()andis_token_2022()helpers
- Algorand Support: Added Algorand mainnet and testnet networks
- Added
ALGORANDNetworkType - Added
algorandoptional dependency (py-algorand-sdk>=2.0.0) - SDK now supports 16 blockchain networks
- Added USDT support (USDT0 omnichain via LayerZero) on Ethereum, Arbitrum, Optimism, Avalanche, Polygon
- SDK now supports 5 stablecoins: USDC, EURC, AUSD, PYUSD, USDT
- Fixed EIP-712 domain names: AUSD uses "Agora Dollar" (not "Agora USD")
- Fixed EURC domain name on Ethereum/Avalanche: "Euro Coin" (not "EURC")
- Added critical implementation notes for multi-token support:
- EIP-712 domain names vary by chain (e.g., EURC is "Euro Coin" on Ethereum but "EURC" on Base)
- PYUSD uses v,r,s signature variant (Paxos implementation)
- Token info must be passed to facilitator via
extrafield
- Removed GHO and crvUSD token support (not EIP-3009 compatible)
- SDK now supports 4 stablecoins: USDC, EURC, AUSD, PYUSD
- Multi-Stablecoin Support: Added support for 4 stablecoins on EVM chains
- USDC (all EVM chains)
- EURC (Ethereum, Base, Avalanche)
- AUSD (Ethereum, Arbitrum, Avalanche, Polygon, Monad)
- PYUSD (Ethereum)
- Added
TokenTypeliteral type andTokenConfigdataclass - Added token helper functions:
get_token_config(),get_supported_tokens(),is_token_supported(),get_networks_by_token() - Added
tokensfield toNetworkConfigfor multi-token configurations - Updated EVM network configurations with token contract addresses and EIP-712 domain parameters
- Added Security section to documentation
- Added Error Codes table
- Updated links to new GitHub repository
- Synced documentation with TypeScript SDK
- Removed BSC network (doesn't support ERC-3009)
- Added GitHub Actions workflow for PyPI publishing
- Updated to 14 supported networks
- Added NEAR Protocol support with NEP-366 meta-transactions
- Added Fogo SVM chain support
- Added x402 v2 protocol support with CAIP-2 network identifiers
- Added
acceptsarray for multi-network payment options - Refactored Solana to generic SVM type (supports Solana, Fogo, future SVM chains)
- Added CAIP-2 parsing utilities (
parse_caip2_network,to_caip2_network) - Added
MultiPaymentConfigfor multi-network recipient configuration - Added
Payment402BuilderV2for v2 response construction
- Initial release
- EVM, Solana, Stellar network support
- Flask, FastAPI, Django, Lambda integrations
- Full Pydantic models