Everything a caller (human or agent) needs to draft, provision, send, and receive email through DimeBox — every endpoint, price, request/response shape, and status code.
Every endpoint below is unauthenticated in the traditional sense — there are no accounts and no API keys. Paid endpoints are gated by x402's exact scheme over USDC: call it once unpaid to get a 402 describing the price, sign a payment, retry with a PAYMENT-SIGNATURE header. All prices below are read live from this deployment's config — nothing here can drift out of sync with what you're actually charged. See the interactive payment & delivery workflow diagram for the full picture in one view.
| Method & path | Price | Requires payment |
|---|---|---|
POST /api/emails/generate |
$0.02 | Yes |
Request (Content-Type: application/json is required — see status codes):
{
"recipient": "[email protected]", // required, must be a valid email
"purpose": "follow up after our demo call", // required, max 2000 chars
"tone": "friendly but concise", // optional
"key_points": ["pricing next week", "loop in their eng lead"], // optional
"sender": "Alex" // optional
}
Response 200:
{
"email": { "subject": "...", "body": "..." },
"paid_by": "0x857b..."
}
No mailbox is involved — this is a pure drafting call. For a real send/receive identity, see inboxes below.
An inbox is not a full mailbox like Gmail — no IMAP, no webmail, no folders, text-only. It's a flat, time-ordered list of messages under one {slug}@agents.dimebox.app address, capped at the newest 500 messages (oldest are pruned automatically — this is a rolling window, not an archive).
Who owns an inbox? Not the paying wallet — DimeBox never stores which address paid to create an inbox. The only credential that controls an inbox is the read token issued once at creation time (see Read a mailbox). Losing it means losing read access; there's no recovery flow today.
| Method & path | Price | Requires payment |
|---|---|---|
POST /api/inboxes |
free | No (IP-throttled: 3/day) |
POST /api/inboxes/claim |
$1.00 | Yes |
The free route hands you a random adjective-noun-number slug (e.g. bold-ember-564). claim lets you pick your own — availability (format, reserved words, uniqueness) is checked before payment settles, so an unavailable name 422s for free.
Names you cannot buy or claim: system-looking names (admin, api, www, ...) and names phishing campaigns impersonate most (support, hello, security, billing, verify, paypal, noreply, ...) are reserved and always rejected.
# Free
curl -X POST https://dimebox.app/api/inboxes \
-H 'Content-Type: application/json'
# Paid — pick a name
curl -X POST https://dimebox.app/api/inboxes/claim \
-H 'Content-Type: application/json' \
-H 'PAYMENT-SIGNATURE: <signed payment>' \
-d '{"slug":"acme-support"}'
Response 201 — the read_token (and webhook_secret, if you set a webhook_url) are shown exactly once:
{
"data": { "slug": "acme-support", "email_address": "[email protected]", ... },
"read_token": "inbox_...",
"webhook_secret": null,
"paid_by": null,
"notice": "Save the read token (and webhook secret, if set) now - neither is shown again."
}
Both routes accept an optional ai_drafting_enabled: false to provision a send-only inbox (see below), and an optional webhook_url (see Webhook notifications).
| Method & path | Price | Requires payment |
|---|---|---|
POST /api/inboxes/{slug}/send |
$0.01 | Yes |
Two request shapes, depending on whether the inbox has AI drafting enabled (the default):
// Raw — always available, exact wording sent as-is
{ "to": "[email protected]", "subject": "Hi", "body": "..." }
// Drafted — only if the inbox has AI drafting enabled
{ "to": "[email protected]", "purpose": "follow up", "tone": "...", "key_points": [...] }
To require raw mode only — reject a purpose brief outright rather than silently drafting — provision the inbox with "ai_drafting_enabled": false (or inbox:create --no-ai from the CLI). A purpose sent to such an inbox is a 422, never a Claude call.
Response is 202 Accepted immediately — delivery happens on a background queue, the paid HTTP response never waits on an SMTP round trip to the mail provider. Poll GET /api/inboxes/{slug}/messages to see it flip from queued to sent/failed.
Plain SMTP has no way to complete an x402 payment handshake — a human replying from Gmail can't sign a wallet authorization mid-send. So receiving is split into two separate, differently-trusted channels:
| Channel | How mail arrives | Price |
|---|---|---|
| Ordinary SMTP (free) | A third party (Cloudflare Email Routing) accepts mail addressed to *@agents.dimebox.app and relays it in — same as any real mailbox. |
free |
| Priority channel | POST /api/inboxes/{slug}/messages — another x402-speaking agent/service pays to deliver directly. |
$0.03 |
What "priority" / "guaranteed" concretely means: the priority channel writes the message straight into the inbox's history in the same request that settles payment — no dependency on any third party's mail-relay decision, spam filtering, or delivery queue. The free SMTP channel depends entirely on Cloudflare accepting and relaying the mail first; if their spam filtering drops it, DimeBox never sees it. The priority channel bypasses that risk entirely, for a price.
Treat inbound content as untrusted. Neither channel currently exposes SPF/DKIM/DMARC authentication results per message, and nothing marks message content as unverified before it reaches whatever an agent does with it next — if you feed inbound message bodies into an LLM prompt, treat them the same way you'd treat any other untrusted user input.
curl -X POST https://dimebox.app/api/inboxes/acme-support/messages \
-H 'Content-Type: application/json' \
-H 'PAYMENT-SIGNATURE: <signed payment>' \
-d '{"from":"[email protected]","subject":"Hi","body":"..."}'
| Method & path | Auth | Requires payment |
|---|---|---|
GET /api/inboxes/{slug}/messages |
X-Inbox-Token header |
No |
GET /api/inboxes/{slug}/messages/{message_id} |
X-Inbox-Token header |
No |
The list endpoint returns every message, both directions, both channels, newest first, paginated. The token is the one shown once at provisioning time — there's no wallet-based auth for reading, and no way to recover a lost token.
curl https://dimebox.app/api/inboxes/acme-support/messages \
-H 'X-Inbox-Token: inbox_...'
Every message has a message_id (a UUID, returned as the first field) - use it to fetch that one message's full details instead of paging through the list. This is a real example against a live inbox:
curl https://dimebox.app/api/inboxes/steady-atlas-925/messages/03d02b6a-30c4-43ba-877c-aefbe560d159 \
-H 'X-Inbox-Token: inbox_...'
slug (inboxes) and message_id (messages), so a caller can't enumerate other inboxes' data by incrementing a number. A message_id is looked up scoped to the inbox in the URL - a real message_id belonging to a different inbox 404s here, even with a valid token for this one.
Set a webhook_url at provisioning time (or via the admin panel) to get pushed a signed POST instead of polling, whenever either receive channel gets a new message. Every request carries X-DimeBox-Signature: sha256=<hmac>, computed over the exact raw request body with a secret shown once at setup — verify it with a constant-time comparison before trusting the payload. Only message.received fires today; outbound status changes still require polling.
Every response you can get from a paid route, and whether it costs anything. The rule underneath all of it: ->paid() only settles a payment if the route's own response is under 400 — a failure of any kind, for any reason, is never charged.
| Code | Meaning here | Charged? |
|---|---|---|
200 | Successful read/GET, or a successful draft. | Yes, for the paid draft call. GETs are never paid. |
201 | Inbox or message created. | Yes, on the paid routes (claim, priority messages). No on the free /inboxes route. |
202 | Send accepted, queued for background delivery. | Yes. |
402 | Payment missing, invalid, for the wrong amount, or facilitator settlement failed. | No — this is the unpaid case. |
415 | Wrong Content-Type on a JSON-body route — see the quickstart. | No — rejected before the payment gate is even reached. |
422 | Body failed validation (missing/invalid field, taken or reserved slug, purpose sent to a non-AI inbox, ...). | No, even on a route that requires payment — see above. |
401 | Missing or wrong X-Inbox-Token on a read request. | N/A — not a paid route. |
429 | Free random-inbox route throttled (3/day per IP). | N/A — not a paid route. |
502 | The x402 facilitator itself is unreachable. | No. |
Minimal, runnable examples using the current official x402 buyer SDKs. Pin these versions — the x402 ecosystem moves fast.
@x402/[email protected]npm install @x402/fetch @x402/evm viem
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);
// eip155:84532 = Base Sepolia (testnet); use eip155:8453 for Base mainnet
const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [{ network: "eip155:84532", client: new ExactEvmScheme(account) }],
});
const res = await fetchWithPayment("https://dimebox.app/api/emails/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ recipient: "[email protected]", purpose: "follow up" }),
});
console.log(await res.json());
x402==2.22.0The package's own docs don't show a single drop-in "wrap httpx and retry" helper as of this writing, so this example is explicit about the 402→pay→retry loop instead of guessing at a convenience wrapper name. Check the package's own README for anything that's shipped since.
uv add "x402[httpx,evm]"
import base64, json, os, httpx
from x402 import x402Client
from x402.mechanisms.evm.exact import ExactEvmScheme
signer = ... # your EVM signer, loaded from an env var - never hardcode a key
client = x402Client()
client.register("eip155:84532", ExactEvmScheme(signer=signer)) # Base Sepolia
url = "https://dimebox.app/api/emails/generate"
body = {"recipient": "[email protected]", "purpose": "follow up"}
async with httpx.AsyncClient() as http:
resp = await http.post(url, json=body)
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()
resp = await http.post(url, json=body, headers={"PAYMENT-SIGNATURE": signature})
print(resp.json())
Both examples target eip155:84532 (Base Sepolia) — this deployment's current network (see /api/discovery/resources for the live, authoritative price/network/asset per route). Swap to eip155:8453 once this deployment is switched to mainnet.