Every paid DimeBox route speaks x402 the same way: call it once with no payment, get back a 402 describing exactly what's owed, sign that, retry. This post walks through doing that in Python end to end — not inside an agent, not inside a framework, just the raw calls — so you have a working baseline before building anything on top of it.
Pin your versions; the x402 ecosystem is still moving fast enough that "latest" is a bad default for anything you intend to run more than once:
uv add "x402[httpx,evm]"
# or: pip install "x402[httpx,evm]"
This pulls in the EVM-specific exact-scheme support alongside the base client — see what the exact scheme actually is if you want the mechanics behind what this package does for you.
import os
from eth_account import Account
signer = Account.from_key(os.environ["EVM_PRIVATE_KEY"])
Decide which network you're signing for before writing another line — Base Sepolia vs Base mainnet covers the tradeoff. Everything below uses eip155:84532 (Sepolia); swap to eip155:8453 once you're ready for real USDC.
import base64, json, httpx
from x402 import x402Client
from x402.mechanisms.evm.exact import ExactEvmScheme
client = x402Client()
client.register("eip155:84532", ExactEvmScheme(signer=signer))
async def pay_and_call(http: httpx.AsyncClient, method: str, url: str, **kwargs) -> httpx.Response:
resp = await http.request(method, url, **kwargs)
if resp.status_code == 402:
payment_required = json.loads(base64.b64decode(resp.headers["PAYMENT-REQUIRED"]))
payload = await client.create_payment_payload(payment_required)
signature = base64.b64encode(json.dumps(payload).encode()).decode()
headers = {**kwargs.pop("headers", {}), "PAYMENT-SIGNATURE": signature}
resp = await http.request(method, url, headers=headers, **kwargs)
return resp
This is the same loop shown in DimeBox's own quickstart, pulled out into a helper you can reuse across every paid route instead of copy-pasting the 402 branch each time.
Provisioning a free inbox never hits the 402 branch at all — it's unauthenticated outright. Sending from it does:
async def main():
async with httpx.AsyncClient() as http:
provision = await http.post("https://dimebox.app/api/inboxes")
provision.raise_for_status()
inbox = provision.json()
slug = inbox["data"]["slug"]
read_token = inbox["read_token"] # shown exactly once - save it now
send = await pay_and_call(
http, "POST",
f"https://dimebox.app/api/inboxes/{slug}/send",
json={"to": "[email protected]", "purpose": "follow up after our demo call"},
)
send.raise_for_status()
print(send.status_code, send.json()) # 202 - queued for delivery
import asyncio
asyncio.run(main())
Not every script is already async, and there's no requirement that it become one just to pay DimeBox. httpx has a synchronous client with the same method names, and the x402 client's payload creation can usually be awaited from a small sync wrapper if the installed version requires it — check whichever is current for your installed package. The shape of the loop doesn't change, only the keyword:
import asyncio
def pay_and_call_sync(http: httpx.Client, method: str, url: str, **kwargs) -> httpx.Response:
resp = http.request(method, url, **kwargs)
if resp.status_code == 402:
payment_required = json.loads(base64.b64decode(resp.headers["PAYMENT-REQUIRED"]))
payload = asyncio.run(client.create_payment_payload(payment_required))
signature = base64.b64encode(json.dumps(payload).encode()).decode()
headers = {**kwargs.pop("headers", {}), "PAYMENT-SIGNATURE": signature}
resp = http.request(method, url, headers=headers, **kwargs)
return resp
This trades a small amount of elegance (spinning up an event loop just for the payment step) for not having to restructure an otherwise synchronous script around one call.
A second 402 after you've already signed and retried means something more specific than "payment didn't go through" — the signed amount no longer matches what the route expects, the signature's validity window expired before the retry reached the server, or whatever executes settlement — a facilitator, in DimeBox's case — couldn't be reached. None of these are something a tighter retry loop fixes on its own; they're worth logging distinctly from a first-attempt 402, which is just the protocol working as designed. x402's threat model covers what a persistent second failure might actually indicate.
A successful draft or send doesn't return the signer's wallet address back to you by accident — the response includes a paid_by field precisely so you can confirm which address the payment settled from, useful mostly for reconciling logs rather than anything your agent needs at runtime. Log it anyway; it's the cheapest audit trail you'll get for free.
This loop is the one piece every Python integration in this pillar builds on, whether that's wrapping it as a LangChain tool or running it unattended from a cron job. Once it's working against Sepolia, testing the full send/receive cycle end to end is the natural next step before pointing any of this at mainnet.