<?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: bykamo</title>
    <description>The latest articles on DEV Community by bykamo (@bykamo).</description>
    <link>https://dev.to/bykamo</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%2F3980371%2F8d055a96-7c69-4fde-9089-1c84325863f4.png</url>
      <title>DEV Community: bykamo</title>
      <link>https://dev.to/bykamo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bykamo"/>
    <language>en</language>
    <item>
      <title>One $0.001 Price, Two Settlement Models: x402 on Base vs MPP Sessions on Tempo</title>
      <dc:creator>bykamo</dc:creator>
      <pubDate>Sun, 14 Jun 2026 21:51:32 +0000</pubDate>
      <link>https://dev.to/bykamo/one-0001-price-two-settlement-models-x402-on-base-vs-mpp-sessions-on-tempo-3n1g</link>
      <guid>https://dev.to/bykamo/one-0001-price-two-settlement-models-x402-on-base-vs-mpp-sessions-on-tempo-3n1g</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvnre7m8werv5mowr362a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvnre7m8werv5mowr362a.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I charged the same $0.001 on two payment rails and measured every call.&lt;/p&gt;

&lt;p&gt;On x402 + Base, each settled call incurred &lt;strong&gt;$0.00144 in on-chain gas&lt;/strong&gt;, more than the payment itself, paid by a facilitator relayer I never see. On Tempo with an MPP session, I deposited once, made 100 calls the chain never recorded individually, and the open-plus-close gas was too small to show up in my balance at all. Same price on the invoice, a different machine underneath.&lt;/p&gt;

&lt;p&gt;One thing up front, because it changes how you should read this. I tested &lt;strong&gt;one&lt;/strong&gt; x402 configuration (the &lt;code&gt;exact&lt;/code&gt; scheme on Base, settled through Coinbase's CDP facilitator) and &lt;strong&gt;one&lt;/strong&gt; MPP session service on Tempo (the official RPC). This is not a universal benchmark of either ecosystem. It's a measurement of two production payment paths an agent can use today, and most of what I can compare cleanly is the buyer's side: cost and latency per call.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The honest framing isn't "x402 vs Tempo." It's x402 (a standard) vs MPP (a protocol) on Tempo (a chain). x402 standardizes the HTTP 402 handshake. MPP, the Machine Payments Protocol co-authored by Stripe and Tempo, optimizes the settlement rail underneath. They sit at different layers, and MPP can wrap an x402-style handshake.&lt;/li&gt;
&lt;li&gt;In the x402 exact scheme I tested, every successful call settled on-chain: 30 calls, 30 transactions, ~$0.00144 gas each (measured), paid by a facilitator relayer. At a $0.001 price, the gas is bigger than the sale, which is why hosted facilitators carry a per-call floor.&lt;/li&gt;
&lt;li&gt;The MPP session I tested deposits once and spends against an off-chain voucher. Across 100 calls the chain saw two transactions, open and close, and the gas for that pair was below my balance's resolution ($0.000001). For a high-frequency agent loop, that's the difference that matters.&lt;/li&gt;
&lt;li&gt;If you're a non-US builder (I'm in Kyoto), Tempo's headline merchant edge, Stripe USD settlement, is US-only today. That one constraint can flip the recommendation back to x402.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A note on fairness. On the x402 side I built the seller endpoint, a Cloudflare Worker. On the Tempo side I paid an existing seller, the official Tempo RPC service, through the &lt;code&gt;tempo&lt;/code&gt; CLI. So per-call buyer cost and latency are a clean comparison. How hard the seller is to build is not, because I never built the Tempo seller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgbryjeqdnh46qj7efla9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgbryjeqdnh46qj7efla9.png" alt=" " width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The hinge: x402 puts the payment in the request; an MPP session puts the request inside a deposit. In the exact scheme, an x402 call is one on-chain settlement: my 30 calls produced 30 transactions. An MPP session is two transactions, open and close, regardless of how many calls ride between them; I sent 100 and the chain still recorded two.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rail A — x402 (the seller, extracted from the production docolumn worker)
&lt;/h3&gt;

&lt;p&gt;The seller answers &lt;code&gt;402&lt;/code&gt; with payment requirements, then verifies and settles through Coinbase's CDP facilitator. The whole paid path is about 140 lines. The core is the verify-settle round-trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bench/08-x402-vs-tempo/x402/worker.ts  (Cloudflare Worker)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createCdpAuthHeaders&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@coinbase/x402&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1) no payment yet -&amp;gt; 402 with requirements (scheme "exact", USDC on Base, "1000" = $0.001)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;xPayment&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;make402&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2) client retried with a signed EIP-3009 payload -&amp;gt; verify, then settle&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifyRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FACILITATOR_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/verify`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* CDP auth + payload */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;verifyJson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isValid&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;make402&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verifyJson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invalidReason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settleRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FACILITATOR_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/settle`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;- waits for the Base tx&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// returns X-Bench-Verify-Ms / X-Bench-Settle-Ms / X-Bench-Tx so the client can record the breakdown&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client is just &lt;code&gt;x402-fetch&lt;/code&gt;. &lt;code&gt;wrapFetchWithPayment(fetch, wallet)&lt;/code&gt; catches the 402, signs &lt;code&gt;transferWithAuthorization&lt;/code&gt; (EIP-3009), and retries. The wallet only signs. It needs USDC but no ETH, because the facilitator submits the transaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rail B — Tempo / MPP (the buyer, via the official CLI)
&lt;/h3&gt;

&lt;p&gt;I didn't hand-roll a Tempo seller. I paid a real one. The &lt;code&gt;tempo&lt;/code&gt; CLI is a curl-compatible client that negotiates MPP automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# bench/08-x402-vs-tempo/tempo/rpc-bench.mjs drives this call N times&lt;/span&gt;
tempo request &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://rpc.mpp.tempo.xyz/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service advertises &lt;code&gt;intent: session&lt;/code&gt; at &lt;code&gt;$0.001/call&lt;/code&gt;. The first call opens a session, depositing to the MPP escrow precompile (&lt;code&gt;0x4d50500…0000&lt;/code&gt;, "MPP" in ASCII), and every call after rides an off-chain voucher against that deposit. The deposit is dynamic; my 100-call run locked about $1.90, and I got all of it back minus the $0.101 I actually spent. Tempo charges gas in the stablecoin itself; there's no native gas token (the account's native balance is a &lt;code&gt;0x4242…&lt;/code&gt; sentinel).&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost
&lt;/h2&gt;

