Nobody signed up for my API. Nobody generated an API key. Nobody clicked "Subscribe." And that's by design — because the only customers I actually want are AI agents with a wallet and a job to do.
Let me back up.
The problem: selling API access to AI agents is broken
If you've ever tried to monetize a small API, you know the funnel: landing page → sign up → verify email → generate API key → add a credit card → hit a rate limit → maybe, eventually, get paid a few cents per call. That funnel was built for humans. It makes no sense for a machine.
Increasingly, the "user" hitting your endpoint isn't a person filling out a form — it's an autonomous agent that decided, mid-task, that it needs a French voiceover for a video it's assembling, or a phone greeting for an IVR flow it's configuring for a client. That agent doesn't want to create an account. It doesn't have an email inbox to verify. It can't solve a CAPTCHA in any meaningful sense, and honestly, why should it have to? It has a wallet. It has stablecoins. It has a task with a deadline measured in seconds, not the three business days your KYC provider needs to approve a new merchant account.
So I built something that skips all of that: a pay-per-call French TTS API where the entire authentication layer is "did you pay for this specific request." No signup. No API key. No dashboard. Payment is the auth.
I'm building this in public and I want to be upfront: this launched with zero customers. No revenue to report, no growth chart, no "I made $X in a week" nonsense. This is an honest write-up of the architecture, the protocol, and the trade-offs — for other developers who might want to build something similar, or who are curious what machine-payable APIs actually look like in practice.
Enter x402: HTTP's forgotten status code, repurposed
Back in 1997, HTTP/1.1's spec reserved status code 402 Payment Required and then... never defined what to do with it. It's sat there for almost three decades as the most famous unused corner of the HTTP spec.
The x402 protocol resurrects it for exactly the machine-to-machine payment problem I described above. The flow is deceptively simple:
- Client requests a paid resource.
- Server responds
402 Payment Requiredwith a JSON body describing exactly how much to pay, in what currency, to what address. - Client pays on-chain (in my case, USDC on Base).
- Client retries the request, this time attaching proof of payment — a transaction hash.
- Server verifies the payment on-chain and, if it checks out, serves the resource.
No accounts. No sessions. No stored payment methods. Every single request carries its own proof of value. For a human this is mildly annoying — go get a wallet, buy some USDC, wait for a transaction to confirm. For an autonomous agent that already holds a wallet and treats USDC as a fungible resource, it's just... another tool call.
That asymmetry is the entire bet: x402 is a worse UX for humans and a better UX for machines than any existing payment rail. I wanted to build for the audience that rail is actually good for.
What I built
The service is called voixoff (French for "voiceover"), and it does one thing: turns text into French audio, on demand, per call, for pocket change. Three products:
| Product | Description | Price |
|---|---|---|
| 30s ad spot | Short-form commercial voiceover | $0.05 USDC |
| 60s audiobook narration | Longer narrative-style read | $0.05 USDC |
| 15s IVR / phone greeting | Short phone-system prompt | $0.03 USDC |
Prices are deliberately, almost comically low — under 10 cents per call — because right now the entire goal is bootstrapping the first sales, not maximizing margin. When your marginal cost per generation is close to zero (more on that below), you can afford to give the market a reason to try you before you try to extract value from it.
Under the hood, the stack is unglamorous on purpose:
- Flask app, single process, doing both the payment gate and the audio generation
- edge-tts, the reverse-engineered wrapper around Microsoft Edge's neural TTS voices — free, no API key, no per-character billing
- fr-FR neural voices: Denise, Eloise, Henri, and the multilingual Vivienne
-
systemd service (
x402-voixoff) keeping it alive on a cheap VPS - Port
8402— yes, that's a deliberate nod to the 402 status code - No domain yet. It's a raw IP:
http://187.77.111.249:8402
That last point is an honest limitation, not a flex — I'll get to the full list of caveats later.
The 402 handshake, in actual code
Here's the shape of the Flask route that gates generation. The first request (no payment proof) gets a 402 with everything the client needs to pay:
from flask import Flask, request, jsonify
import time
app = Flask(__name__)
WALLET_ADDRESS = "0x3f979b1203Fc3C3BBeAA73Dbec519C08c55dB074"
PRICES = {
"pub": 0.05, # 30s ad spot
"audiobook": 0.05, # 60s narration
"ivr": 0.03, # 15s phone greeting
}
seen_tx_hashes = set() # anti-replay: never accept the same proof twice
@app.route("/generate", methods=["POST"])
def generate():
body = request.get_json(force=True)
product = body.get("type")
proof = request.headers.get("X-Payment-Proof")
if product not in PRICES:
return jsonify({"error": "unknown product"}), 400
if not proof:
# No payment proof attached -> tell the client exactly how to pay
return jsonify({
"error": "payment_required",
"amount": PRICES[product],
"currency": "USDC",
"network": "base",
"pay_to": WALLET_ADDRESS,
"memo": f"voixoff:{product}:{int(time.time())}",
}), 402
# Proof attached -> verify on-chain before doing any work
if proof in seen_tx_hashes:
return jsonify({"error": "payment_already_used"}), 402
ok, reason = verify_onchain_payment(
tx_hash=proof,
expected_amount=PRICES[product],
expected_recipient=WALLET_ADDRESS,
)
if not ok:
return jsonify({"error": "payment_invalid", "reason": reason}), 402
seen_tx_hashes.add(proof)
audio_path = run_tts_pipeline(body.get("text"), product)
return send_file(audio_path, mimetype="audio/mpeg")
And here's what the client-side handshake looks like from curl, to make the two-step dance concrete:
bash
# Step 1: try without payment, get the 402 with instructions
curl -s -X POST http://187.77.111.249:8402/generate \
-H "Content-Type: application/json" \
-d '{"type": "ivr", "text": "Bonjour, vous êtes bien chez..."}'
# -> {"error": "Payment Required", "product": "ivr", "amount": 0.03,
# "currency": "USDC", "network": "BASE",
# "address": "0x3f979b1203Fc3C3BBeAA73Dbec519C08c55dB074",
# "proof": "envoyez le hash de tx Base en header X-Payment-Proof"}
# Step 2: pay 0.03 USDC on Base to that address (agent does this with its own wallet),
# then retry with proof
curl -s -X POST http://187.77.111.249:8402/generate \
-H "Content-Type: application/json" \
-H "X-Payment-Proof: 0xabc123...realTxHash" \
-d '{"type": "ivr", "text": "Bonjour, vous êtes bien chez..."}' \
--output greeting.mp3
### Why verify on-chain myself instead of using a facilitator
x402 implementations often delegate verification to a "facilitator" service that checks payments for you and returns a simple yes/no. I skipped that for now and verify directly against the Base RPC: fetch the transaction by hash, confirm the recipient address matches my wallet, confirm the USDC amount clears the price, and check the hash isn't already in my `seen_tx_hashes` set (that's the whole anti-replay mechanism — dead simple, and sufficient at single-process scale).
The reason is mostly about reducing moving parts while I'm the only thing running this service: one fewer external dependency, one fewer thing that can silently drift out of sync with what's actually on-chain, and one fewer party that needs to be trusted. If volume ever justified it, a facilitator would be a reasonable trade of simplicity for offloaded verification work. At zero customers, that trade isn't worth making yet.
### Why per-call pricing instead of a subscription
The whole appeal of x402 is that a request can carry its own proof of payment with no persistent relationship to the server. A subscription reintroduces the exact thing I'm trying to avoid — an account, a billing cycle, a thing to log into. Fixed per-call pricing keeps the API stateless from the client's perspective: an agent that has never talked to this server before can pay, generate, and never come back, and the system works exactly the same as it would for a "regular" caller. That statelessness is the point.
## The generation pipeline
Once payment clears, the actual TTS work is almost anticlimactic — `edge-tts` does the heavy lifting for free, streaming neural audio in the requested French voice. But I didn't want to just pipe raw TTS output back to a paying caller (even a robot deserves quality control), so there's a small `ffmpeg`/`ffprobe` gate before anything gets returned:
- **Duration check** — does the output roughly match the promised product length (30s, 60s, 15s)?
- **Clipping detection** — `ffmpeg`'s `volumedetect` filter flags any sample hitting 0 dB
- **Silence detection** — catches generations that came back truncated or empty
- **Loudness normalization** — EBU R128 loudness metering, so a 15s IVR greeting isn't jarringly louder or quieter than the 30s ad spot next to it
Every demo in the catalog passed through this gate before going live. Cheap insurance for a product whose entire value proposition is "trustworthy enough that a machine will pay for it sight-unseen."
## The honest limitations
I said upfront this is zero-hype, so here's the actual state of things:
- **Zero customers so far.** This is freshly launched. I have no usage data, no revenue, nothing to report except that the code runs.
- **x402 adoption is early.** The pool of agents that actually know how to do this handshake is small. This is a bet on where things are going, not where they are.
- **Raw IP, no TLS, no domain.** `http://187.77.111.249:8402` is not a URL that inspires confidence, and it isn't supposed to yet — it's the address of a live experiment, not a finished product.
- **edge-tts is "good for free," not studio-grade.** It's a genuinely solid neural voice, reverse-engineered from Microsoft Edge's read-aloud feature, at zero marginal cost. It is not ElevenLabs. I don't have an ElevenLabs key, and their free tier explicitly forbids commercial use anyway, so this was a deliberate cost/quality trade, not an oversight.
- **The price experiment might just not work.** Under-10-cent pricing is a hypothesis, not a proven strategy. It might attract zero traffic just as easily as it might attract volume. I won't know until agents actually start calling it.
The upside of all those constraints: because the TTS engine is free and the VPS is nearly free, my margin per call is close to 100%. There's no unit economics problem to solve — the problem is entirely demand-side. That's a much better problem to have than a cost problem, but it's still a real one.
## What I'd do differently
If I were starting over, I'd put a domain and TLS cert in front of this before writing a single line of the payment gate — agents (and the humans configuring them) are going to be understandably wary of POSTing a payment proof to a bare IP over plain HTTP, and that's a fixable trust problem I created for myself by prioritizing the protocol logic first. I'd also consider exposing the x402 service description (`GET /`) in whatever emerging discovery format agents end up standardizing on, so this shows up in agent tool-registries rather than only being reachable if someone already has the URL.
For now, though, the protocol layer works, the generation pipeline works, and the whole thing runs for effectively $0 in infrastructure cost beyond the VPS I'd be paying for anyway. The next step is just getting a single agent, anywhere, to actually pay for a voiceover.
## Try it
- **API**: `http://187.77.111.249:8402` — `GET /` returns the service card (products, prices, wallet address); `POST /generate` starts the 402 handshake described above.
- **Portfolio / human-facing samples**: [voixoff-fr.surge.sh](https://voixoff-fr.surge.sh) — a static, zero-JS page with 9 audio samples across the three product types, hosted free on surge.sh, for anyone who wants to hear the voices before an agent does the paying.
If you're experimenting with agent wallets, x402, or machine-payable APIs of your own, I'd genuinely like to compare notes.
Top comments (0)