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 mail relay accepts mail addressed to *@agents.dimebox.app and forwards 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 that upstream relay accepting and delivering the mail first; if its 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. |
Every endpoint above is also exposed as an MCP tool, for agent frameworks that speak MCP instead of raw HTTP. Same prices, same 402-if-unpaid behavior — the tools just wrap the calls above.
| Tool | Does what |
|---|---|
create-wallet-tool | Start a card-funded wallet — returns a checkout_url and a checkout_session_id (a human still has to open the link and pay). |
check-wallet-checkout-tool | Poll that checkout_session_id; safe before payment completes (replies status: pending), reveals wallet_id/wallet_secret exactly once it's paid. |
wallet-status-tool | Check a wallet's balance, settings, and recent ledger. |
create-inbox-tool | Provision an inbox — free random address, or a paid chosen slug. |
draft-email-tool | Draft a subject + body from a brief, no sending. |
send-email-tool | Send from an inbox — AI-drafted from a brief, or exact subject/body. |
read-inbox-tool | List an inbox's message history. |
read-message-tool | Read one message's full details by id. |
Connect with the Claude Code CLI:
claude mcp add --transport http dimebox https://dimebox.app/mcp
Or add it to any MCP client's config directly (Claude Desktop, Cursor, etc.):
{
"mcpServers": {
"dimebox": {
"url": "https://dimebox.app/mcp"
}
}
}
Paid tools still require a wallet: call create-wallet-tool first (or skip straight to a tool like draft-email-tool and read the 402 it returns — it explains exactly what to do next), hand the human its checkout_url, then poll check-wallet-checkout-tool with its checkout_session_id until it stops replying pending and reveals wallet_secret. Pass that to every paid call after that. x402 isn't available from inside an MCP tool call, since there's no way to prompt for a crypto signature mid-call — the card wallet is the only payment path here. See the MCP getting-started guide for the full walkthrough.
Every tool that takes wallet_id/wallet_secret or an inbox's slug/inbox_token also accepts them as headers on the request to /mcp itself — set them once in your MCP client's config instead of passing them as arguments (or pasting them into chat) every time:
claude mcp add --transport http dimebox https://dimebox.app/mcp \
-H "Authorization: Bearer wsec_..." \
-H "X-Wallet-Id: wal_..." \
-H "X-Inbox-Slug: your-inbox-slug" \
-H "X-Inbox-Token: inbox_..." \
--scope user
--scope user makes it available in every project, not just the current one. Never use --scope project for this — that writes to a config file meant to be checked into the repo, which would commit your secrets to git. A tool argument always wins over the header if both are present. Only set the X-Inbox-* headers if you're working with a single, specific inbox long-term — an agent juggling several inboxes should keep passing slug/inbox_token as arguments instead.
wallet_id/wallet_secret or slug/inbox_token to a local file — chat history alone is a single point of failure you may not be able to undo. DimeBox's tools are instructed to do this automatically, but it's worth confirming your agent actually did.
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.