Python SDK for x402 with Circle Gateway batching. Uses titanoboa for on-chain interactions.
Built for the Vyper ecosystem.
Note: The pip package is
circle-titanoboa-sdkbut the Python import iscirclekit:from circlekit import GatewayClient
# As a dependency
pip install .
# With x402 integration
pip install ".[x402]"
# For development
uv syncYou need testnet USDC on Arc Testnet (or another supported chain).
- Go to faucet.circle.com
- Select Arc Testnet
- Paste your wallet address
- Get 20 USDC (every 2 hours per address per network)
On Arc Testnet, USDC is the native gas token. You only need USDC for both payments and gas.
import asyncio
from circlekit import GatewayClient
async def main():
client = GatewayClient(
chain="arcTestnet",
private_key="0x..."
)
result = await client.pay("https://api.example.com/premium")
print(f"Got: {result.data}")
print(f"Paid: {result.formatted_amount} USDC")
await client.close()
asyncio.run(main())The middleware is framework-agnostic. Here's an example with FastAPI.
To run the examples: pip install circlekit[examples]
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from circlekit import create_gateway_middleware
app = FastAPI()
gateway = create_gateway_middleware(
seller_address="0xYourAddress",
chain="arcTestnet",
)
@app.get("/api/analyze")
async def analyze(request: Request):
result = await gateway.process_request(
payment_header=request.headers.get("PAYMENT-SIGNATURE"),
path=request.url.path,
price="$0.01",
)
if isinstance(result, dict):
# 402: return body + PAYMENT-REQUIRED header
resp = JSONResponse(result["body"], status_code=result["status"])
for k, v in result.get("headers", {}).items():
resp.headers[k] = v
return resp
# Success: return data + PAYMENT-RESPONSE header
resp = JSONResponse({"data": "Premium content", "paid_by": result.payer})
for k, v in result.response_headers.items():
resp.headers[k] = v
return respgateway = create_gateway_middleware(
seller_address="0xYourAddress",
chain="arcTestnet",
networks=["arcTestnet", "baseSepolia"],
)The 402 response will include one accepts entry per network, and incoming
payments will be validated against the accepted set.
If you already use the x402 Python package, add Circle Gateway as a facilitator:
pip install ".[x402]"The x402 package API may change independently of circlekit. See https://github.com/x402/x402 for the latest usage.
from circlekit.x402_integration import create_resource_server
server = create_resource_server(is_testnet=True)
server.initialize()
# Use with FastAPI, Flask, or any frameworkOr use BatchFacilitatorClient directly with x402ResourceServer:
from x402.server import x402ResourceServer
from circlekit import BatchFacilitatorClient
server = x402ResourceServer(BatchFacilitatorClient())
server.initialize()circle-titanoboa-sdk uses titanoboa internally, so you can combine x402 payments with Vyper contract interactions seamlessly.
# storage.vy
stored_value: public(uint256)
@external
def set_value(val: uint256):
self.stored_value = valimport boa
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from circlekit import create_gateway_middleware
app = FastAPI()
gateway = create_gateway_middleware(
seller_address="0xYourAddress",
chain="arcTestnet",
)
boa.set_network_env("https://arc-testnet.drpc.org")
storage = boa.load("storage.vy")
@app.get("/read")
async def read_value(request: Request):
result = await gateway.process_request(
payment_header=request.headers.get("PAYMENT-SIGNATURE"),
path=request.url.path,
price="$0.001",
)
if isinstance(result, dict):
resp = JSONResponse(result["body"], status_code=result["status"])
for k, v in result.get("headers", {}).items():
resp.headers[k] = v
return resp
value = storage.stored_value()
resp = JSONResponse({"value": value, "paid_by": result.payer})
for k, v in result.response_headers.items():
resp.headers[k] = v
return respGatewayClient separates two wallet capabilities:
| Capability | Protocol | Used by |
|---|---|---|
| EIP-712 signing | Signer |
pay(), withdraw() intent |
| Onchain tx execution | TxExecutor |
deposit(), withdraw() mint |
from circlekit import GatewayClient
# Simple: private_key creates both a Signer and TxExecutor:
client = GatewayClient(
chain="arcTestnet",
private_key="0x...",
rpc_url=None # Optional custom RPC
)
# Pay-only (signer is enough for gasless payments):
from circlekit import PrivateKeySigner
signer = PrivateKeySigner("0x...")
client = GatewayClient(chain="arcTestnet", signer=signer)
# client.pay() works; client.deposit()/withdraw() raise ValueError
# Advanced: inject capabilities separately:
from circlekit import BoaTxExecutor
client = GatewayClient(
chain="arcTestnet",
signer=my_signer,
tx_executor=BoaTxExecutor("0x..."),
)
# Properties
client.address # Your wallet address
client.chain_name # "Arc Testnet"
client.chain_id # 5042002
client.domain # 26 (Gateway domain)
# Methods
result = await client.pay(url) # Pay for resource (gasless, needs Signer)
balances = await client.get_balances() # Check balances (no capability needed)
support = await client.supports(url) # Check if URL accepts payments
await client.deposit("10.0") # Deposit USDC to Gateway (needs TxExecutor)
await client.deposit_for("10.0", depositor="0x...") # Deposit on behalf of another address
await client.withdraw("5.0", chain="baseSepolia") # Withdraw to another chain (needs both)An alternative to withdraw() that doesn't require the Gateway API. Uses on-chain delay instead:
delay = await client.get_trustless_withdrawal_delay() # Delay in blocks
await client.initiate_trustless_withdrawal("1.0") # Start withdrawal
# ... wait for delay blocks ...
block = await client.get_trustless_withdrawal_block() # Check eligible block
result = await client.complete_trustless_withdrawal() # Complete after delayTxExecutor is a Protocol for executing onchain transactions. BoaTxExecutor is the default implementation using titanoboa with a private key.
from circlekit import TxExecutor, BoaTxExecutor
# Default implementation:
executor = BoaTxExecutor("0xPrivateKey...")
# Custom implementations can wrap any execution backend (e.g., Circle
# Programmable Wallets). Just implement the TxExecutor protocol.Use Circle Developer-Controlled Wallets (MPC-backed) instead of a raw private key. No private key ever leaves Circle's infrastructure.
pip install ".[wallets]"from circlekit import GatewayClient
from circlekit.wallets import CircleWalletSigner, CircleTxExecutor
signer = CircleWalletSigner(wallet_id="...", wallet_address="0x...")
tx_executor = CircleTxExecutor(wallet_id="...", wallet_address="0x...")
# CIRCLE_API_KEY and CIRCLE_ENTITY_SECRET are read from env vars automatically
client = GatewayClient(chain="arcTestnet", signer=signer, tx_executor=tx_executor)Synchronous wrapper for scripts, CLIs, and Jupyter notebooks that cannot use async/await. Mirrors every GatewayClient method.
from circlekit import GatewayClientSync
client = GatewayClientSync(chain="arcTestnet", private_key="0x...")
result = client.pay("https://api.example.com/premium")
balances = client.get_balances()
client.close()from circlekit import create_gateway_middleware
gateway = create_gateway_middleware(
seller_address="0x...", # Where to receive payments
chain="arcTestnet", # Primary chain
networks=["arcTestnet", "baseSepolia"], # Accepted networks (optional)
description="My API", # Description in 402 response
)
# Framework-agnostic. Use process_request() in any handler:
result = await gateway.process_request(
payment_header=request.headers.get("PAYMENT-SIGNATURE"),
path=request.url.path,
price="$0.01",
)
if isinstance(result, dict):
# 402 response: set PAYMENT-REQUIRED header
resp = JSONResponse(result["body"], status_code=result["status"])
for k, v in result.get("headers", {}).items():
resp.headers[k] = v
return resp
else:
# PaymentInfo: set PAYMENT-RESPONSE header
resp = JSONResponse({"data": "..."})
for k, v in result.response_headers.items():
resp.headers[k] = v
return respThe returned GatewayMiddleware also exposes lower-level methods for custom flows:
# Build a 402 response manually:
payment_required = gateway.require("$0.01", "/api/analyze")
# Verify a payment without settling:
verify_result = await gateway.verify(payment_header, "$0.01")
# Settle a verified payment:
payment_info = await gateway.settle(payment_header, "$0.01")Creates EIP-712 TransferWithAuthorization payment payloads for the Gateway batching protocol.
from circlekit import BatchEvmScheme, PrivateKeySigner
signer = PrivateKeySigner("0x...")
scheme = BatchEvmScheme(signer)
payload = scheme.create_payment_payload(
x402_version=2,
requirements=requirements,
)from circlekit.x402 import (
parse_402_response,
create_payment_header,
decode_payment_header,
is_batch_payment,
get_verifying_contract,
)
from circlekit import PrivateKeySigner
# Parse 402 response
x402 = parse_402_response(response.content)
requirements = x402.get_gateway_option()
# Check if requirements use Gateway batching
if is_batch_payment(requirements):
contract = get_verifying_contract(requirements)
print(f"Gateway contract: {contract}")
# Create payment signature
signer = PrivateKeySigner("0x...")
header = create_payment_header(signer=signer, requirements=requirements)
# Decode an existing payment header
payload = decode_payment_header(header)
# Retry with payment
response = httpx.get(url, headers={"PAYMENT-SIGNATURE": header})from circlekit.boa_utils import (
setup_boa_env,
setup_boa_with_account,
get_usdc_balance,
get_gateway_balance,
execute_approve,
execute_deposit,
)
from circlekit.constants import get_chain_config
# Read-only setup
setup_boa_env("arcTestnet")
config = get_chain_config("arcTestnet")
import boa
usdc = boa.load_partial("path/to/IERC20.json").at(config.usdc_address)
balance = usdc.balanceOf("0x...")
# Transaction setup (adds signing account)
setup_boa_with_account("arcTestnet", "0xPrivateKey...")
# Now you can deploy/call Vyper contracts with real transactions| Network | Chain ID | Gateway Domain | Type |
|---|---|---|---|
| Arc Testnet | 5042002 | 26 | Testnet |
| Base Sepolia | 84532 | 6 | Testnet |
| Ethereum Sepolia | 11155111 | 0 | Testnet |
| Avalanche Fuji | 43113 | 1 | Testnet |
| HyperEVM Testnet | 998 | 19 | Testnet |
| Sonic Testnet | 14601 | 13 | Testnet |
| World Chain Sepolia | 4801 | 14 | Testnet |
| Sei Atlantic Testnet | 1328 | 16 | Testnet |
| Arbitrum Sepolia | 421614 | 3 | Testnet |
| Optimism Sepolia | 11155420 | 2 | Testnet |
| Polygon Amoy | 80002 | 7 | Testnet |
| Unichain Sepolia | 1301 | 10 | Testnet |
| Ethereum | 1 | 0 | Mainnet |
| Base | 8453 | 6 | Mainnet |
| Arbitrum One | 42161 | 3 | Mainnet |
| Polygon | 137 | 7 | Mainnet |
| Optimism | 10 | 2 | Mainnet |
| Avalanche C-Chain | 43114 | 1 | Mainnet |
| Sonic | 146 | 13 | Mainnet |
| Unichain | 130 | 10 | Mainnet |
| World Chain | 480 | 14 | Mainnet |
| HyperEVM | 999 | 19 | Mainnet |
| Sei | 1329 | 16 | Mainnet |
USDC contract addresses sourced from https://developers.circle.com/stablecoins/usdc-contract-addresses
Note: Arc Testnet uses USDC as the native gas token. Gateway Domain IDs are Circle's internal domain identifiers, not chain IDs.
titanoboa uses a global boa.env singleton with a SQLite-backed cache that is not thread-safe. GatewayClient mitigates this by routing all blocking boa calls through a single-thread ThreadPoolExecutor, but you should avoid creating multiple GatewayClient instances that call deposit(), withdraw(), or other on-chain methods concurrently in the same process. Gasless operations (pay(), get_balances(), supports()) use only HTTP and are safe to call concurrently.
If you need concurrent on-chain operations, run each in a separate process or use GatewayClientSync in separate threads with independent boa environments.
uv sync # Install all deps including dev group
uv run pre-commit install # Set up pre-commit hooksuv run pytest # Unit tests (360 tests)
# E2E tests (requires testnet USDC):
PRIVATE_KEY=0x... uv run pytest tests/test_e2e.py -v -sPre-commit runs automatically on git commit, or manually:
uv run ruff check --fix circlekit/ tests/ # Lint
uv run ruff format circlekit/ tests/ # Format
uv run mypy circlekit/ # Type checkcirclekit/
├── __init__.py # Package exports
├── constants.py # Chain configs, gateway addresses, protocol constants
├── signer.py # Signer protocol + PrivateKeySigner (EIP-712)
├── tx_executor.py # TxExecutor protocol + BoaTxExecutor (onchain txs)
├── facilitator.py # BatchFacilitatorClient (Gateway API verify/settle)
├── boa_utils.py # titanoboa helpers, contract ABIs, transactions
├── x402.py # x402 protocol (parse 402, create signatures, headers)
├── x402_integration.py # Optional x402 package integration
├── key_utils.py # Private key normalization, PrivateKeyLike type
├── wallets.py # Circle Developer-Controlled Wallets adapters
├── sync_client.py # GatewayClientSync (synchronous wrapper)
├── client.py # GatewayClient (pay, deposit, withdraw, balances)
└── server.py # Framework-agnostic payment middleware
MIT - see LICENSE
This is an unaudited reference implementation provided for educational and development purposes only. It is not production-ready software. Use at your own risk. The authors accept no liability for any losses or damages arising from its use or deployment.