&lt;p&gt;Measured directly: x402 over 30 calls, Tempo over 100 (&lt;code&gt;$0.001/call&lt;/code&gt; on both, Base mainnet ETH=$1,667). No extrapolation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;x402 exact / Base / CDP (N=30)&lt;/th&gt;
&lt;th&gt;MPP session / Tempo RPC (N=100)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Seller can safely release the response&lt;/td&gt;
&lt;td&gt;settle p50 &lt;strong&gt;1,569ms&lt;/strong&gt; (p95 2,349)&lt;/td&gt;
&lt;td&gt;voucher p50 &lt;strong&gt;554ms&lt;/strong&gt; warm (p95 831)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;verify (validity check only)&lt;/td&gt;
&lt;td&gt;p50 269ms (p95 594)&lt;/td&gt;
&lt;td&gt;folded into the voucher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP response (wall)&lt;/td&gt;
&lt;td&gt;p50 1,863ms (p95 2,757)&lt;/td&gt;
&lt;td&gt;warm p50 554ms; cold/idle first call 3,163ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-chain transactions&lt;/td&gt;
&lt;td&gt;every call (30 calls = 30 tx)&lt;/td&gt;
&lt;td&gt;2 total (open + close), for all 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-call gas&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0.00144&lt;/strong&gt; (gasUsed ~86,278, paid by a relayer)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~$0&lt;/strong&gt; (open+close gas below $0.000001 in my balance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who pays the gas&lt;/td&gt;
&lt;td&gt;a facilitator relayer (you hold no ETH)&lt;/td&gt;
&lt;td&gt;you, in stablecoin, on open/close&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attempts / failures&lt;/td&gt;
&lt;td&gt;30 calls, 0 failed&lt;/td&gt;
&lt;td&gt;100 calls completed; 1 cold-start attempt failed (1 of 101 paid attempts)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three numbers, not one, because "fast" depends on what you mean by done.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;verify&lt;/strong&gt; confirms the payment is valid. In x402 that's quick (p50 269ms), but the funds haven't moved yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seller is covered&lt;/strong&gt; is the one that matters for billing. In x402 that's settle (p50 1,569ms): USDC has actually moved on-chain and the call is final. In the MPP session it's the voucher (p50 554ms warm): an accepted voucher backed by funds already in escrow, not a per-call on-chain settlement. These are different assurance mechanisms, not just different speeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-chain finality&lt;/strong&gt; is per-call for x402 (same as settle) and happens once, at close, for the session.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compared on the same definition (seller is covered), the Tempo session was about 3x faster per call: 554ms vs 1,569ms. The x402 latency is dominated by &lt;code&gt;waitUntil: "confirmed"&lt;/code&gt;, which makes each call wait for a Base block (settle p50 1,569ms, p95 2,349ms).&lt;/p&gt;

&lt;p&gt;On cost, the gap is starker. x402 settles on-chain every call, so the relayer pays ~$0.00144 of gas each time, on top of moving a $0.001 payment. The MPP session paid that gas only twice, at open and close, and even then it was too small to register against my balance. I completed 100 calls, but the cold-start failed once and was retried, so 101 paid vouchers were charged in total: $0.101 in service fees. The unused remainder of the dynamic deposit was refunded on close.&lt;/p&gt;

&lt;p&gt;One caveat I won't paper over: I billed myself 30 x402 calls, not 100, and the Tempo "~$0 gas" is a balance-delta measurement, not a per-tx receipt readout. It means the open/close gas is below $0.000001 in my wallet, not provably zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;Recorded while wiring both rails. This is the part you can't get from docs.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;x402: every call 402'd, and it wasn't the balance or the keys.&lt;/strong&gt; It was &lt;code&gt;self_send_not_allowed&lt;/code&gt;. I'd wired the bench with the payer and the &lt;code&gt;payTo&lt;/code&gt; set to the same wallet. The CDP facilitator rejects &lt;code&gt;from == to&lt;/code&gt; before it checks balance, so every call failed with a generic 402. The fix was one line: print the 402 body so the &lt;code&gt;invalidReason&lt;/code&gt; surfaces, then split the addresses. The deeper lesson is a real property of the rail. A hosted facilitator structurally blocks self-dealing, so you can't quietly settle your own $0.001 against yourself for free.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tempo: the first paid request died on an "untrusted escrow contract."&lt;/strong&gt; The CLI rejected the session challenge because the escrow it was handed (&lt;code&gt;0x4d50500…0000&lt;/code&gt;) wasn't in its trust list; it wanted &lt;code&gt;0x33b9010…24f25&lt;/code&gt;. That address is the real MPP escrow precompile, and the deposit genuinely lands there, so this wasn't a malicious server. It was a stale CLI extension. The retry auto-updated &lt;code&gt;tempo-request&lt;/code&gt; to v0.5.2 and the same call went through. Mainnet-early tooling churn, caught live.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tempo: the cold first call of a session is slow and can fail.&lt;/strong&gt; Across 101 paid attempts, exactly one failed: the cold first call, with &lt;code&gt;KeyAlreadyExists&lt;/code&gt; in a keychain precompile during gas estimation. It took 3.2s before the retry succeeded, and the other 100 calls went through. The access key was already provisioned, but opening the session tried to register it again. Worth knowing if you script this: budget a retry for the first call, and expect an idle session's next call to run 2–3s before it warms back to ~554ms.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Which rail to build on
&lt;/h2&gt;

&lt;p&gt;The measurements only matter if they change a decision. Here's the framework I'd use, and the short version is: it depends on how often your agent pays.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If your situation is…&lt;/th&gt;
&lt;th&gt;Lean&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One-shot charges (a webhook, a report, an occasional inference)&lt;/td&gt;
&lt;td&gt;x402&lt;/td&gt;
&lt;td&gt;per-call settlement is fine; the gas overhead matters less when calls are infrequent or higher-priced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tens-to-hundreds of small calls in a loop (LLM inference, metered tools, streaming)&lt;/td&gt;
&lt;td&gt;MPP session&lt;/td&gt;
&lt;td&gt;the session amortizes the chain to ~$0/call and lands at ~554ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-US business, can't use Stripe's US-only stablecoin settlement&lt;/td&gt;
&lt;td&gt;x402&lt;/td&gt;
&lt;td&gt;Tempo's biggest merchant edge doesn't apply to you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want multi-chain optionality (Base, Polygon, Arbitrum, World, Solana)&lt;/td&gt;
&lt;td&gt;x402&lt;/td&gt;
&lt;td&gt;broader chain support today&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You're building closer to an agent marketplace than a single endpoint&lt;/td&gt;
&lt;td&gt;MPP session&lt;/td&gt;
&lt;td&gt;payment QoS and pre-funded sessions fit that shape&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest Kyoto-builder conclusion: for my setup, where I already run a production x402 worker, x402 was the shorter path to monetize this quarter, and the per-call gas overhead is acceptable at the prices I charge. To bet on the high-frequency agent loop, the MPP session model is genuinely a different machine, and the numbers back it up, but its loudest selling point (Stripe USD settlement) is US-only right now. I measured two production payment paths. Which one fits is a decision about your billing shape and your geography, not about which logo wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix — reproduce this
&lt;/h2&gt;

&lt;p&gt;Everything that produced the numbers above. Nothing here is paywalled — the worker, the client, the gas/latency scripts, and a results writeup are in a public gist: &lt;a href="https://gist.github.com/bykamodev-web/e66ab462b7c727b60410dea9a5fe52c8" rel="noopener noreferrer"&gt;https://gist.github.com/bykamodev-web/e66ab462b7c727b60410dea9a5fe52c8&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At a glance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x402   30 calls · 30 Base txs · 0 failed · gas mean $0.00144 · settle p50/p95 1,569/2,349 ms
MPP    100 calls (101 paid attempts, 1 cold-start fail) · 2 Tempo txs · voucher p50/p95 554/831 ms · ~$0 gas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# x402 seller = Cloudflare Worker (exact scheme, USDC on Base), client = x402-fetch + viem&lt;/span&gt;
npx wrangler dev &lt;span class="nt"&gt;--port&lt;/span&gt; 8787
&lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30 node &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env bench.mjs      &lt;span class="c"&gt;# per-call wall / verify / settle&lt;/span&gt;
node gas.mjs &amp;lt;tx hashes&amp;gt;                  &lt;span class="c"&gt;# gas + from per tx&lt;/span&gt;

&lt;span class="c"&gt;# Tempo buyer = tempo CLI against the official RPC service (intent=session)&lt;/span&gt;
&lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100 node rpc-bench.mjs                  &lt;span class="c"&gt;# latency percentiles + balance-delta gas accounting&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measured 2026-06-14, ETH ≈ $1,667.&lt;/li&gt;
&lt;li&gt;x402: &lt;code&gt;exact&lt;/code&gt; scheme, USDC on Base mainnet, Coinbase CDP hosted facilitator.&lt;/li&gt;
&lt;li&gt;Tempo: mainnet chain 4217, &lt;code&gt;tempo&lt;/code&gt; CLI v1.8.2, &lt;code&gt;tempo-request&lt;/code&gt; extension v0.5.2, token USDC.e.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;x402 ran 30 settlements on Base, all submitted by relayer &lt;code&gt;0x93f6601151ccb08f333ab4b1cccfb1e188c0be44&lt;/code&gt;, gasUsed ~86,278 each (full tx hashes are in the bench output / EXPERIMENT-LOG). Tempo ran one 100-call session whose only on-chain transactions were open and close; the cooperative close receipt is &lt;code&gt;0x4875a71e7427b1ee2b5d6d553cbdccd446bdd63ab6329203678a980534c296d0&lt;/code&gt; on explore.tempo.xyz. Service spent: $0.101 (101 paid vouchers — 100 calls plus one failed cold-start that still consumed a voucher); the unused remainder of the dynamic deposit was refunded on close.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm KAMO, a developer in Kyoto. I write implementation logs: working code, real costs, what broke. No theory I didn't run myself.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Get every log:&lt;/strong&gt; &lt;a href="https://bykamo.substack.com" rel="noopener noreferrer"&gt;https://bykamo.substack.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce it (free):&lt;/strong&gt; &lt;a href="https://gist.github.com/bykamodev-web/e66ab462b7c727b60410dea9a5fe52c8" rel="noopener noreferrer"&gt;the bench&lt;/a&gt; — worker, client, gas/latency scripts, and a results writeup. Not paywalled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kits (in progress):&lt;/strong&gt; &lt;a href="https://ko-fi.com/bykamo" rel="noopener noreferrer"&gt;https://ko-fi.com/bykamo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio:&lt;/strong&gt; bykamo.dev&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aiagents</category>
      <category>payments</category>
      <category>x402</category>
      <category>stablecoin</category>
    </item>
    <item>
      <title>I Made 8 Book Trailers for $9.98 (They're Not Perfect, and That's the Point)</title>
      <dc:creator>bykamo</dc:creator>
      <pubDate>Sun, 14 Jun 2026 02:20:22 +0000</pubDate>
      <link>https://dev.to/bykamo/i-made-8-book-trailers-for-998-theyre-not-perfect-and-thats-the-point-3l7j</link>
      <guid>https://dev.to/bykamo/i-made-8-book-trailers-for-998-theyre-not-perfect-and-thats-the-point-3l7j</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibg1rvpgcdveaol0qdv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibg1rvpgcdveaol0qdv2.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I had eight short mystery novels and no budget for video. So I built a pipeline: turn each storyboard frame into a 5-second clip with PIKA's image-to-video, then do all the cutting, panning, titling and cross-fades locally with ffmpeg. PIKA gave me 1,500 trial credits. I burned through all of them, topped up twice at $4.99, and shipped eight 17-second trailers. Total cash out of pocket: &lt;strong&gt;$9.98&lt;/strong&gt;. The ffmpeg half cost nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Up front, so nobody feels misled: &lt;strong&gt;these trailers are not flawless.&lt;/strong&gt; Look closely and you'll find a frame where a hand isn't quite right, or a background detail that shimmers. Every one of the eight has a rough spot or two. The point of this post isn't "AI makes perfect video now" — it's the opposite. AI makes &lt;em&gt;flawed&lt;/em&gt; video, predictably, in specific places, and the whole game is knowing where it breaks and routing around those spots for $0 instead of burning credits trying to force perfection. That's how eight trailers cost ten dollars instead of a hundred.&lt;/p&gt;

&lt;p&gt;See for yourself — here's one of the eight (Book 6, 17 seconds): &lt;/p&gt;

&lt;p&gt;[&lt;a href="https://youtube.com/shorts/EZp40yhB_rs?si=ADccBnDbspgNGdAQ" rel="noopener noreferrer"&gt;https://youtube.com/shorts/EZp40yhB_rs?si=ADccBnDbspgNGdAQ&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Rough spots and all.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Eight book trailers, end-to-end AI pipeline: &lt;strong&gt;image → PIKA image-to-video → local ffmpeg&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The animation prompt is the whole trick: &lt;em&gt;"extremely subtle motion only"&lt;/em&gt; — let steam, light and petals move, freeze everything else. The moment a character's &lt;strong&gt;hands&lt;/strong&gt; move, PIKA melts them.&lt;/li&gt;
&lt;li&gt;Real cost: 1,500 free trial credits + &lt;strong&gt;$9.98&lt;/strong&gt; in top-ups. ffmpeg = $0.&lt;/li&gt;
&lt;li&gt;Audio: an original score I composed, added with the free OSS editor OpenCut — $0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They're not perfect.&lt;/strong&gt; Each trailer has 1–2 visible rough spots. The method is about &lt;em&gt;containing&lt;/em&gt; that, not eliminating it.&lt;/li&gt;
&lt;li&gt;The honest part is in &lt;em&gt;What broke&lt;/em&gt;: melted fingers, a face that turned into a horror shot, and an ffmpeg sample-aspect bug that stretched every tilt clip 2% wide until I caught it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this method does and doesn't do
&lt;/h2&gt;

&lt;p&gt;Because the framing matters before you read a single command:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It does:&lt;/strong&gt; produce watchable, atmospheric 9:16 trailers cheaply and repeatably, with every deterministic decision (timing, framing, text, transitions) under your control in ffmpeg.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't:&lt;/strong&gt; make PIKA stop mangling hands, faces, and small props. That limitation is real and this pipeline does not fix it. What it does is give you a &lt;strong&gt;$0 escape hatch&lt;/strong&gt; — animate the still instead of the motion — so a broken shot costs you nothing and never spirals into a re-roll bill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want broadcast-perfect output, this isn't it. If you want eight decent trailers for the price of a sandwich, read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  novel text (.md) + storyboard (.png)
            │
            ▼   [ image generation — done separately, Codex-driven,
   8 still scenes              one character sheet as the consistency anchor ]
   (941×1672 portrait
    or 1672×941 land.)
            │
            ▼   PIKA: upload_asset → R2 PUT → md5 verify
   generate_video
   (image_to_video, pika 2.2, 1080p, 5s,
    prompt = "extremely subtle motion only")
            │
            ▼   download each clip
   ┌─────────  LOCAL ffmpeg  ─────────┐
   │  Ken Burns (zoompan / crop)       │
   │  normalize SAR (setsar=1)         │
   │  concat 8 clips                   │
   │  burn captions (drawtext ×4)      │
   │  cross-fade to cover (xfade 0.8s) │
   └───────────────────────────────────┘
            │
            ▼
   final .mp4  (≈17–19s, 1080p30, silent)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hinge is the split: &lt;strong&gt;PIKA only animates; it never composes.&lt;/strong&gt; Every cut, pan, caption and transition is deterministic ffmpeg I can re-run for free. That's what keeps the variable cost near zero — I only pay PIKA for motion, and I pay it once per shot.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note on the upstream image step (done in ChatGPT, outside the ffmpeg kit): I first define the cast on one &lt;strong&gt;character sheet&lt;/strong&gt; — every recurring character in a single image, which becomes the consistency anchor. Then I ask for a &lt;strong&gt;vertical storyboard&lt;/strong&gt;: eight numbered panels, each with a 9:16 frame, a timecode (&lt;code&gt;0:00–0:02&lt;/code&gt; …), and a scene / action / camera note. Then I have each panel rendered as a still. The storyboard is the spec for everything downstream — framing, order and duration are all decided there. This post picks up where the eight stills exist and need to move.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;One book = 8 clips. PIKA returns 5-second 1080p clips; ffmpeg trims each to &lt;strong&gt;1.8s&lt;/strong&gt; (the final shot to 2.4s) → a 15.0s body, then a 0.8s cross-fade into the cover for ~17s total.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Animate each still on PIKA
&lt;/h3&gt;

&lt;p&gt;I drive PIKA through its &lt;strong&gt;MCP server&lt;/strong&gt;, not the REST API directly — so the steps below (&lt;code&gt;upload_asset&lt;/code&gt; → R2 PUT → md5 verify → &lt;code&gt;generate_video&lt;/code&gt;) are MCP tool calls an agent makes on my behalf, which is why the whole thing is scriptable from a chat session. Each still goes in as one image with an &lt;code&gt;image_to_video&lt;/code&gt; call (pika 2.2, 1080p).&lt;/p&gt;

&lt;p&gt;The per-shot prompt isn't hand-written — it's generated one cut at a time by reading three things together: the &lt;strong&gt;storyboard panel&lt;/strong&gt; (scene / action / camera / on-screen text), the &lt;strong&gt;actual still&lt;/strong&gt; (looked at directly — who's wearing what, what's in frame, what's in their hands), and the &lt;strong&gt;story beat&lt;/strong&gt; from the manuscript (where this moment sits emotionally). Out of that comes a four-part instruction that follows the same template every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[what's in the frame, read from the image]
+ [the ONLY things allowed to move: steam / light / petals / water]
+ [what must stay frozen: people still, hands stable, objects unchanging]
+ [one word of tone, from the story beat]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a real one — Book 6, the shot where a note is tucked against a cup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A held, still moment: a young woman in a yellow cardigan gently
tucks a small folded note against a paper cup among a row of
leaf-latte cups, Aoi watching quietly behind, the cat nearby.
Her hand and the note stay completely still and stable, the
paper unchanging. The ONLY motion is faint steam rising and a
soft flicker of warm lamp light.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the balance: almost every word tells PIKA what &lt;em&gt;not&lt;/em&gt; to move. That's the opposite of normal video prompting, and it's deliberate — PIKA's image-to-video gets &lt;em&gt;worse&lt;/em&gt; the more motion you ask for, so the prompt's real job is to fence off everything fragile (hands, small props, faces) and leave only steam, light, petals and water free. The negative prompt does the same defensively: &lt;code&gt;deformed hands, extra fingers, melting hand, morphing note, warping paper&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One hard rule: camera moves never go to PIKA.&lt;/strong&gt; Pans, tilts and zooms — the storyboard's CAMERA column — are added &lt;em&gt;later&lt;/em&gt; in ffmpeg (&lt;code&gt;zoompan&lt;/code&gt;/&lt;code&gt;crop&lt;/code&gt;, Step 2). Ask PIKA to move the camera and it warps the whole composition, so PIKA only ever adds ambient micro-motion in place; every deliberate camera move is a deterministic ffmpeg pass. Same thesis as before — the generative model does the one thing it's good at, ffmpeg does everything that has to be repeatable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Ken Burns each clip (portrait example)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;zoompan&lt;/code&gt; gives a clean slow push and, importantly, comes out at SAR 1:1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; c2.mp4 &lt;span class="nt"&gt;-t&lt;/span&gt; 1.8 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;"[0:v]fps=30,scale=1380:2400,setsar=1,&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
zoompan=z='min(zoom+0.0009,1.11)':d=1:&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1080x1920:fps=30[v]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[v]"&lt;/span&gt; &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-r&lt;/span&gt; 30 p2.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Mind the quotes. The &lt;code&gt;min(...)&lt;/code&gt; expression contains a comma, and inside &lt;code&gt;-filter_complex&lt;/code&gt; an unquoted comma is read as a filter separator — &lt;code&gt;zoompan=z=min(zoom+0.0009,1.11)&lt;/code&gt; fails with &lt;code&gt;No option name near '1:x='&lt;/code&gt;. Single-quote every expression value (&lt;code&gt;z='...'&lt;/code&gt;, &lt;code&gt;x='...'&lt;/code&gt;, &lt;code&gt;y='...'&lt;/code&gt;) and the comma is safe. (I verified this whole sequence end-to-end on the real frames before publishing; the unquoted version genuinely does not run.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a tilt-up (river surface → café) I use &lt;code&gt;crop&lt;/code&gt; with a time-driven offset — but &lt;code&gt;crop&lt;/code&gt; quietly poisons the sample aspect ratio, so it gets re-normalized immediately (see &lt;em&gt;What broke #SAR&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; c1.mp4 &lt;span class="nt"&gt;-t&lt;/span&gt; 1.8 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;"[0:v]fps=30,scale=1380:2400,setsar=1[b];&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
[b]crop=1080:1920:(in_w-1080)/2:(ih-1920)*(1-(t/1.8)):1[v]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[v]"&lt;/span&gt; &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-r&lt;/span&gt; 30 p1.mp4
ffmpeg &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; p1.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"setsar=1"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-r&lt;/span&gt; 30 &lt;span class="nt"&gt;-an&lt;/span&gt; p1fix.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3 — Concat the 8 clips
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"file %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; p1.mp4 p2.mp4 p3.mp4 p4.mp4 p5.mp4 p6.mp4 p7.mp4 p8.mp4 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; cl.txt
ffmpeg &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; concat &lt;span class="nt"&gt;-safe&lt;/span&gt; 0 &lt;span class="nt"&gt;-i&lt;/span&gt; cl.txt &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"setsar=1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-r&lt;/span&gt; 30 &lt;span class="nt"&gt;-an&lt;/span&gt; body.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4 — Burn captions, one pass per line
&lt;/h3&gt;

&lt;p&gt;Stacking multiple &lt;code&gt;drawtext&lt;/code&gt; filters in one &lt;code&gt;-vf&lt;/code&gt; string kept blowing up in the shell. So each caption is its own pass, daisy-chained — pass 1 reads &lt;code&gt;body.mp4&lt;/code&gt; and writes &lt;code&gt;s1.mp4&lt;/code&gt;, pass 2 reads &lt;code&gt;s1.mp4&lt;/code&gt;, and so on, four captions = four passes. Each caption's &lt;code&gt;alpha&lt;/code&gt; fade window is set to the duration of the cut it sits under, with a hand-rolled fade-in/hold/fade-out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;FONT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf"&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; body.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;"drawtext=fontfile=&lt;/span&gt;&lt;span class="nv"&gt;$FONT&lt;/span&gt;&lt;span class="s2"&gt;:text=Some words travel quietly.:&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
fontcolor=white:fontsize=46:x=(w-text_w)/2:y=h-460:&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
shadowcolor=black@0.8:shadowx=2:shadowy=2:&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
alpha='if(lt(t,3.7),0,if(lt(t,4.3),(t-3.7)/0.6,if(lt(t,5.2),1,if(lt(t,5.7),(5.7-t)/0.5,0))))'"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-r&lt;/span&gt; 30 &lt;span class="nt"&gt;-an&lt;/span&gt; s2.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Two portability gotchas here: &lt;code&gt;drawtext&lt;/code&gt; only exists if your ffmpeg was built &lt;code&gt;--enable-libfreetype&lt;/code&gt; (some minimal builds ship without it — check &lt;code&gt;ffmpeg -filters | grep drawtext&lt;/code&gt;), and the &lt;code&gt;fontfile&lt;/code&gt; path is OS-specific (the DejaVu path above is Linux; on macOS point it at something like &lt;code&gt;/System/Library/Fonts/Supplemental/Georgia.ttf&lt;/code&gt;). The &lt;code&gt;alpha='...'&lt;/code&gt; value is single-quoted for the same reason as the &lt;code&gt;zoompan&lt;/code&gt; expressions — those commas would otherwise split the filter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 5 — Cross-fade into the cover
&lt;/h3&gt;

&lt;p&gt;A portrait cover (1600×2560) is height-fit and center-cropped to fill 9:16 with no black bars, then &lt;code&gt;xfade&lt;/code&gt;d over the last 0.8s of the body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-loop&lt;/span&gt; 1 &lt;span class="nt"&gt;-i&lt;/span&gt; cover_v.png &lt;span class="nt"&gt;-t&lt;/span&gt; 2.8 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;"scale=-1:1920,setsar=1,crop=1080:1920:(in_w-1080)/2:0,&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
zoompan=z='min(zoom+0.0003,1.03)':d=1:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1080x1920:fps=30"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-r&lt;/span&gt; 30 &lt;span class="nt"&gt;-an&lt;/span&gt; cover_clip.mp4

ffmpeg &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; body_text.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; cover_clip.mp4 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;"[0:v]settb=AVTB,fps=30,format=yuv420p,setsar=1[a];&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
[1:v]settb=AVTB,fps=30,format=yuv420p,setsar=1[b];&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
[a][b]xfade=transition=fade:duration=0.8:offset=14.2[v]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[v]"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-r&lt;/span&gt; 30 &lt;span class="nt"&gt;-an&lt;/span&gt; final.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it cost
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PIKA image-to-video:&lt;/strong&gt; 1,500 free trial credits, fully consumed, then &lt;strong&gt;2 × $4.99&lt;/strong&gt; top-ups = &lt;strong&gt;$9.98&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ffmpeg (all editing):&lt;/strong&gt; $0 — runs locally, no per-render charge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image generation (upstream):&lt;/strong&gt; ran on a separate Codex-based workflow; not billed into this number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Music:&lt;/strong&gt; $0 cash — every trailer carries an original score I composed myself. No licensing, no library fees. ffmpeg locks the silent video; then I drop the track on in &lt;a href="https://github.com/OpenCut-app/OpenCut" rel="noopener noreferrer"&gt;OpenCut&lt;/a&gt;, a free open-source editor, and export. So the whole audio layer is $0 too: free tool, own music.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total cash out of pocket: $9.98&lt;/strong&gt; for eight finished trailers. I won't pretend the trial credits were worth literally nothing, but the only money that left my account was the two top-ups.&lt;/p&gt;

&lt;p&gt;(One honest gap: PIKA's API response doesn't return a per-job credit charge, so I can't give you a clean cost-per-clip. What I &lt;em&gt;can&lt;/em&gt; count is generations — roughly 8–9 video calls per book, because a couple of shots per book got re-rolled or rescued, not animated.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;This is the part that actually transfers. None of these got fully "solved" — they got &lt;em&gt;contained&lt;/em&gt;. That distinction is the whole method.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Moving hands and props melt.&lt;/strong&gt; The single most frequent failure. Handing over a coin, lifting a spoon to the mouth, slipping a folded note into a cup — fingers morph and the object duplicates or smears. A Gemini pass on the output flagged it &lt;em&gt;severe&lt;/em&gt; ("fingers fuse, the note becomes a warped block"). &lt;strong&gt;Fix:&lt;/strong&gt; for that one shot, don't animate it at all — drive the &lt;em&gt;original still&lt;/em&gt; through ffmpeg Ken Burns (zoom only). The hand never moves, so it can't break, and it costs zero extra credits. That's the trick that kept the bill at $9.98 instead of a re-roll spiral.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A face eating head-on turns into a horror shot.&lt;/strong&gt; One book had an elderly woman lifting a spoon while facing the camera; the animated version was genuinely unsettling. Swapped the source frame to a &lt;strong&gt;profile&lt;/strong&gt; and it resolved. Lesson: for an emotional close-up, a side angle is safer than a front angle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;crop&lt;/code&gt; silently sets SAR to 46:45 and stretches everything 2% wide.&lt;/strong&gt; The worst bug because it's invisible until you compare side by side. &lt;code&gt;zoompan&lt;/code&gt; clips come out 1:1; &lt;code&gt;crop&lt;/code&gt;-based tilt clips don't. &lt;strong&gt;Fix:&lt;/strong&gt; force &lt;code&gt;setsar=1&lt;/code&gt; on &lt;em&gt;every&lt;/em&gt; clip and verify the concatenated result with &lt;code&gt;ffprobe&lt;/code&gt; (&lt;code&gt;sample_aspect_ratio=1:1&lt;/code&gt;) before titling. Never trust the concat to inherit it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multiple &lt;code&gt;drawtext&lt;/code&gt; filters in one &lt;code&gt;-vf&lt;/code&gt; string die in the shell&lt;/strong&gt; (returncode 234) — the commas and quoting collide. &lt;strong&gt;Fix:&lt;/strong&gt; one caption per ffmpeg pass (four captions = four passes). And any comma &lt;em&gt;inside&lt;/em&gt; the text has to be escaped &lt;code&gt;\\,&lt;/code&gt; or it's read as a filter separator.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A landscape title card in a 9:16 frame = black bars.&lt;/strong&gt; Fixed by using the &lt;strong&gt;portrait&lt;/strong&gt; cover, &lt;code&gt;scale=-1:1920&lt;/code&gt; then center-crop, so it fills the frame. Rule I added: always extract the final frame and eyeball it — captions and cover text get clipped in ways the timeline doesn't show.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What I couldn't contain.&lt;/strong&gt; Being straight about the leftover rough spots: a couple of shots still have a slightly-off hand or a background element that shimmers under motion, even after routing the worst offenders to the still-image escape hatch. At trailer speed (1.8s per shot, lots of cuts) most viewers don't catch them, but they're there. I decided eight watchable trailers today beat zero perfect ones next month. Your call on where that line sits.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;There's a lot of "I made a video with AI" content, and almost none of it tells you where the seams are. The seam here is specific and reusable: &lt;strong&gt;let the generative model do only the thing it's good at — ambient motion — and move every deterministic decision (timing, framing, text, transitions) into ffmpeg, where it's free and repeatable.&lt;/strong&gt; That division is why eight trailers cost ten dollars instead of a hundred, and why the failures were containable: when a shot broke, I had a $0 escape hatch (animate the still instead) sitting right there.&lt;/p&gt;

&lt;p&gt;It's also just the texture of building things in Kyoto on a shoestring — eight little mystery novels set in a riverside café, given motion by a trial account and a text editor full of ffmpeg one-liners.&lt;/p&gt;




&lt;h3&gt;
  
  
  Want the scripts?
&lt;/h3&gt;

&lt;p&gt;I packaged the whole thing — the parametrized &lt;code&gt;make_trailer.sh&lt;/code&gt; (portrait + landscape), the PIKA shot-by-shot prompt recipes, the SAR-verification script, and a README that lays out which shots to animate vs. route to the $0 still-image escape hatch — into a kit. It assumes the same honest premise as this post: it won't make PIKA flawless, it'll help you ship watchable trailers cheaply and skip the failures I already paid for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ ffmpeg Book Trailer Kit on Ko-fi:&lt;/strong&gt; &lt;a href="https://ko-fi.com/s/a637e3c118" rel="noopener noreferrer"&gt;https://ko-fi.com/s/a637e3c118&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm KAMO, a developer in Kyoto. I write implementation logs — working code, real costs, what broke.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Get every log:&lt;/strong&gt; &lt;a href="https://bykamo.substack.com" rel="noopener noreferrer"&gt;https://bykamo.substack.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio:&lt;/strong&gt; bykamo.dev&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aivideo</category>
      <category>ffmpeg</category>
      <category>pika</category>
      <category>automation</category>
    </item>
    <item>
      <title>I Made My Website Charge AI Crawlers with HTTP 402. In 30 Days, 5,811 Came and 5 Paid.</title>
      <dc:creator>bykamo</dc:creator>
      <pubDate>Fri, 12 Jun 2026 06:48:03 +0000</pubDate>
      <link>https://dev.to/bykamo/i-made-my-website-charge-ai-crawlers-with-http-402-in-30-days-5811-came-and-5-paid-2112</link>
      <guid>https://dev.to/bykamo/i-made-my-website-charge-ai-crawlers-with-http-402-in-30-days-5811-came-and-5-paid-2112</guid>
      <description>&lt;p&gt;I run a content site, &lt;a href="https://do-and-coffee.com" rel="noopener noreferrer"&gt;do-and-coffee.com&lt;/a&gt;. Like everyone else, it gets scraped by AI crawlers. Instead of blocking them, I did something else: I put a paywall in front of the site that returns &lt;strong&gt;HTTP 402 Payment Required&lt;/strong&gt; to bots, with machine-readable payment instructions. If a crawler pays a cent in USDC, it gets the article. If it doesn't, it gets the 402 and nothing else.&lt;/p&gt;

&lt;p&gt;Then I let it run for 30 days and watched. Here's what actually happened — and it's not the number you'd put on a pitch deck.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Cloudflare Worker sits in front of the site. AI crawlers get &lt;strong&gt;402 + x402 payment requirements&lt;/strong&gt;; humans and search bots pass through free.&lt;/li&gt;
&lt;li&gt;Payment is &lt;strong&gt;USDC on Base&lt;/strong&gt;, $0.01 per article, verified and settled through Coinbase's CDP facilitator.&lt;/li&gt;
&lt;li&gt;30-day result: &lt;strong&gt;5,811 crawler requests, 5 paid, 5,806 served a 402.&lt;/strong&gt; Revenue at $0.01/article ≈ &lt;strong&gt;$0.05&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The interesting part isn't the revenue. It's &lt;em&gt;who&lt;/em&gt; paid: &lt;strong&gt;GPTBot paid 4 times out of 48 requests; ClaudeBot paid once out of 651.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;do-and-coffee.com/blog/article/*  ─▶  x402 Worker (Cloudflare)
                                       │
   has X-PAYMENT-RESPONSE? ───────────┤─▶ yes ─▶ proxy origin (200)
   KV cache hit (payer:url)? ─────────┤─▶ yes ─▶ proxy origin (200)
   no X-PAYMENT? ─────────────────────┤─▶ 402 + payment requirements
   has X-PAYMENT? ────────────────────┘
        │
        ├─▶ CDP /verify   (is the signed payment valid?)
        ├─▶ CDP /settle   (waitUntil: confirmed — on-chain)
        └─▶ on success: KV.put(payer:url, receipt, ttl 24h) ─▶ proxy origin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker speaks the &lt;strong&gt;x402&lt;/strong&gt; protocol: a 402 response carries an &lt;code&gt;accepts&lt;/code&gt; array describing exactly how to pay (scheme &lt;code&gt;exact&lt;/code&gt;, network &lt;code&gt;base&lt;/code&gt;, asset USDC, amount, &lt;code&gt;payTo&lt;/code&gt; wallet). A compliant agent reads that, signs a USDC payment, and retries with an &lt;code&gt;X-PAYMENT&lt;/code&gt; header. The worker verifies and settles it through Coinbase's facilitator, then proxies the real article.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The 402 response
&lt;/h3&gt;

&lt;p&gt;When there's no payment, the worker builds the requirements and returns 402:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildPaymentRequirements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resourceUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PaymentRequirements&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxAmountRequired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 0.01 USDC (6 decimals)&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resourceUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access to Do and Coffee premium article content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RESOURCE_WALLET_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxTimeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// USDC on Base&lt;/span&gt;
    &lt;span class="na"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD Coin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verify, then settle
&lt;/h3&gt;

&lt;p&gt;When a payment does arrive, the worker decodes the &lt;code&gt;X-PAYMENT&lt;/code&gt; header, then calls the CDP facilitator twice — once to verify the signature is valid, once to settle it on-chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settleRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FACILITATOR_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/settle`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;settleHeaders&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;x402Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;paymentPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paymentRequirements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reqs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// block until the chain confirms&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Don't charge twice
&lt;/h3&gt;

&lt;p&gt;A paid payer is cached in Workers KV for 24 hours, keyed by &lt;code&gt;payer:url&lt;/code&gt;, so a crawler that re-fetches the same article within a day isn't billed again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;settleJson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;expirationTtl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it cost / earned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Worker + KV: within Cloudflare's free tier at this volume — &lt;strong&gt;$0&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Facilitator: Coinbase CDP, no per-call fee at this scale.&lt;/li&gt;
&lt;li&gt;Revenue over 30 days: &lt;strong&gt;5 payments × $0.01 = ~$0.05&lt;/strong&gt; (the dashboard tracks counts, not USD; this is the configured price × paid count).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: net a few cents. As a business, this is nothing. As a measurement, it's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-day numbers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F405tc9a0ezubhili048b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F405tc9a0ezubhili048b.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Window: May 14 – June 12. Source: the worker's own crawl-stats dashboard.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total crawler requests&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,811&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Served 402&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,806&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Per crawler:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Crawler&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;Paid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude-SearchBot&lt;/td&gt;
&lt;td&gt;1,536&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PetalBot&lt;/td&gt;
&lt;td&gt;1,502&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClaudeBot&lt;/td&gt;
&lt;td&gt;651&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amzn-SearchBot&lt;/td&gt;
&lt;td&gt;444&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAI-SearchBot&lt;/td&gt;
&lt;td&gt;337&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPTBot&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other (long tail of minor UAs)&lt;/td&gt;
&lt;td&gt;1,293&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,811&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most-hit paths: &lt;code&gt;/blog/article/*&lt;/code&gt; (1,633), &lt;code&gt;/robots.txt&lt;/code&gt; (1,575), &lt;code&gt;/sitemap.xml&lt;/code&gt; (840).&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke / what I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Almost nothing pays.&lt;/strong&gt; 0.086% of requests resulted in a payment. The overwhelming default behavior when an AI crawler meets a 402 is to leave. If you're imagining passive USDC income from agentic traffic, the live data says: not yet, not at this volume.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The crawlers that &lt;em&gt;can&lt;/em&gt; pay still mostly don't.&lt;/strong&gt; GPTBot paid on 4 of 48 requests (~8%); ClaudeBot on 1 of 651 (~0.15%). Treat exact attributions cautiously — these are user-agent strings, which can be spoofed — but as a first-party observation, the agents presenting OpenAI's GPTBot UA were by far the most likely to actually complete an x402 payment. Everything else (search-indexing bots like Claude-SearchBot, PetalBot, OAI-SearchBot) just hammered &lt;code&gt;/robots.txt&lt;/code&gt; and &lt;code&gt;/sitemap.xml&lt;/code&gt; and bounced off the 402.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The origin has to trust the paywall.&lt;/strong&gt; The worker proxies to my real site, so my site needs to know "this request was paid." I had it mint a short-lived &lt;strong&gt;HMAC access token&lt;/strong&gt; (signed &lt;code&gt;{payer, resource, exp}&lt;/code&gt;) and pass it as an &lt;code&gt;Authorization: Bearer&lt;/code&gt; header, plus spoof a browser User-Agent so the origin returns full HTML instead of its own bot treatment. Without the browser UA, the origin's own anti-bot logic fought the paywall.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Settling synchronously costs latency.&lt;/strong&gt; &lt;code&gt;waitUntil: "confirmed"&lt;/code&gt; blocks the response until Base confirms the transfer. It's the safe choice (you serve content only after the money is real), but it adds seconds to a paid request. For a $0.01 article that's fine; for a high-frequency API it would be the first thing I'd revisit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;URL normalization matters for verify.&lt;/strong&gt; The &lt;code&gt;resource&lt;/code&gt; in the payment requirements has to match exactly on the retry, so I strip operational query params (&lt;code&gt;__x402&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;) before composing it. A mismatch there silently fails verification.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;There are a lot of essays about the "agentic web" and machines paying machines. There are very few sites actually returning 402 to real crawlers and reading the receipts. This is one of them, and the honest takeaway is: the rails work — verify, settle, USDC on Base, all fine — but the demand side isn't here yet. The number that matters today isn't the $0.05. It's the 5-out-of-5,811, and the fact that one vendor's bot pays 50× more often than another's. That ratio is the thing worth watching as this evolves.&lt;/p&gt;

&lt;p&gt;I'll keep it running and report the curve.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm KAMO, a developer in Kyoto. I write implementation logs — working code, real costs, what broke.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Get every log:&lt;/strong&gt; &lt;a href="https://bykamo.substack.com" rel="noopener noreferrer"&gt;https://bykamo.substack.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio:&lt;/strong&gt; bykamo.dev&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aiagents</category>
      <category>web3</category>
      <category>cloudflare</category>
      <category>x402</category>
    </item>
    <item>
      <title>I Bought a $100 Hat for $0 — Proving an AI Agent Was Human-Backed with World AgentKit</title>
      <dc:creator>bykamo</dc:creator>
      <pubDate>Fri, 12 Jun 2026 05:46:26 +0000</pubDate>
      <link>https://dev.to/bykamo/i-bought-a-100-hat-for-0-proving-an-ai-agent-was-human-backed-with-world-agentkit-23e0</link>
      <guid>https://dev.to/bykamo/i-bought-a-100-hat-for-0-proving-an-ai-agent-was-human-backed-with-world-agentkit-23e0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmp85490bq4pi28ow2kc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmp85490bq4pi28ow2kc.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A $100 hat showed up at my door in Kyoto. I paid nothing for it — not for the hat, not for shipping. $0.00.&lt;/p&gt;

&lt;p&gt;It wasn't a coupon I found or a sale. The store, &lt;a href="https://humanrequired.shop" rel="noopener noreferrer"&gt;humanrequired.shop&lt;/a&gt;, only gives that discount to AI agents that can &lt;em&gt;prove a real human is standing behind them&lt;/em&gt;. So I made my agent prove it, on-chain, with a zero-knowledge proof. The discount it got back was 100% off. Here's the whole path, because the design held together far better than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;World Foundation's &lt;strong&gt;AgentKit&lt;/strong&gt; lets you prove on-chain that an agent is "human-backed."&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://humanrequired.shop" rel="noopener noreferrer"&gt;humanrequired.shop&lt;/a&gt; hands a human-only &lt;strong&gt;100%-off&lt;/strong&gt; discount to verified agents — once per World ID.&lt;/li&gt;
&lt;li&gt;The official Claude Code plugin (&lt;code&gt;worldcoin/agentkit-shopify-demo&lt;/code&gt;) ships the whole flow as skills.&lt;/li&gt;
&lt;li&gt;Result: one "Human in the Loop" Hat, &lt;strong&gt;$100 → $0, shipping included&lt;/strong&gt;, delivered to Japan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Claude Code]
   ├─ plugin: agentkit-shopify
   │   ├─ skill: shopify-agent-discount  (SIWE signature → World discount API)
   │   └─ skill: shopify-storefront       (Shopify product JSON → cart URL)
   │
   ├─ Agent Wallet (local Ethereum keypair)
   │   └─ registered in AgentBook (on World Chain) = the human-backed proof
   │
   └─ Shopify (humanrequired.shop)
       └─ /api/verify gate (Worldcoin's verification endpoint)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key idea: you don't give the &lt;em&gt;agent&lt;/em&gt; a World ID. A human delegates the agent's public key on-chain. The private key never leaves the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 — Install the plugin
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add worldcoin/agentkit-shopify-demo
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;agentkit-shopify@worldcoin-agentkit
/reload-plugins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Generate an agent key
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run &lt;span class="nt"&gt;--with&lt;/span&gt; eth-account python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from eth_account import Account; print(Account.create().key.hex())"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .agent-key
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 .agent-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the wallet address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run &lt;span class="nt"&gt;--with&lt;/span&gt; eth-account python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from eth_account import Account; print(Account.from_key(open('.agent-key').read().strip()).address)"&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; 0xC56A...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3 — Register in AgentBook (the human-backed proof)
&lt;/h3&gt;

&lt;p&gt;In a &lt;strong&gt;separate terminal&lt;/strong&gt; (it shows a QR code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @worldcoin/agentkit-cli register 0xC56A...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Scan the QR in the World App.&lt;/li&gt;
&lt;li&gt;Your Orb-verified World ID generates a zero-knowledge proof.&lt;/li&gt;
&lt;li&gt;The AgentBook contract on World Chain writes &lt;code&gt;agent_address → human_nullifier&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Worldcoin's relayer covers the gas, so you pay nothing to register.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4 — Call the discount API
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;get-coupon.py&lt;/code&gt; does four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Signs a &lt;strong&gt;SIWE&lt;/strong&gt; message (Sign-In with Ethereum, EIP-4361) with the key in &lt;code&gt;.agent-key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Base64-encodes the signature into an &lt;code&gt;agentkit:&lt;/code&gt; HTTP header.&lt;/li&gt;
&lt;li&gt;POSTs the product URL to &lt;code&gt;https://discount-app.worldcoin.org/api/verify&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The server checks AgentBook — if the agent maps to a registered human, it returns a discount code.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PRIVATE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .agent-key&lt;span class="si"&gt;)&lt;/span&gt; ./get-coupon.py https://humanrequired.shop/products/human-in-the-loop-hat
&lt;span class="c"&gt;# =&amp;gt; WORLD-ID-ced1a8fe6682&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5 — Build the checkout URL
&lt;/h3&gt;

&lt;p&gt;No special API needed on the Shopify side — the plain product JSON endpoint is enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://humanrequired.shop/products/human-in-the-loop-hat.json"&lt;/span&gt; | jq &lt;span class="s1"&gt;'.product.variants[0].id'&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; 46991516106914&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shopify's standard cart permalink finishes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;https://humanrequired.shop/cart/&lt;span class="nt"&gt;&amp;lt;variant_id&amp;gt;&lt;/span&gt;:&lt;span class="nt"&gt;&amp;lt;qty&amp;gt;&lt;/span&gt;?discount=&lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;
https://humanrequired.shop/cart/46991516106914:1?discount=WORLD-ID-ced1a8fe6682
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it cost
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The hat: &lt;strong&gt;$0.00&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Shipping to Kyoto: &lt;strong&gt;$0.00&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Gas to register on World Chain: &lt;strong&gt;$0.00&lt;/strong&gt; (Worldcoin's relayer paid it)&lt;/li&gt;
&lt;li&gt;Total out of pocket: &lt;strong&gt;$0.00&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;npx ...register&lt;/code&gt; needs its own terminal.&lt;/strong&gt; It draws a QR code; running it inside Claude Code's Bash mangles the output. Run it in a real terminal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't do a test run.&lt;/strong&gt; The discount code is derived deterministically from your nullifier hash, so "let me just call it once to see" spends your one real code. I held off on hitting the API until the checkout URL was fully assembled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strip query params off the product URL.&lt;/strong&gt; A trailing &lt;code&gt;?variant=123&lt;/code&gt; can make the discount API treat it as a different product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/reload-plugins&lt;/code&gt; did nothing in my setup.&lt;/strong&gt; The skills are just bash scripts under &lt;code&gt;~/.claude/plugins/marketplaces/worldcoin-agentkit/skills/&lt;/code&gt;, so I ran them by hand and the flow still completed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;A few design choices stood out, and they're the reason I bothered writing this down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The discount code appears to be deterministic.&lt;/strong&gt; The tail of &lt;code&gt;WORLD-ID-ced1a8fe6682&lt;/code&gt; matched the tail of the nullifier hash from registration (&lt;code&gt;0x2fd8701b...a8fe6682&lt;/code&gt;). I can't see the server, but that match strongly suggests the code is derived from the nullifier rather than from a stored counter or random value — which would mean "one human, one code" is enforced cryptographically, not by a database row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The private key never goes over the wire.&lt;/strong&gt; Auth is a SIWE signature; the server does &lt;code&gt;ecrecover&lt;/code&gt; to get the signer address, then looks up the human link in AgentBook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The MCP turned out to be unnecessary.&lt;/strong&gt; The plugin registers a Shopify Storefront MCP, but the &lt;code&gt;shopify-storefront&lt;/code&gt; skill just curls Shopify's public product JSON. Easy to miss, and the right call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I spend a lot of time watching the supplier side of agentic commerce — sites that gate AI traffic. This was the rare chance to be the &lt;em&gt;consumer&lt;/em&gt; passing through one of those gates, and the gate's logic is positive: not "block AI," but "let human-backed agents through." Claude Code + plugin + external MCP + a local private key + a ZK proof + an on-chain registry, all meshing cleanly. The hat is the souvenir.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm KAMO, a developer in Kyoto. I write implementation logs — working code, real costs, what broke.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Get every log:&lt;/strong&gt; &lt;a href="https://bykamo.substack.com" rel="noopener noreferrer"&gt;https://bykamo.substack.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio:&lt;/strong&gt; bykamo.dev&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aiagents</category>
      <category>automation</category>
      <category>agenticcommerce</category>
      <category>worldcoin</category>
    </item>
  </channel>
</rss>
