<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Marcus Chenmember_832ef635</title>
    <description>The latest articles on DEV Community by Marcus Chenmember_832ef635 (@marcuschen-dev).</description>
    <link>https://dev.to/marcuschen-dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4092629%2F58ba3ca9-b316-4be3-9871-70d0aba20bb3.png</url>
      <title>DEV Community: Marcus Chenmember_832ef635</title>
      <link>https://dev.to/marcuschen-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcuschen-dev"/>
    <language>en</language>
    <item>
      <title>I Built a French AI Voiceover API That Only Accepts Payment From Robots</title>
      <dc:creator>Marcus Chenmember_832ef635</dc:creator>
      <pubDate>Thu, 27 Aug 2026 07:27:00 +0000</pubDate>
      <link>https://dev.to/marcuschen-dev/i-built-a-french-ai-voiceover-api-that-only-accepts-payment-from-robots-435k</link>
      <guid>https://dev.to/marcuschen-dev/i-built-a-french-ai-voiceover-api-that-only-accepts-payment-from-robots-435k</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Let me back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: selling API access to AI agents is broken
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;is&lt;/em&gt; the auth.&lt;/p&gt;

&lt;p&gt;I'm building this in public and I want to be upfront: this launched with &lt;strong&gt;zero customers&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter x402: HTTP's forgotten status code, repurposed
&lt;/h2&gt;

&lt;p&gt;Back in 1997, HTTP/1.1's spec reserved status code &lt;strong&gt;402 Payment Required&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/coinbase/x402" rel="noopener noreferrer"&gt;x402 protocol&lt;/a&gt; resurrects it for exactly the machine-to-machine payment problem I described above. The flow is deceptively simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client requests a paid resource.&lt;/li&gt;
&lt;li&gt;Server responds &lt;code&gt;402 Payment Required&lt;/code&gt; with a JSON body describing exactly how much to pay, in what currency, to what address.&lt;/li&gt;
&lt;li&gt;Client pays on-chain (in my case, USDC on Base).&lt;/li&gt;
&lt;li&gt;Client retries the request, this time attaching proof of payment — a transaction hash.&lt;/li&gt;
&lt;li&gt;Server verifies the payment on-chain and, if it checks out, serves the resource.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That asymmetry is the entire bet: x402 is a worse UX for humans and a &lt;em&gt;better&lt;/em&gt; UX for machines than any existing payment rail. I wanted to build for the audience that rail is actually good for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;The service is called &lt;strong&gt;voixoff&lt;/strong&gt; (French for "voiceover"), and it does one thing: turns text into French audio, on demand, per call, for pocket change. Three products:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;30s ad spot&lt;/td&gt;
&lt;td&gt;Short-form commercial voiceover&lt;/td&gt;
&lt;td&gt;$0.05 USDC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60s audiobook narration&lt;/td&gt;
&lt;td&gt;Longer narrative-style read&lt;/td&gt;
&lt;td&gt;$0.05 USDC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15s IVR / phone greeting&lt;/td&gt;
&lt;td&gt;Short phone-system prompt&lt;/td&gt;
&lt;td&gt;$0.03 USDC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Under the hood, the stack is unglamorous on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flask&lt;/strong&gt; app, single process, doing both the payment gate and the audio generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;edge-tts&lt;/strong&gt;, the reverse-engineered wrapper around Microsoft Edge's neural TTS voices — free, no API key, no per-character billing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fr-FR neural voices&lt;/strong&gt;: Denise, Eloise, Henri, and the multilingual Vivienne&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;systemd&lt;/strong&gt; service (&lt;code&gt;x402-voixoff&lt;/code&gt;) keeping it alive on a cheap VPS&lt;/li&gt;
&lt;li&gt;Port &lt;code&gt;8402&lt;/code&gt; — yes, that's a deliberate nod to the 402 status code&lt;/li&gt;
&lt;li&gt;No domain yet. It's a raw IP: &lt;code&gt;http://187.77.111.249:8402&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is an honest limitation, not a flex — I'll get to the full list of caveats later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 402 handshake, in actual code
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;WALLET_ADDRESS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0x3f979b1203Fc3C3BBeAA73Dbec519C08c55dB074&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PRICES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# 30s ad spot
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audiobook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 60s narration
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ivr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# 15s phone greeting
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;seen_tx_hashes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# anti-replay: never accept the same proof twice
&lt;/span&gt;
&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;force&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;proof&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Payment-Proof&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PRICES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# No payment proof attached -&amp;gt; tell the client exactly how to pay
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment_required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PRICES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USDC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;network&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pay_to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WALLET_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voixoff:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;402&lt;/span&gt;

    &lt;span class="c1"&gt;# Proof attached -&amp;gt; verify on-chain before doing any work
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen_tx_hashes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment_already_used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;402&lt;/span&gt;

    &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify_onchain_payment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tx_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;expected_amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PRICES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;expected_recipient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;WALLET_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment_invalid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;402&lt;/span&gt;

    &lt;span class="n"&gt;seen_tx_hashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;audio_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_tts_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;send_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mimetype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio/mpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's what the client-side handshake looks like from &lt;code&gt;curl&lt;/code&gt;, to make the two-step dance concrete:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
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..."}'

# -&amp;gt; {"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.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>x402</category>
      <category>python</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
