DeadDrop is a zero-knowledge one-time-secret API: POST ciphertext, get a
self-destructing link, pay $0.01 in USDC per call. No accounts, no API keys,
no billing portal - because the customers are increasingly not humans.
This post walks through the three design problems that made it interesting:
honest zero-knowledge in a web app, an atomic "exactly once" read on
Cloudflare, and a payment wall for machines - including why I deleted the
official payment middleware and spoke the protocol directly in ~200 lines.
Try it first if you like (the landing has a free in-browser demo of the
crypto flow): https://deaddrop.jerrywrongalot.workers.dev
The problem
Pasting an API key, a password, or a seed phrase into chat or email leaves
it in logs, backups, and search indexes forever. The old answer is a
one-time link: the secret is destroyed on first read.
The newer, second problem: how do you charge for infrastructure like this
when the caller might be an AI agent? Agents cannot fill out signup forms,
verify email addresses, or type credit card numbers. Subscriptions and API
keys assume a human with a browser and a wallet-on-file. I wanted the
machine equivalent of a coin slot.
That is what x402 is: HTTP status 402 (Payment Required), revived as a real
protocol. The server answers an unpaid request with a challenge; the client
signs a payment authorization and retries; the server verifies and settles
through a "facilitator" service. No account anywhere. More on it below.
Zero-knowledge that survives scrutiny
"We encrypt your data" is easy to say. The design has to survive the
obvious follow-up questions.
The core move: encryption happens in the browser (AES-256-GCM via Web
Crypto), and the key travels in the URL fragment - the part after #.
Fragments never leave the client; browsers do not send them in requests.
The server stores ciphertext only. We could not read the secrets even if
compelled, because nothing decryptable ever reaches us.
The parts people usually miss:
-
XSS is the real enemy of fragment keys. If injected script runs on
the reveal page, it can read
location.hash. So the reveal page ships a hash-pinned Content-Security-Policy: the exact inline scripts are SHA-256-pinned, nounsafe-inline, no external script sources. An attacker cannot add a script that exfiltrates the fragment without breaking the CSP. -
Link previewers must not burn the secret. Slack, Telegram, and iMessage
all prefetch links. A GET on a secret URL therefore never consumes it -
it returns metadata or a viewer page. Revealing is an explicit POST, and
the response carries
Cache-Control: no-storeplusX-Robots-Tag: noindex. - Logs are a side channel. The event log is a strict allowlisted union: event names, sizes, TTLs, outcomes. No secret ids ever (the id IS the read capability), and no IP addresses at all - not even hashed ones, because the IPv4 space brute-forces in seconds, so a hashed IP is just an IP with extra steps.
Exactly-once on Cloudflare: Durable Objects as an atomicity primitive
"Destroyed after one read" is a concurrency claim, and eventually
consistent storage cannot make it. With Workers KV, two clients revealing
simultaneously could both win the race and both receive the secret.
Durable Objects solve this structurally: one DO instance per secret, and
workerd's input gates serialize access to it. The consume path is
read-then-delete inside a single object - there is no interleaving in
which two readers both see the ciphertext:
async consume(): Promise<string | null> {
const ct = await this.ctx.storage.get<string>("ciphertext");
if (ct === undefined) return null;
await this.ctx.storage.deleteAll(); // one reader, then gone
return ct;
}
TTL expiry is a DO alarm that burns whatever is left. Nothing to garbage
collect, nothing to back up - by design there is nothing to recover.
Charging machines: the x402 flow
An unpaid POST /secret gets a 402 with a machine-readable challenge:
{
"error": "X-PAYMENT header is required",
"x402Version": 1,
"accepts": [{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "10000",
"payTo": "0x...",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"maxTimeoutSeconds": 300
}]
}
The client (a library like x402-fetch, or an agent) signs an EIP-3009
transferWithAuthorization for exactly that amount - a gasless signature;
the facilitator submits it on-chain and pays the gas - and retries with the
signature in an X-PAYMENT header. The server then:
-
verifies before doing any work (facilitator
/verify), - runs the handler,
-
settles only after success (facilitator
/settle), and - attaches a receipt header with the on-chain transaction.
Two properties fall out of the ordering: no failure mode charges a caller
without delivering, and unpaid probes cost you almost nothing. Reading a
secret is free - the recipient of a link needs no wallet, which matters if
you want normal humans on the receiving end.
The payer's identity is a wallet signature. That is the whole "account
system".
The supply-chain story: 4.8 MB -> 90 KB, 32 audit findings -> 0
The official Hono middleware for x402 worked in an afternoon. Then I looked
at the deployed bundle: 4.8 MB for a service whose own code is a few
hundred lines. npm audit: 32 findings, 23 high, every one of them
transitive.
The cause: the payment package compiles client AND server features into
shared chunks with module-level imports, so importing the server middleware
drags in the entire client-side wallet stack - MetaMask connectors,
WalletConnect, viem with WebSocket transports - code a server never
executes. Tree-shaking cannot remove module-level imports, and there was no
fixed upstream release. Dormant code, but it is all attack surface, it
keeps every scanner red, and it 25x-ed the artifact.
So I deleted both packages and spoke the protocol directly. The server side
of x402 is genuinely small: build the challenge JSON, decode a base64
header, and make two REST calls -
POST {facilitator}/verify {x402Version, paymentPayload, paymentRequirements}
POST {facilitator}/settle {x402Version, paymentPayload, paymentRequirements}
- with verify-before-work and settle-after-success ordering. About 200 lines including error taxonomy and telemetry hooks. To make sure I was not quietly diverging from the ecosystem, I pinned it against a byte-exact 402 challenge captured from the official middleware in production before the swap, plus the upstream client source. The live challenge after the swap was identical.
Results: dependencies -> just hono. Bundle 4,821 KB -> 90 KB. Worker
startup 18 ms -> 5 ms. Audit findings -> zero. All cryptographic
verification and on-chain settlement stays delegated to the facilitator -
the 200 lines contain no key handling and no hand-rolled crypto.
The general lesson: payment SDKs are often client+server monoliths. The
server half of a payment protocol is usually a thin REST client wearing a
big trench coat.
The part most launch posts skip: a security gate
Before pointing this at mainnet, I ran a principle-driven security review
against a 65-principle checklist and treated it as a real gate with a
human signature - not vibes. First verdict: BLOCKED, two highs.
- No end-to-end paid proof. Closed with a real testnet payment from a standard client, settled on-chain, receipt verified, one-time semantics held under the paid path.
- Mainnet facilitator auth unwired. The production facilitator authenticates with per-request Ed25519 JWTs. Web Crypto in workerd signs Ed25519 natively, so this needed zero new dependencies. The nice trick: you can prove the whole auth + protocol path WITHOUT owning any USDC - sign a real payment from an empty wallet and check that the facilitator returns a structured "insufficient balance"-class verdict. An auth failure returns a completely different shape. Nothing is charged; the path is proven on both testnet and mainnet.
Then the human signs, then you flip. If you are shipping anything that
moves money, some version of this gate is the cheapest insurance you will
ever buy.
What $0.01 actually buys
People ask why it is not free. A cent per secret buys:
- No accounts or billing infra - no signup wall for humans, no OAuth dance for agents, no invoices, no card-on-file.
- Aligned incentives - free services die or monetize your data. A priced service does neither, and this one cannot even read what it stores.
- Spam economics - bots do not pay coin slots. The paid gate plus rate limiting bounds abuse without CAPTCHAs.
- No payment gatekeeper on the receiving end - receiving USDC needs an address, not a merchant account.
DeadDrop is live: https://deaddrop.jerrywrongalot.workers.dev - the landing
demo runs the full encrypt/decrypt flow in your browser for free, and the
API costs a cent per secret if you want to use it for real (your agent can
pay for itself over x402). There is an agent-readable spec at /llms.txt and
an open-source MCP server (https://github.com/jerrywrongalot-byte/deaddrop-mcp)
if you want Claude to drop and reveal secrets natively.
I would love feedback on the crypto design, the x402 flow, and whether
per-call micropayments feel right for infrastructure like this.
Top comments (0)