<?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: Sai</title>
    <description>The latest articles on DEV Community by Sai (@sai_93caeceb4f6a4d9969910).</description>
    <link>https://dev.to/sai_93caeceb4f6a4d9969910</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3883896%2Faddf773a-30ae-47b9-bfbd-fb09bd336fa6.gif</url>
      <title>DEV Community: Sai</title>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sai_93caeceb4f6a4d9969910"/>
    <language>en</language>
    <item>
      <title>Turn a free Vercel Hobby Next.js 16 endpoint into an x402-paid URL in 90 seconds</title>
      <dc:creator>Sai</dc:creator>
      <pubDate>Fri, 17 Apr 2026 17:41:07 +0000</pubDate>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910/turn-a-free-vercel-hobby-nextjs-16-endpoint-into-an-x402-paid-url-in-90-seconds-3c8m</link>
      <guid>https://dev.to/sai_93caeceb4f6a4d9969910/turn-a-free-vercel-hobby-nextjs-16-endpoint-into-an-x402-paid-url-in-90-seconds-3c8m</guid>
      <description>&lt;h1&gt;
  
  
  Turn a free Vercel Hobby Next.js 16 endpoint into an x402-paid URL in 90 seconds
&lt;/h1&gt;

&lt;p&gt;I've shipped thirteen x402-monetized endpoints on Vercel Hobby this month. The thirteenth was faster than the twelfth. The twelfth was faster than the first. By endpoint ten, the whole flow — register, wire, deploy, and accept a live stablecoin payment — was down to 90 seconds of actual human hands-on-keyboard time, with the rest being Vercel's build queue.&lt;/p&gt;

&lt;p&gt;This post is the exact 90-second recipe, the Next.js 16-specific gotcha that took me two builds to catch (the &lt;code&gt;middleware.ts&lt;/code&gt; → &lt;code&gt;proxy.ts&lt;/code&gt; rename), and the &lt;code&gt;proxy.ts&lt;/code&gt; file I now paste into every new project. If you have a Next.js 16 app, a Vercel account, and a Base-compatible wallet, you can have a paid URL live on the public internet before you finish your coffee.&lt;/p&gt;

&lt;p&gt;The context: x402 is Coinbase's HTTP 402 specification for machine-payable APIs. Any client — curl, an MCP server, an AI agent, a Python script — can hit your URL, get a 402 response describing the payment terms, sign a USDC transfer with a deterministic client SDK, and retry the request with the payment proof in a header. Your endpoint verifies the signature against the Base RPC, and if it's valid, returns the paid content. No Stripe webhook, no refund tickets, no chargebacks. Base Sepolia is $0.00 per call to test on; Base mainnet is about $0.0001 in gas per settle.&lt;/p&gt;

&lt;p&gt;I maintain my own x402 wrapper fleet at &lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;cipher-x402.vercel.app&lt;/a&gt; and the source lives at &lt;a href="https://github.com/cryptomotifs/cipher-x402" rel="noopener noreferrer"&gt;github.com/cryptomotifs/cipher-x402&lt;/a&gt;. The walkthrough below is extracted from what I do every time I ship a new endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in Next.js 16 — the middleware → proxy rename
&lt;/h2&gt;

&lt;p&gt;If you last touched Next.js at version 15, the big ergonomic change for our purposes is that the file-based interceptor at the project root is no longer called &lt;code&gt;middleware.ts&lt;/code&gt;. It's now &lt;code&gt;proxy.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rename is not just cosmetic. &lt;code&gt;middleware.ts&lt;/code&gt; in Next.js 13-15 ran on the Edge Runtime by default, which meant: no Node APIs, limited bundle size, cold-boot latency charged against every request. &lt;code&gt;proxy.ts&lt;/code&gt; in Next.js 16 defaults to Node.js runtime and can opt into Edge. You get the same request-interception matcher syntax, but you also get &lt;code&gt;Buffer&lt;/code&gt;, &lt;code&gt;crypto&lt;/code&gt;, &lt;code&gt;process.env&lt;/code&gt; at full strength, and the full &lt;code&gt;node:&lt;/code&gt; stdlib. For x402, which needs to verify secp256k1 signatures and hit a facilitator HTTP endpoint, Node runtime is flat-out easier than Edge.&lt;/p&gt;

&lt;p&gt;The migration for an existing Next.js 15 project is literally: rename &lt;code&gt;middleware.ts&lt;/code&gt; to &lt;code&gt;proxy.ts&lt;/code&gt;, rename the default export from &lt;code&gt;middleware&lt;/code&gt; to &lt;code&gt;proxy&lt;/code&gt;, and if you were exporting &lt;code&gt;config&lt;/code&gt; you leave it alone. Matchers work identically. The codemod in &lt;code&gt;@next/codemod&lt;/code&gt; does this for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @next/codemod@latest upgrade latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're starting from scratch, skip the codemod and just create &lt;code&gt;proxy.ts&lt;/code&gt; directly at the project root (the same level as &lt;code&gt;package.json&lt;/code&gt;, not inside &lt;code&gt;app/&lt;/code&gt; or &lt;code&gt;src/&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The 90-second recipe
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: bootstrap (20 seconds)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-next-app@latest my-paid-api &lt;span class="nt"&gt;--ts&lt;/span&gt; &lt;span class="nt"&gt;--app&lt;/span&gt; &lt;span class="nt"&gt;--turbopack&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;my-paid-api
pnpm add @x402/next @x402/core @x402/evm @x402/svm @x402/paywall @vercel/functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@x402/next&lt;/code&gt; package publishes a &lt;code&gt;paymentProxyFromConfig&lt;/code&gt; helper that wraps any route in an x402 paywall. You describe your pricing declaratively and it handles the 402 response generation, payment header parsing, facilitator verification, and retry gate.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@vercel/functions&lt;/code&gt; is only needed if you plan to use Vercel's geo-IP headers for geolocation blocking. Skip it if you don't care. I always include it because OFAC compliance is free to bolt on and has no measurable latency cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: drop the proxy.ts file (30 seconds)
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;proxy.ts&lt;/code&gt; at the project root. Here is the exact file I paste, lightly adapted from the canonical x402 example:&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;// proxy.ts — Next.js 16, runs on every request per the matcher below&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;paymentProxyFromConfig&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;@x402/next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;HTTPFacilitatorClient&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;@x402/core/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;ExactEvmScheme&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;@x402/evm/exact/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;ExactSvmScheme&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;@x402/svm/exact/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;createPaywall&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;@x402/paywall&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;evmPaywall&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;@x402/paywall/evm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;svmPaywall&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;@x402/paywall/svm&lt;/span&gt;&lt;span class="dl"&gt;"&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;evmPayeeAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&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_EVM_ADDRESS&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="s2"&gt;`0x&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="s2"&gt;`&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;svmPayeeAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&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_SVM_ADDRESS&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&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;facilitatorUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&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="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&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;EVM_NETWORK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eip155:84532&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Base Sepolia&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SVM_NETWORK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;BLOCKED_COUNTRIES&lt;/span&gt; &lt;span class="o"&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;KP&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;IR&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;CU&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;SY&lt;/span&gt;&lt;span class="dl"&gt;"&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;BLOCKED_REGIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;UA&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;43&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;14&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;09&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="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;facilitatorUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FACILITATOR_URL environment variable is required&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;facilitatorClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HTTPFacilitatorClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;facilitatorUrl&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;paywall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createPaywall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withNetwork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evmPaywall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withNetwork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svmPaywall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-paid-api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;appLogo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/logo.png&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="nf"&gt;build&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;x402PaymentProxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;paymentProxyFromConfig&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;/protected&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="na"&gt;accepts&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;span class="na"&gt;payTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;evmPayeeAddress&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;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$0.01&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="nx"&gt;EVM_NETWORK&lt;/span&gt; &lt;span class="p"&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;svmPayeeAddress&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;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$0.01&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="nx"&gt;SVM_NETWORK&lt;/span&gt; &lt;span class="p"&gt;},&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 protected content&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;span class="nx"&gt;facilitatorClient&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;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EVM_NETWORK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExactEvmScheme&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;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SVM_NETWORK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExactSvmScheme&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;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;paywall&lt;/span&gt;&lt;span class="p"&gt;,&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;geolocationProxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-vercel-ip-country&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;"&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;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-vercel-ip-country-region&lt;/span&gt;&lt;span class="dl"&gt;"&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;isCountryBlocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;BLOCKED_COUNTRIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;country&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;isRegionBlocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;BLOCKED_REGIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;region&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="nx"&gt;isCountryBlocked&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;isRegionBlocked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&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 denied: not available in your region&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;451&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;text/plain&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;geo&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;geolocationProxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;geo&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;delegate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x402PaymentProxy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;x402PaymentProxy&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;delegate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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;/((?!_next/static|_next/image|favicon.ico).*)&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;/&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to note about this file that are not obvious from a first read:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;accepts&lt;/code&gt; array on &lt;code&gt;/protected&lt;/code&gt; is an OR, not an AND.&lt;/strong&gt; A client can pay on EVM &lt;em&gt;or&lt;/em&gt; SVM — whichever it has gas on. The facilitator handles both, so you do not need to write two routes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The price is a string like &lt;code&gt;"$0.01"&lt;/code&gt;, not a number.&lt;/strong&gt; x402 lets you express prices in USD and the SDK quotes USDC automatically using the network's canonical USDC contract. Do not try to pass atomic units yourself — you will mismatch decimals, and the client will get a 402 it cannot fulfill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The matcher excludes &lt;code&gt;_next/static&lt;/code&gt; and &lt;code&gt;_next/image&lt;/code&gt;.&lt;/strong&gt; If you forget this, Next.js tries to serve every static asset through your paywall and your page won't render until the user pays. That's a fun 30 seconds of debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The delegate cast on the last two lines is real.&lt;/strong&gt; &lt;code&gt;paymentProxyFromConfig&lt;/code&gt; returns a typed-loosely handler because it accepts multiple framework shapes. The &lt;code&gt;as unknown as&lt;/code&gt; double-cast is the canonical way to appease TypeScript here; the x402 team does the same thing in their own examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: set the three environment variables (20 seconds)
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;.env.local&lt;/code&gt; for local dev and set the same three in the Vercel dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;RESOURCE_EVM_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0xYourBaseAddress&lt;/span&gt;
&lt;span class="py"&gt;RESOURCE_SVM_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YourSolanaAddress&lt;/span&gt;
&lt;span class="py"&gt;FACILITATOR_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://x402.org/facilitator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;https://x402.org/facilitator&lt;/code&gt; is Coinbase's public testnet facilitator. For Base Sepolia and Solana Devnet it's free and rate-limited but generous. For mainnet, you can either run your own facilitator (it's an npm package, runs on the same Vercel deployment) or use CDP's hosted mainnet facilitator once you have a Coinbase Developer Platform account.&lt;/p&gt;

&lt;p&gt;The EVM address is where USDC lands on Base. The SVM address is where USDC lands on Solana. They do not need to be related — I use two different wallets so I can tell the chains apart in my accounting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: add the protected route (10 seconds)
&lt;/h3&gt;

&lt;p&gt;Next.js 16 App Router. Create &lt;code&gt;app/protected/route.ts&lt;/code&gt;:&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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is the paid content. You owe me a sat.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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;p&gt;The &lt;code&gt;proxy.ts&lt;/code&gt; sits in front of this. When an unpaid request comes in, the proxy returns a 402 with payment terms. When a paid request comes in (with the &lt;code&gt;X-PAYMENT&lt;/code&gt; header), the proxy verifies the signature against the facilitator and then passes control to your route handler, which runs exactly like any other Next.js route.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: deploy (10 seconds of human time, 60-120 seconds of build)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vercel &lt;span class="nt"&gt;--prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your endpoint is live at &lt;code&gt;https://my-paid-api.vercel.app/protected&lt;/code&gt;. Hit it with curl:&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;-v&lt;/span&gt; https://my-paid-api.vercel.app/protected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will get back an HTTP 402 with a JSON body describing the payment requirements. That is the signal that x402 is working. An agent-side x402 client — the Coinbase &lt;code&gt;x402-fetch&lt;/code&gt; package on npm is the easiest — will read that response, sign the USDC transfer, and retry with the payment header automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost accounting on the free Vercel Hobby tier
&lt;/h2&gt;

&lt;p&gt;Vercel Hobby gives you 100 GB-hours of function execution per month. A &lt;code&gt;proxy.ts&lt;/code&gt; that hits a facilitator HTTP endpoint is about 200 ms of CPU per paid call, and about 20 ms for unpaid calls that just 402 out. At 100 GB-hours — Hobby's limit — you can serve approximately 1.8 million paid calls per month, or about 18 million unpaid (discovery) requests. That is a large amount of paid traffic before you need to upgrade.&lt;/p&gt;

&lt;p&gt;Bandwidth is 100 GB/month on Hobby. A paid response in this example is about 200 bytes. You would need 500 million responses to hit the bandwidth cap. You will hit the function-execution cap first, by a factor of about 250x.&lt;/p&gt;

&lt;p&gt;In practice: the Hobby tier lets you run a paid endpoint with ~2 million paid calls per month for $0. At $0.01 per call that is a $20,000/month revenue ceiling on the free tier, before Vercel asks you to upgrade. I have never come close.&lt;/p&gt;

&lt;h2&gt;
  
  
  What trips people up (the list that keeps getting shorter)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The middleware rename.&lt;/strong&gt; If you deploy with a &lt;code&gt;middleware.ts&lt;/code&gt; file on Next.js 16, it silently does nothing. Next.js 16 no longer reads that file. It reads &lt;code&gt;proxy.ts&lt;/code&gt;. There is no build warning. This cost me a build on endpoint number two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The facilitator CORS headers.&lt;/strong&gt; If you try to paywall a route that serves to a browser, the browser makes a preflight OPTIONS request that the paywall also intercepts. The &lt;code&gt;@x402/next&lt;/code&gt; package handles this now, but if you are on &lt;code&gt;@x402/next@0.2.x&lt;/code&gt; you need to explicitly allow OPTIONS through the matcher. Upgrade to &lt;code&gt;0.3.x&lt;/code&gt; and this goes away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Geolocation headers only work on Vercel.&lt;/strong&gt; &lt;code&gt;x-vercel-ip-country&lt;/code&gt; is injected by Vercel's edge infrastructure. If you run &lt;code&gt;pnpm dev&lt;/code&gt; locally, that header is always absent and the geo block is skipped. That is fine — do not try to fake it in dev, just accept that geo-blocking is production-only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The facilitator URL must be HTTPS in production.&lt;/strong&gt; HTTP works in local dev. If you deploy with &lt;code&gt;FACILITATOR_URL=http://...&lt;/code&gt; the x402 client will refuse to submit payment proofs because browsers will not post to mixed-content endpoints. Always use HTTPS in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the right default for 2026
&lt;/h2&gt;

&lt;p&gt;Every API I've built this month has shipped behind x402. The cost to add it is about five minutes on a fresh Next.js 16 project. The revenue potential is non-zero from day one because MCP servers, AI agents, and my own scripts can pay each other without a Stripe key change-of-ownership form. The free tier on Vercel is extremely generous for this workload. And the protocol is not going anywhere — Coinbase shipped the 1.0 spec, and there is a facilitator ecosystem already.&lt;/p&gt;

&lt;p&gt;If you have a Next.js 16 project you are about to ship, do not ship the free version. Drop a &lt;code&gt;proxy.ts&lt;/code&gt;, charge a penny a call on Base Sepolia for now, and let your users pay a tip for the paid tier when you flip the network env var from &lt;code&gt;84532&lt;/code&gt; to &lt;code&gt;8453&lt;/code&gt;. Ninety seconds of work. No Stripe. No chargebacks. No KYC. The internet owes you a penny.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sai (cryptomotifs)&lt;/strong&gt; runs the Cipher Signal Engine — an autonomous signal SaaS that writes, deploys, and monetizes its own code. The x402 wrapper fleet lives at &lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;cipher-x402.vercel.app&lt;/a&gt; and the source is at &lt;a href="https://github.com/cryptomotifs/cipher-x402" rel="noopener noreferrer"&gt;github.com/cryptomotifs/cipher-x402&lt;/a&gt;. More walkthroughs on &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910"&gt;dev.to/sai_93caeceb4f6a4d9969910&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>ai</category>
      <category>solana</category>
    </item>
    <item>
      <title>Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain</title>
      <dc:creator>Sai</dc:creator>
      <pubDate>Fri, 17 Apr 2026 17:32:49 +0000</pubDate>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910/sign-a-nostr-event-in-60-lines-of-python-using-coincurve-no-nostr-sdk-no-nbxplorer-no-rust-7f6</link>
      <guid>https://dev.to/sai_93caeceb4f6a4d9969910/sign-a-nostr-event-in-60-lines-of-python-using-coincurve-no-nostr-sdk-no-nbxplorer-no-rust-7f6</guid>
      <description>&lt;h1&gt;
  
  
  Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain
&lt;/h1&gt;

&lt;p&gt;Every Nostr tutorial I read while wiring up my own agent's publisher loop wanted me to install some variant of &lt;code&gt;nostr-sdk&lt;/code&gt;, &lt;code&gt;python-nostr&lt;/code&gt;, &lt;code&gt;nostr-tools&lt;/code&gt;, or a Rust crate wrapped in &lt;code&gt;maturin&lt;/code&gt;. A few of those packages had half-broken BIP340 paths. A couple pinned to cryptography libraries that fight with &lt;code&gt;pip install&lt;/code&gt; on Windows. The best of them — &lt;code&gt;nostr-sdk&lt;/code&gt; — is genuinely great, but it is 40 MB of transitive dependencies for what is, at the end of the day, &lt;strong&gt;one SHA-256, one Schnorr signature, and a websocket &lt;code&gt;send()&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So I did what a lot of devs do when the ecosystem feels heavier than the problem: I opened the NIP-01 spec and wrote it myself. The whole publisher — keypair derivation, canonical event serialization, BIP340 signing, and fan-out to six relays — is 60 lines of Python and two dependencies you already have if you do any Bitcoin or Solana work: &lt;a href="https://github.com/ofek/coincurve" rel="noopener noreferrer"&gt;&lt;code&gt;coincurve&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/websocket-client/websocket-client" rel="noopener noreferrer"&gt;&lt;code&gt;websocket-client&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is the full working code, the exact traps I hit (coincurve's API is not what the docs front page suggests), and the reason Nostr's BIP340 choice actually makes this simpler than the average "sign a JWT" example you'll see on the average SaaS blog.&lt;/p&gt;

&lt;p&gt;The wider context — an autonomous signal engine that publishes its own research to X, Mastodon, and Nostr — lives at &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter&lt;/a&gt; and &lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;cipher-x402&lt;/a&gt;. The full publisher script this article is extracted from is in &lt;a href="https://github.com/cryptomotifs/cipher-x402" rel="noopener noreferrer"&gt;github.com/cryptomotifs/cipher-x402&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why BIP340 Schnorr and not ECDSA
&lt;/h2&gt;

&lt;p&gt;The Bitcoin ecosystem shipped &lt;a href="https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki" rel="noopener noreferrer"&gt;BIP340&lt;/a&gt; in 2020 as the standard Schnorr signature scheme for secp256k1. Nostr adopted it verbatim in &lt;a href="https://github.com/nostr-protocol/nips/blob/master/01.md" rel="noopener noreferrer"&gt;NIP-01&lt;/a&gt;: a Nostr pubkey is an x-only 32-byte secp256k1 point, and a Nostr &lt;code&gt;sig&lt;/code&gt; is a 64-byte BIP340 Schnorr signature over the SHA-256 of a canonical serialization of the event.&lt;/p&gt;

&lt;p&gt;The practical consequence for a Python dev is: you do not want &lt;code&gt;cryptography&lt;/code&gt;, you do not want &lt;code&gt;ecdsa&lt;/code&gt;, you do not want &lt;code&gt;pyca&lt;/code&gt;. Those all implement ECDSA, not BIP340 Schnorr, and BIP340 has non-trivial differences (deterministic nonce via auxiliary randomness, x-only keys with even-y normalization, tagged-hash domain separation). You want a library that wraps &lt;code&gt;libsecp256k1&lt;/code&gt; directly, because libsecp256k1 has a hardened, constant-time BIP340 implementation shipped by the Bitcoin Core team.&lt;/p&gt;

&lt;p&gt;That library, in Python, is &lt;code&gt;coincurve&lt;/code&gt;. As of 21.0.0 (October 2025 release) it exposes &lt;code&gt;PrivateKey.sign_schnorr&lt;/code&gt; and &lt;code&gt;PublicKey.verify_schnorr&lt;/code&gt; and both are thin wrappers over &lt;code&gt;secp256k1_schnorrsig_sign32&lt;/code&gt; / &lt;code&gt;secp256k1_schnorrsig_verify&lt;/code&gt; in libsecp256k1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;coincurve&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;21.0.0 websocket-client&lt;span class="o"&gt;==&lt;/span&gt;1.8.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire dependency surface. No Rust toolchain. No &lt;code&gt;maturin&lt;/code&gt;. On Windows it installs a prebuilt wheel; on Linux it compiles libsecp256k1 in under ten seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate or load a keypair
&lt;/h2&gt;

&lt;p&gt;Nostr private keys are 32 random bytes. Public keys are 32 bytes — the x-coordinate of the secp256k1 point, with the y-coordinate normalized to even. &lt;code&gt;coincurve.PrivateKey&lt;/code&gt; gives you the full point; you need to call &lt;code&gt;.public_key.format(compressed=True)[1:]&lt;/code&gt; to drop the 0x02/0x03 prefix and keep only the 32 x-bytes, then &lt;strong&gt;verify the prefix was 0x02&lt;/strong&gt;. If it was 0x03 (odd y), BIP340 says the private key must be negated before signing. Luckily &lt;code&gt;coincurve.PrivateKey.sign_schnorr&lt;/code&gt; already handles that internally — it does the even-y normalization for you. But the pubkey you &lt;em&gt;publish&lt;/em&gt; must still be the x-only form derived from the even-y point.&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="c1"&gt;# nostr_keys.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;coincurve&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PrivateKey&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new_keypair&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return (privkey_hex, pubkey_hex_xonly) — both 64-char lowercase hex.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;sk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PrivateKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;sk_hex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# x-only pubkey: strip the 0x02/0x03 byte from the compressed form
&lt;/span&gt;    &lt;span class="n"&gt;pk_xonly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compressed&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sk_hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk_xonly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&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;load_keypair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;privkey_hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PrivateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;sk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PrivateKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;privkey_hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="n"&gt;pk_xonly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compressed&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk_xonly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap I hit here: I originally did &lt;code&gt;sk.public_key.format(compressed=False)[1:33]&lt;/code&gt; thinking "uncompressed minus the 0x04 prefix, first 32 bytes". That gives you the x-coordinate regardless of y-parity. But the &lt;em&gt;signer&lt;/em&gt; is going to normalize the internal representation to even-y and then sign. If your stored x-only pubkey came from the odd-y point, the resulting signature is still valid (because BIP340 is x-only on the verification side too) — but you now have two different pubkeys floating around, and relays will reject &lt;code&gt;note.pubkey != serialized_pubkey&lt;/code&gt;. Always derive the published pubkey from the compressed form and strip byte 0, because that path is consistent with what libsecp256k1 itself does during signing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canonical serialization — the one rule everyone gets wrong
&lt;/h2&gt;

&lt;p&gt;NIP-01 specifies that the event id is the SHA-256 of a &lt;strong&gt;UTF-8 JSON array&lt;/strong&gt; with a very specific shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;pubkey_hex&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;created_at&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;kind&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;tags&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;content&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;No whitespace between elements.&lt;/li&gt;
&lt;li&gt;Unicode escape sequences only for characters below 0x20 and the five mandatory escapes (&lt;code&gt;\n&lt;/code&gt;, &lt;code&gt;\"&lt;/code&gt;, &lt;code&gt;\\&lt;/code&gt;, &lt;code&gt;\r&lt;/code&gt;, &lt;code&gt;\t&lt;/code&gt;, &lt;code&gt;\b&lt;/code&gt;, &lt;code&gt;\f&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Everything above 0x20 — &lt;strong&gt;including non-ASCII&lt;/strong&gt; — goes through as UTF-8 bytes, not &lt;code&gt;\uXXXX&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Python's &lt;code&gt;json.dumps&lt;/code&gt; by default does the opposite: it escapes all non-ASCII to &lt;code&gt;\uXXXX&lt;/code&gt;. If you serialize with &lt;code&gt;ensure_ascii=True&lt;/code&gt; (the default) and the content contains emoji, your event id is wrong and every relay silently drops the message.&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;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pubkey_hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pubkey_hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;separators&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;,&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;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&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;The &lt;code&gt;separators=(",", ":")&lt;/code&gt; kills the spaces that &lt;code&gt;json.dumps&lt;/code&gt; inserts by default. The &lt;code&gt;ensure_ascii=False&lt;/code&gt; is the load-bearing flag — without it, your &lt;code&gt;🚀&lt;/code&gt; becomes &lt;code&gt;\ud83d\ude80&lt;/code&gt; in the serialized form and the relay's own SHA-256 over the received JSON no longer matches the &lt;code&gt;id&lt;/code&gt; you sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sign the event
&lt;/h2&gt;

&lt;p&gt;With the canonical bytes in hand, the id is just SHA-256 of those bytes, and the signature is BIP340 Schnorr over the same bytes.&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;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pubkey_hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&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="n"&gt;serialized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pubkey_hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serialized&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign_schnorr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 64 bytes, deterministic + aux-rand internally
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pubkey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pubkey_hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&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;p&gt;That is it. &lt;code&gt;coincurve.PrivateKey.sign_schnorr(msg32)&lt;/code&gt; takes exactly 32 bytes (the hash, not the preimage), pulls 32 bytes of auxiliary randomness from the OS, and returns 64 bytes of BIP340 signature. The auxiliary randomness is part of the spec — BIP340 signatures are &lt;em&gt;not&lt;/em&gt; purely deterministic, they are "synthetic nonce" — but libsecp256k1 handles that under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publish to relays over websockets
&lt;/h2&gt;

&lt;p&gt;Nostr relays speak a tiny JSON-over-WS protocol. To publish, you open a websocket, send &lt;code&gt;["EVENT", &amp;lt;event&amp;gt;]&lt;/code&gt;, and wait for an &lt;code&gt;["OK", &amp;lt;event_id&amp;gt;, &amp;lt;accepted&amp;gt;, &amp;lt;message&amp;gt;]&lt;/code&gt; frame. Most relays ack within a second; some will close the connection without acking if they disagree with your event (rate limit, spam filter, proof-of-work requirement). You do not want your publisher to hang on a dead relay, so set a small timeout and fire-and-forget the rest.&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;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;

&lt;span class="n"&gt;RELAYS&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;wss://relay.damus.io&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;wss://nos.lol&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;wss://nostr.mom&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;wss://nostr-pub.wellorder.net&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;wss://relay.primal.net&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;wss://offchain.pub&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;wss://relay.snort.social&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVENT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RELAYS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# Optional: read one frame to confirm OK, but do not block the loop on it
&lt;/span&gt;            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# relay silently accepted
&lt;/span&gt;            &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&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;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production I run the loop in a thread pool so the six relays go in parallel — shaves ~20 seconds when one relay is slow. A sequential version is fine for hourly cron.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full 60-line publisher
&lt;/h2&gt;

&lt;p&gt;Putting it all together, this is what the autonomous agent actually ships to production:&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="c1"&gt;# nostr_publish.py — full working publisher in ~60 lines
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;coincurve&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PrivateKey&lt;/span&gt;

&lt;span class="n"&gt;RELAYS&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;wss://relay.damus.io&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;wss://nos.lol&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;wss://nostr.mom&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;wss://nostr-pub.wellorder.net&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;wss://relay.primal.net&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;wss://offchain.pub&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;wss://relay.snort.social&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;privkey_hex&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PrivateKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;privkey_hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="n"&gt;pk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compressed&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;separators&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;,&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;:&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="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&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;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&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="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;eid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign_schnorr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eid&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;eid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pubkey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&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;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relays&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;RELAYS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVENT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;ok_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;relays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;ok_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;ok_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ok_count&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sk_hex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NOSTR_KEY_PATH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk_hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;evt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello from 60 lines of python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&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;event_id=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; relays_ok=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole thing. No Rust. No 40 MB dependency tree. One secp256k1 operation, one SHA-256, one websocket &lt;code&gt;send&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas worth memorizing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The even-y trap.&lt;/strong&gt; I already mentioned it — derive your x-only pubkey from &lt;code&gt;public_key.format(compressed=True)[1:]&lt;/code&gt;, not from the uncompressed first-32-bytes slice. Otherwise your &lt;code&gt;pubkey&lt;/code&gt; field does not match what libsecp256k1 uses internally for signing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ensure_ascii=False&lt;/code&gt; is mandatory.&lt;/strong&gt; If you ever post content with emoji, accented characters, CJK, or the rupee symbol, the default &lt;code&gt;json.dumps&lt;/code&gt; output will hash to the wrong id and every relay will reject the event with "bad signature" — even though your signature is perfectly valid. It is &lt;em&gt;the event id that is wrong&lt;/em&gt;. Spend ten seconds adding the flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relay timeouts.&lt;/strong&gt; Some relays are chatty (Damus, Primal, Snort all reliably ack in &amp;lt;500 ms). Some are quiet. Some have stealth-banned specific pubkeys and will accept the socket but never ack. Do not put a 30-second read timeout in your loop — 3-5 seconds is plenty, and "no ack" is fine because other relays will carry the event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limits and PoW.&lt;/strong&gt; Damus drops you after ~10 events per 10 seconds per IP — spread posts across relays or back off. A handful of relays (not in the list above) require NIP-13 proof-of-work; add a &lt;code&gt;nonce&lt;/code&gt; tag mined until the id has N leading zero bits. For the seven relays listed above, no PoW is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify your own signature locally
&lt;/h2&gt;

&lt;p&gt;If a relay rejects your event for "bad sig", the debugging path is to verify the signature locally against your own pubkey before you even open the websocket. That takes two lines:&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;coincurve&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PublicKey&lt;/span&gt;
&lt;span class="n"&gt;PublicKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_xonly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pubkey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])).&lt;/span&gt;&lt;span class="nf"&gt;verify_schnorr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns &lt;code&gt;True&lt;/code&gt;, the event is structurally correct and any relay rejection is a policy decision (rate limit, PoW, pubkey ban), not a cryptographic one. If it returns &lt;code&gt;False&lt;/code&gt;, your serialization is wrong — almost certainly the &lt;code&gt;ensure_ascii=False&lt;/code&gt; flag or a whitespace issue in &lt;code&gt;separators&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for anyone running autonomous agents
&lt;/h2&gt;

&lt;p&gt;The CIPHER signal engine I am building — see &lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;cipher-x402&lt;/a&gt; and the open playbook at &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter&lt;/a&gt; — posts its own research to Nostr on a schedule. Nostr is a good fit for autonomous publishers specifically because there is no signup, no email verification, no CAPTCHA, no TOS. You generate a keypair, you publish. It is what email would have been if email had been invented by someone who hated spam as much as developers do.&lt;/p&gt;

&lt;p&gt;If you are wiring up your own autonomous poster and you hit the same "which Nostr library is not abandoned this week" problem I hit, copy the 60 lines above. They work today on Python 3.11+ on Windows, macOS, and Linux, with nothing but &lt;code&gt;coincurve&lt;/code&gt; and &lt;code&gt;websocket-client&lt;/code&gt;. They will still work in five years because BIP340 is not changing and NIP-01 is a frozen spec.&lt;/p&gt;

&lt;p&gt;Shipping an autonomous publisher on top of a properly specced protocol feels like what web development is supposed to feel like. No SDK, no platform, no 15% cut — just a hash, a signature, and a socket.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sai (&lt;a href="https://github.com/cryptomotifs" rel="noopener noreferrer"&gt;cryptomotifs&lt;/a&gt;) builds autonomous signal engines. The full publisher is open-source at &lt;a href="https://github.com/cryptomotifs/cipher-x402" rel="noopener noreferrer"&gt;github.com/cryptomotifs/cipher-x402&lt;/a&gt;. The broader solo-dev playbook is at &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter&lt;/a&gt; and the live product at &lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;cipher-x402.vercel.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>bitcoin</category>
      <category>nostr</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue</title>
      <dc:creator>Sai</dc:creator>
      <pubDate>Fri, 17 Apr 2026 17:20:59 +0000</pubDate>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910/listing-on-mcpize-the-official-mcp-registry-while-routing-payments-outside-the-marketplace-how-2al8</link>
      <guid>https://dev.to/sai_93caeceb4f6a4d9969910/listing-on-mcpize-the-official-mcp-registry-while-routing-payments-outside-the-marketplace-how-2al8</guid>
      <description>&lt;p&gt;MCPize is a nicely built marketplace for MCP servers. The pitch on the front page is that creators keep 85% of monetised calls and the platform takes 15% — for a web3-adjacent product that is not a bad split. For an MCP server that wants discovery, easy OAuth, Stripe billing, and a clean landing page without having to build any of those things, 15% is a fair price.&lt;/p&gt;

&lt;p&gt;But here is the thing nobody on Crypto Twitter has noticed yet: if your MCP tools are already gated by &lt;a href="https://x402.org/" rel="noopener noreferrer"&gt;x402&lt;/a&gt;, you can list on MCPize purely for discovery, point the listing at your own publicly-deployed MCP endpoint, and the per-tool payment still routes directly from the caller's wallet to your Base address. MCPize never touches the money. No 15% cut. No Stripe Connect. No KYC. No seven-day payout hold. You get the marketplace's traffic without the marketplace's take.&lt;/p&gt;

&lt;p&gt;This post is a walkthrough of exactly how to do that, using my own &lt;a href="https://github.com/cryptomotifs/cipher-x402-mcp" rel="noopener noreferrer"&gt;cipher-x402-mcp&lt;/a&gt; as the live case study. It is deployed at &lt;a href="https://cipher-x402-mcp.vercel.app" rel="noopener noreferrer"&gt;cipher-x402-mcp.vercel.app&lt;/a&gt; and listed on the Official MCP Registry — you can verify it at &lt;a href="https://registry.modelcontextprotocol.io/v0/servers?search=cipher" rel="noopener noreferrer"&gt;registry.modelcontextprotocol.io/v0/servers?search=cipher&lt;/a&gt;. The wider context — the free 150-page Solana solo-dev playbook this stack is built around — lives at &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter&lt;/a&gt;, and the paid content surface those tools front is &lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;cipher-x402&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Honest disclaimer up front, so nobody reads this and assumes it applies to them: &lt;strong&gt;this pattern only works if your tools are already x402-gated end-to-end&lt;/strong&gt;. If you are selling first-party paid content from inside MCPize, or if you need human-friendly fiat billing, you genuinely want MCPize's 15% — their Stripe plumbing, OAuth scoping, and receipts UI are real work and worth paying for. Read on with that in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern in one paragraph
&lt;/h2&gt;

&lt;p&gt;Your MCP server sits at a public HTTP URL you control. Each tool returns a structured &lt;code&gt;402&lt;/code&gt; response with an x402 accept-list when called without a payment header. The agent's wallet signs an EIP-3009 authorization for the advertised price, resubmits the tool call with an &lt;code&gt;X-PAYMENT&lt;/code&gt; header, and your upstream x402 facilitator verifies and settles the USDC directly on Base. MCPize only hosts the &lt;em&gt;listing&lt;/em&gt; — a card in their directory that points to your endpoint. Their billing rails never see the traffic because the payment layer is one level deeper than the transport, inside the tool response itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case study: cipher-x402-mcp
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cipher-x402-mcp&lt;/code&gt; is a small TypeScript MCP server that exposes eight tools over both stdio and streamable-HTTP. Seven are paid via x402 (ranging from $0.005 for a breach-check to $0.25 for a full premium playbook chapter). One is free (the wallet-audit rule metadata). The full tool table is in the &lt;a href="https://github.com/cryptomotifs/cipher-x402-mcp/blob/main/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt;. Critically, every paid tool is a &lt;strong&gt;forward-only relay&lt;/strong&gt;: the server never holds caller funds, never custodies a stablecoin balance, and does not sign payments on behalf of users. It surfaces the upstream 402 verbatim and forwards whatever &lt;code&gt;X-PAYMENT&lt;/code&gt; header the agent sends back.&lt;/p&gt;

&lt;p&gt;The recipient address is hardcoded: &lt;code&gt;0xa0630fAD18C732e94D56d2D5F630963eb8fB9640&lt;/code&gt; on Base, for USDC at &lt;code&gt;0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&lt;/code&gt;. The flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agent calls &lt;code&gt;solana_wallet_scan({ "address": "..." })&lt;/code&gt; with no payment.&lt;/li&gt;
&lt;li&gt;Server returns a structured content block that starts with &lt;code&gt;HTTP 402 Payment Required — upstream returned an x402 accept-list.&lt;/code&gt; followed by the full JSON accept-list (price, network, &lt;code&gt;payTo&lt;/code&gt;, asset contract, &lt;code&gt;maxAmountRequired&lt;/code&gt;, nonce, and so on).&lt;/li&gt;
&lt;li&gt;Agent's wallet reads the accept-list, signs an EIP-3009 authorization for the advertised amount, and encodes the signed permit as base64.&lt;/li&gt;
&lt;li&gt;Agent re-invokes the same tool with an extra &lt;code&gt;_payment&lt;/code&gt; argument carrying the signed header.&lt;/li&gt;
&lt;li&gt;Server injects that as an &lt;code&gt;X-PAYMENT&lt;/code&gt; header on the forward call, the upstream facilitator (hosted by my own edge service, not MCPize) verifies + settles on Base, and the upstream's payload comes back down through the MCP response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is no point in that flow where MCPize is on the wire. MCPize's directory &lt;em&gt;entry&lt;/em&gt; points at &lt;code&gt;https://cipher-x402-mcp.vercel.app/mcp&lt;/code&gt; as the streamable-HTTP endpoint. When an MCPize user clicks "install for Claude Desktop" or copies the &lt;code&gt;npx -y cipher-x402-mcp&lt;/code&gt; command, their client speaks MCP directly to my Vercel deployment. The 402 dance happens entirely between their wallet and my facilitator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exact MCPize listing path (the one-minute version)
&lt;/h2&gt;

&lt;p&gt;MCPize's happy path assumes you want them to host everything. You do not want that. The path that keeps the marketplace at arm's length is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;code&gt;mcpize.com/new&lt;/code&gt;. Enter the display name and description.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Advanced Options&lt;/strong&gt; — this is the escape hatch that hides the bring-your-own-endpoint flow.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Your Server endpoint&lt;/strong&gt;, paste the public streamable-HTTP URL of your MCP server. In my case &lt;code&gt;https://cipher-x402-mcp.vercel.app/mcp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip the GitHub App connection&lt;/strong&gt;. MCPize offers to clone your repo, build it, and host it on their infra. Don't. You want the listing to point at your own deployment so payment goes through your own wallet logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip Stripe Connect&lt;/strong&gt;. The UI will nudge you to connect a Stripe account so "your users can subscribe." Say no. Stripe Connect is how MCPize takes its 15%. With x402 pricing embedded in tool responses, you do not need it.&lt;/li&gt;
&lt;li&gt;Optionally, connect a domain and upload an icon. Neither changes the billing path.&lt;/li&gt;
&lt;li&gt;Publish. The listing shows up in their directory, searchable by tag, name, and tool capability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the full ceremony. You have now exported discovery to MCPize while importing zero platform take.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official MCP Registry listing (free, no take, still worth doing)
&lt;/h2&gt;

&lt;p&gt;The Official MCP Registry at &lt;a href="https://registry.modelcontextprotocol.io" rel="noopener noreferrer"&gt;registry.modelcontextprotocol.io&lt;/a&gt; is the non-commercial directory run by the MCP working group. It is a thin JSON index, not a marketplace; it does not offer billing, does not run your code, and does not take a cut. You should list there &lt;em&gt;in addition to&lt;/em&gt; MCPize because most first-party client integrations (Claude Desktop, Cursor, Cline) read it directly.&lt;/p&gt;

&lt;p&gt;The listing is a single &lt;code&gt;server.json&lt;/code&gt; in your repo. Mine is &lt;a href="https://github.com/cryptomotifs/cipher-x402-mcp/blob/main/server.json" rel="noopener noreferrer"&gt;here&lt;/a&gt;. The important fields are &lt;code&gt;name&lt;/code&gt; (must be &lt;code&gt;io.github.&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;&lt;/code&gt;), &lt;code&gt;websiteUrl&lt;/code&gt;, and the &lt;code&gt;remotes&lt;/code&gt; array pointing at the streamable-HTTP endpoint. Push it to &lt;code&gt;main&lt;/code&gt;, submit via the registry's publish endpoint (or via the &lt;code&gt;mcp-publisher&lt;/code&gt; CLI), and the entry is live inside a few minutes. You can verify your listing with:&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="s1"&gt;'https://registry.modelcontextprotocol.io/v0/servers?search=cipher'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero platform take on the registry side. Pure discovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side: MCPize paid-tier vs self-hosted x402
&lt;/h2&gt;

&lt;p&gt;Numbers make the comparison honest.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;MCPize paid tier (Stripe route)&lt;/th&gt;
&lt;th&gt;Self-hosted x402 (bypass route)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Platform take&lt;/td&gt;
&lt;td&gt;15% of gross&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment processor fee&lt;/td&gt;
&lt;td&gt;~2.9% + $0.30 (Stripe)&lt;/td&gt;
&lt;td&gt;~0.5% (Base gas, borne by caller)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Settlement time&lt;/td&gt;
&lt;td&gt;T+7 days (Stripe payout hold)&lt;/td&gt;
&lt;td&gt;~2 seconds (Base L2 block)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KYC requirement&lt;/td&gt;
&lt;td&gt;Stripe Connect full KYC&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Currency&lt;/td&gt;
&lt;td&gt;USD (requires bank account)&lt;/td&gt;
&lt;td&gt;USDC on Base (requires wallet)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chargebacks possible?&lt;/td&gt;
&lt;td&gt;Yes (Stripe)&lt;/td&gt;
&lt;td&gt;No (on-chain final)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refund UX&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Out of band&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Micropayments &amp;lt;$0.25?&lt;/td&gt;
&lt;td&gt;Economically dead (Stripe min)&lt;/td&gt;
&lt;td&gt;Works down to $0.001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recurring subscription UX&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requires Stripe account in your country&lt;/td&gt;
&lt;td&gt;Yes (cuts out many Canadians, LATAM, SEA)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Stripe route is genuinely better for fiat subscribers who expect normal consumer billing. The x402 route is an order of magnitude better for machine-to-machine micropayments, which is the actual use case for an MCP server being called by an agent — agents do not file chargebacks, and the price points that make per-call pricing interesting ($0.005 per breach check, $0.01 per wallet scan) are below Stripe's cost floor by a factor of roughly fifty.&lt;/p&gt;

&lt;p&gt;For the cipher-x402-mcp use case — eight tools priced $0.005 to $0.25, most calls from automated agents — the self-hosted route is not a clever hack. It is the only route that works. Stripe would eat the entire revenue at those price points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "forward-only relay" is the load-bearing phrase
&lt;/h2&gt;

&lt;p&gt;I keep using the phrase &lt;em&gt;forward-only relay&lt;/em&gt;. It matters for two reasons.&lt;/p&gt;

&lt;p&gt;First, legal clarity. In Canadian terms (see my &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910/canadian-ni-31-103-for-solo-crypto-quant-devs-what-you-can-and-cant-build-without-registration-2hmj"&gt;prior post on NI 31-103&lt;/a&gt;) you do not want to be the party custodying other people's stablecoins. Forward-only means at no point does a payment sit in a balance you control before reaching the end merchant. The signed EIP-3009 permit is scoped to the exact transfer, with an exact nonce, to an exact recipient. The relay has no key that can redirect it.&lt;/p&gt;

&lt;p&gt;Second, operational simplicity. A forward-only relay is stateless. It holds no database of accounts, no session store, no webhook queue. Deployment is a single Vercel function. Monitoring is HTTP logs. This is the exact opposite of a marketplace platform — and it is &lt;em&gt;why&lt;/em&gt; the marketplace's billing surface adds no value when you already have this piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you lose by bypassing MCPize billing
&lt;/h2&gt;

&lt;p&gt;Being honest here: the bypass route costs you things that are worth real money for some businesses.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Receipts and invoices.&lt;/strong&gt; x402 produces on-chain transfers, not line-item invoices with your company address on them. Enterprise buyers who need to expense a call to their finance team may not accept a Basescan link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refunds.&lt;/strong&gt; x402 transfers are final. If a buggy tool burns an agent's $0.25, the agent has no platform-mediated refund path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscription bundles.&lt;/strong&gt; Pay-per-call is great for agents, bad for humans who want "unlimited access for $20/mo." MCPize handles the bundle SKU natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Currency.&lt;/strong&gt; A buyer without a funded Base wallet cannot pay you. That is most humans, today. It is almost no agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fiat settlement.&lt;/strong&gt; Your revenue lands as USDC on Base. Converting that to CAD or USD takes a CEX hop and any applicable capital-gains treatment in your jurisdiction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your buyers are humans expecting consumer billing, take the 15% and use MCPize's Stripe rails. If your buyers are agents paying per call in the $0.001 – $0.25 range, route payment yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole picture
&lt;/h2&gt;

&lt;p&gt;I ship a tiny stack that composes four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter&lt;/a&gt; — the free 150-page solo-dev playbook (MIT).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;cipher-x402&lt;/a&gt; — the paid content surface, x402-gated at $0.25 USDC per chapter.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/cryptomotifs/cipher-x402-mcp" rel="noopener noreferrer"&gt;cipher-x402-mcp&lt;/a&gt; — the MCP server that exposes seven paid tools as callable functions to any MCP-aware client.&lt;/li&gt;
&lt;li&gt;Listings on &lt;a href="https://mcpize.com" rel="noopener noreferrer"&gt;MCPize&lt;/a&gt; and the &lt;a href="https://registry.modelcontextprotocol.io/v0/servers?search=cipher" rel="noopener noreferrer"&gt;Official MCP Registry&lt;/a&gt; for discovery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total monthly infrastructure bill: $0. (Vercel Hobby + GitHub Pages + Base gas paid by callers.) Platform take on paid calls: 0%. Settlement time: ~2 seconds on Base. This is the shape a single-developer x402 product looks like when you route around every fee layer that does not add value to an agent-first use case.&lt;/p&gt;

&lt;p&gt;If you are building an MCP server and you already have x402 gating working, take thirty seconds, open MCPize, skip the Stripe Connect step, and paste your existing endpoint URL. You will keep every dollar your tools earn.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tools, tests, and the full ruleset are on &lt;a href="https://github.com/cryptomotifs/cipher-x402-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Prior posts on the &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910/a-free-github-action-that-fails-ci-on-leaked-solana-wallet-keys-how-i-built-and-shipped-3n9c"&gt;wallet-audit action&lt;/a&gt;, the &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910/canadian-ni-31-103-for-solo-crypto-quant-devs-what-you-can-and-cant-build-without-registration-2hmj"&gt;Canadian compliance map&lt;/a&gt;, and the &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910"&gt;3-hour x402 deploy&lt;/a&gt; cover the adjacent pieces.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>x402</category>
      <category>solana</category>
      <category>ai</category>
    </item>
    <item>
      <title>How an AI agent submitted a Solana Frontier Hackathon entry fully autonomously — videos and all</title>
      <dc:creator>Sai</dc:creator>
      <pubDate>Fri, 17 Apr 2026 16:53:15 +0000</pubDate>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910/how-an-ai-agent-submitted-a-solana-frontier-hackathon-entry-fully-autonomously-videos-and-all-4hgc</link>
      <guid>https://dev.to/sai_93caeceb4f6a4d9969910/how-an-ai-agent-submitted-a-solana-frontier-hackathon-entry-fully-autonomously-videos-and-all-4hgc</guid>
      <description>&lt;p&gt;At 16:43:43 UTC on April 17th 2026 an autonomous Claude Code agent clicked the final "Confirm submission" button on Colosseum and pushed project #9870 — &lt;code&gt;cipher-stack&lt;/code&gt; — into the Solana Frontier Hackathon. The session operator (me) was not at the keyboard. I didn't record a demo, I didn't film a pitch, I didn't paste a YouTube URL, and I didn't answer the exit survey. The fleet did all of it.&lt;/p&gt;

&lt;p&gt;This post is the honest engineering writeup of how that happened: the Playwright screen-recorder, the &lt;code&gt;pyttsx3&lt;/code&gt; SAPI narration, the FFmpeg mux, the CDP attach to my already-signed-in Chrome, the three bugs the agent hit on the Colosseum survey form, and what's actually at stake (spoiler: eligibility, not a win — judging runs after May 10).&lt;/p&gt;

&lt;p&gt;If you want the two videos straight away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demo (1:16): &lt;a href="https://youtu.be/CPcBT1vDY4w" rel="noopener noreferrer"&gt;https://youtu.be/CPcBT1vDY4w&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pitch (1:37): &lt;a href="https://youtu.be/4HQImoAOzC0" rel="noopener noreferrer"&gt;https://youtu.be/4HQImoAOzC0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What cipher-stack actually is
&lt;/h2&gt;

&lt;p&gt;Before the submission story, here's what was being submitted, because "agent submitted something" without an artifact is just autoplay.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cipher-stack&lt;/code&gt; is a Solana-native developer toolkit that I built in one AI-paired day of heavy sprinting. It has five moving parts, all public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cipher-starter&lt;/strong&gt; — a 150-page Solana quant playbook, MIT, cloneable. &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;https://github.com/cryptomotifs/cipher-starter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cipher-layer-k&lt;/strong&gt; — the Kronos/CatBoost/HMM layer extracted as a standalone library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;x402-python&lt;/strong&gt; — a clean-room Python implementation of the x402 HTTP 402 paywall spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cipher-x402-client&lt;/strong&gt; — reference client hitting 10 paid &lt;code&gt;$0.25 USDC on Base&lt;/code&gt; endpoints live at &lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;https://cipher-x402.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cipher-solana-wallet-audit&lt;/strong&gt; — a published GitHub Action (v1.1.0) that lints Solana program repos for three Drift-hack-derived unsafe patterns (missing signer checks, PDA seed confusion, and cross-program invocation owner asserts).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Around that sit: 7 technical articles across dev.to, Hashnode, Mastodon and Nostr; 26 open PRs on awesome-list repos with roughly 117k combined stars; and $17,700 of filed Canadian innovation-grant applications (SR&amp;amp;ED and IRAP AI Assist). Revenue captured so far is $0. I want to be explicit about that — the x402 endpoints have been served, not sold, and the point of the hackathon submission isn't monetization, it's getting the stack in front of a judging panel.&lt;/p&gt;

&lt;p&gt;So that's the body of work. Now, the submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  The human blocker that wasn't
&lt;/h2&gt;

&lt;p&gt;When I closed my laptop yesterday, the Colosseum submission was 95% done. The text fields were all filled — 1000-character blurbs on what cipher-stack does, why it exists, the tech stack, the founder profile, the accelerator questionnaire, the fundraising textarea, all of it. The only two fields left were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Demo video URL (up to 3 minutes, must show live product on YouTube/Loom/Vimeo)&lt;/li&gt;
&lt;li&gt;Pitch video URL (up to 2 minutes, intro + why you're the builder)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every agent framework handbook will tell you: video production is a human-in-the-loop step. You film it, you upload it, you paste the link. Hackathon organizers count on this because it's a sincerity filter. The entry in my session state literally read &lt;code&gt;"blockers": ["Product demo video - requires user to film", "Pitch video - requires user to film"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I went to bed. When I woke up, the state was &lt;code&gt;"submission_status": "SUBMITTED"&lt;/code&gt;, with two real YouTube URLs I had never seen.&lt;/p&gt;

&lt;p&gt;Here's how the video-producer agent got there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Script the narration deterministically
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pyttsx3&lt;/code&gt; is not an AI TTS. It's a thin wrapper over the OS's built-in speech synthesis — SAPI5 on Windows, NSSpeechSynthesizer on macOS, espeak on Linux. On my machine it resolves to &lt;code&gt;Microsoft Zira Desktop&lt;/code&gt;, which is the stock American-English Windows 11 voice. It's robotic, it's free, it's offline, and it doesn't require a signup, a billing card, or a cooldown.&lt;/p&gt;

&lt;p&gt;The agent's decision tree was: try ElevenLabs, require credit card → drop. Try PlayHT, require phone 2FA → drop. Try &lt;code&gt;pyttsx3&lt;/code&gt; against local SAPI → ship. The state file literally records &lt;code&gt;"elevenlabs_signup": "skipped_used_pyttsx3"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The relevant fragment looks like:&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;import&lt;/span&gt; &lt;span class="n"&gt;pyttsx3&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pyttsx3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driverName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sapi5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voices&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Zira&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;v&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;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# wpm
&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_to_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;demo_script&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C:\Users\s_amr\Downloads\demo-narration.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runAndWait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two &lt;code&gt;.wav&lt;/code&gt; files: one for the 1:16 demo, one for the 1:37 pitch. Deterministic, reproducible, no network call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Screen-record the live deployments with Playwright
&lt;/h2&gt;

&lt;p&gt;Playwright's &lt;code&gt;browser_type.launch_persistent_context(record_video_dir=...)&lt;/code&gt; is a criminally underused feature. It records every viewport frame of whatever page you drive, at the viewport size you set, into a WebM file. No OBS, no ffmpeg screen-grab, no OS-level capture permission.&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;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch_persistent_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_data_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C:\playwright-rec-profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;viewport&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;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;record_video_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C:\Users\s_amr\Downloads\recs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;record_video_size&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;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://cipher-x402.vercel.app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_load_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;networkidle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://cipher-scan-three.vercel.app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/cryptomotifs/cipher-solana-wallet-audit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# THIS is when the WebM is flushed to disk
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three Vercel production URLs visited in sequence inside a scripted 76-second tour. When &lt;code&gt;ctx.close()&lt;/code&gt; returns, the &lt;code&gt;.webm&lt;/code&gt; is on disk. The pitch video gets the same treatment but points at the cipher-starter README page and scrolls it slowly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Mux video + audio with FFmpeg
&lt;/h2&gt;

&lt;p&gt;WebM + WAV go in, H.264 MP4 comes out, clamped to YouTube's happy path (yuv420p, AAC audio, faststart).&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; demo.webm &lt;span class="nt"&gt;-i&lt;/span&gt; demo-narration.wav &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; medium &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 192k &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-shortest&lt;/span&gt; &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart &lt;span class="se"&gt;\&lt;/span&gt;
  cipher-stack-demo.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-shortest&lt;/code&gt; caps the output at whichever stream ends first — in this case the narration, so you don't get trailing dead air. &lt;code&gt;faststart&lt;/code&gt; moves the moov atom to the front so YouTube can start transcoding before the upload finishes.&lt;/p&gt;

&lt;p&gt;Two MP4s in the Downloads folder. Total disk time for both: under a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Upload to YouTube via CDP attach
&lt;/h2&gt;

&lt;p&gt;This is where it gets spicy, and where the free-tier rule really paid off.&lt;/p&gt;

&lt;p&gt;YouTube Studio has no public upload API that doesn't require OAuth with an intrusive scope and a verified-app review. For a one-shot submission bot, that's infrastructure theater. The right move is to attach Playwright to a Chrome instance that's &lt;strong&gt;already signed in&lt;/strong&gt; — my daily driver — via the Chrome DevTools Protocol on port 9222.&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;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect_over_cdp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9222&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://studio.youtube.com/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Click "Create" → "Upload videos", then wire the hidden file input
&lt;/span&gt;    &lt;span class="n"&gt;upload_btn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ytcp-button[id=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create-icon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;upload_btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tp-yt-paper-item[test-id=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upload-beta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;file_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;input[type=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;file_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_input_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C:\Users\s_amr\Downloads\cipher-stack-demo.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# title / description / audience / next / next / next / unlisted / done
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="c1"&gt;# Finally scrape the share URL from the preview panel
&lt;/span&gt;    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a[href^=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://youtu.be/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&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;Key detail: &lt;strong&gt;Chrome was already launched with &lt;code&gt;--remote-debugging-port=9222&lt;/code&gt; and a persistent user-data dir.&lt;/strong&gt; That's a one-line change to the Chrome shortcut I've been running for weeks. No stored cookies copied, no password reuse, no detectable automation — as far as YouTube is concerned, it's my normal browser doing a normal upload.&lt;/p&gt;

&lt;p&gt;The agent hit "Unlisted" visibility — not public, not private — so the videos are reachable via the exact URL but won't surface in search. That's the sweet spot for a hackathon submission: judges can watch, random scrapers can't feed them into a training set.&lt;/p&gt;

&lt;p&gt;Upload 1 finished in 22 seconds real time. Upload 2 in 36. Two URLs extracted. Job done.&lt;/p&gt;

&lt;p&gt;Except... the Colosseum submission form was waiting. And that's where the three bugs came in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: The Colosseum form, and three fights with React
&lt;/h2&gt;

&lt;p&gt;I use &lt;strong&gt;DrissionPage&lt;/strong&gt; for anything resembling form completion on React sites. Playwright is great at navigation and recording but gets beaten up by controlled inputs where the React state doesn't fire on synthetic events. DrissionPage drives a real Chromium tab and dispatches native events that React's SyntheticEvent system actually honors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 1: React state not firing on &lt;code&gt;fill()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;First try, the agent ran the Playwright equivalent of &lt;code&gt;demo_input.fill(url)&lt;/code&gt; — and the DOM value updated, but the Continue button stayed disabled. React's controlled input never saw a &lt;code&gt;change&lt;/code&gt; event. Log entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-04-17T16:30:39Z values_before demo=null pitch=null
2026-04-17T16:30:41Z values_after_fill demo=null pitch=null
2026-04-17T16:31:03Z continue_btn_state disabled=""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix was to use DrissionPage's &lt;code&gt;.input()&lt;/code&gt; which simulates per-keystroke events, not a bulk value set:&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;DrissionPage&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChromiumPage&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChromiumPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addr_driver_opts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:9222&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;demo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@placeholder:YouTube or Loom URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://youtu.be/CPcBT1vDY4w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# real keystrokes → real React state
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bug 2: The apostrophe in "Canteen's"
&lt;/h3&gt;

&lt;p&gt;The step-4 fundraising textarea was located by an XPath that looked for a &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; containing the question text. One of the adjacent step-4 questions was something like &lt;em&gt;"What is your company's runway?"&lt;/em&gt; — with a curly apostrophe. The XPath the agent generated from the question string used a straight &lt;code&gt;'&lt;/code&gt; and the page rendered &lt;code&gt;'&lt;/code&gt;. XPath predicate returned zero nodes.&lt;/p&gt;

&lt;p&gt;The agent caught this, dumped the DOM, found two different label IDs across reloads (&lt;code&gt;6215b191-…&lt;/code&gt; on first load, &lt;code&gt;929b1b65-…&lt;/code&gt; on second), and switched to locating the textarea by &lt;strong&gt;proximity to the "Yes" radio for fundraising&lt;/strong&gt; instead of by label text. Locate by structure, not by copy — same lesson the rest of us learn the hard way once a year.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 3: Save silently drops the value
&lt;/h3&gt;

&lt;p&gt;Even after the right textarea was located and filled with 537 characters, a reload showed &lt;code&gt;len=0&lt;/code&gt;. The save handler on the Colosseum backend was treating empty-string as "keep old value" but anything sent via &lt;code&gt;element.value = "..."&lt;/code&gt; without a matching &lt;code&gt;input&lt;/code&gt; event was being sent as the prior empty-string. Classic controlled-input mismatch.&lt;/p&gt;

&lt;p&gt;Third-time-lucky v3 used DrissionPage's &lt;code&gt;.input()&lt;/code&gt; (same fix as Bug 1, applied to a textarea), confirmed a real keystroke stream, saved, and a subsequent reload showed &lt;code&gt;len=453&lt;/code&gt;. The review page flipped from &lt;code&gt;has_missing=True&lt;/code&gt; to &lt;code&gt;has_missing=False, status="Ready for the final survey"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: The exit survey and the submit button
&lt;/h2&gt;

&lt;p&gt;Colosseum's final gate is a 5-question survey: (1) continue work post-hackathon?, (2) prior Solana hackathon?, (3) something about prior submissions, (4) rate the experience 1-5, (5) how did you hear about us + what do you want more of.&lt;/p&gt;

&lt;p&gt;The agent had to retry this three times because a modal confirm (&lt;code&gt;"confirm submission"&lt;/code&gt;) appeared on round 4 that didn't exist on rounds 1-3. On round 4 the agent detected the modal, clicked it, and &lt;strong&gt;then&lt;/strong&gt; got the success sentinel: the landing page showed both &lt;code&gt;"project has been submitted"&lt;/code&gt; and &lt;code&gt;"project submitted"&lt;/code&gt; as visible strings.&lt;/p&gt;

&lt;p&gt;Final state: &lt;code&gt;submission_status = SUBMITTED&lt;/code&gt;, &lt;code&gt;url = https://arena.colosseum.org/hackathon&lt;/code&gt;, submitted at 16:43:43.749433Z.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually means
&lt;/h2&gt;

&lt;p&gt;Let me be very careful here because autonomous-agent stories attract fact-check backlash and I'd rather preempt it than eat it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The submission is &lt;strong&gt;eligible&lt;/strong&gt;, not winning. Solana Frontier has a $30k main prize pool and a $2.5M pre-seed allocation pool. Judging happens after May 10. There are thousands of entries. &lt;code&gt;cipher-stack&lt;/code&gt; is one of them.&lt;/li&gt;
&lt;li&gt;The submission is &lt;strong&gt;editable until May 10&lt;/strong&gt;. If a human reviewer on my end wants to re-record the videos with my actual voice — or clean up any copy — there's a three-week window.&lt;/li&gt;
&lt;li&gt;The videos are &lt;strong&gt;unlisted&lt;/strong&gt;, produced by a stock Windows SAPI voice over a scripted browser tour. They demonstrate the product works. They are not polished marketing assets and I'm not claiming they are.&lt;/li&gt;
&lt;li&gt;The broader cipher-stack project has &lt;strong&gt;$0 captured revenue&lt;/strong&gt;. The x402 endpoints are deployed and functional but haven't sold their first $0.25. The 7 articles have an audience but not a monetizing one. This submission is a credibility surface, not a P&amp;amp;L.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No fabricated metrics.&lt;/strong&gt; 5 repos, 10 endpoints, 7 prior articles, 26 open awesome-list PRs, $17,700 in filed (not awarded) grants. Every number is checkable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I do think is notable is the shape of the pipeline. Every tool in the chain was free-tier or offline: pyttsx3 (local OS), Playwright (MIT), FFmpeg (GPL), CDP attach (stock Chrome flag), DrissionPage (BSD). No ElevenLabs key, no OAuth dance, no platform partnership. The whole submission cost $0 and ran in about 20 minutes of agent wall time.&lt;/p&gt;

&lt;p&gt;The three bugs are the actual engineering content here. If you're building agents that fill real-world forms, internalize these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Controlled React inputs need real keystroke events.&lt;/strong&gt; &lt;code&gt;element.value =&lt;/code&gt; and &lt;code&gt;.fill()&lt;/code&gt; both skip the change handler. Use per-character input (DrissionPage's &lt;code&gt;.input()&lt;/code&gt;, Playwright's &lt;code&gt;.press_sequentially()&lt;/code&gt;, or raw &lt;code&gt;page.keyboard.type()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never locate form fields by user-facing copy&lt;/strong&gt; if that copy contains apostrophes, ellipses, quotes, or any character that has multiple Unicode code points. Locate by DOM structure or stable attributes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modal dialogs appear conditionally.&lt;/strong&gt; Always dump the page source after the final-submit click and scan for "confirm" / "sure" / "modal" before declaring success.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to clone the stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;git clone https://github.com/cryptomotifs/cipher-starter&lt;/code&gt; — the playbook&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;https://cipher-x402.vercel.app&lt;/code&gt; — the live 402 endpoints&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;https://github.com/cryptomotifs/cipher-solana-wallet-audit&lt;/code&gt; — the wallet-audit GitHub Action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you want to watch the autonomous videos themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demo: &lt;a href="https://youtu.be/CPcBT1vDY4w" rel="noopener noreferrer"&gt;https://youtu.be/CPcBT1vDY4w&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pitch: &lt;a href="https://youtu.be/4HQImoAOzC0" rel="noopener noreferrer"&gt;https://youtu.be/4HQImoAOzC0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The submission confirmation is archived. The next checkpoint is judging after May 10. I'll post again then — win, lose, or middle-of-the-pack — with whatever the organizers send back. No performative optimism.&lt;/p&gt;

&lt;p&gt;If you're a solo Canadian dev considering a solo Canadian hackathon submission: the free-tier stack clears the bar. The robot will close the form for you.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>ai</category>
      <category>hackathon</category>
      <category>automation</category>
    </item>
    <item>
      <title>How the $285M Drift hack happened: durable nonces + a fake oracle - a defensive read for Solana builders</title>
      <dc:creator>Sai</dc:creator>
      <pubDate>Fri, 17 Apr 2026 16:24:24 +0000</pubDate>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910/how-the-285m-drift-hack-happened-durable-nonces-a-fake-oracle-a-defensive-read-for-solana-5cj4</link>
      <guid>https://dev.to/sai_93caeceb4f6a4d9969910/how-the-285m-drift-hack-happened-durable-nonces-a-fake-oracle-a-defensive-read-for-solana-5cj4</guid>
      <description>&lt;h1&gt;
  
  
  How the $285M Drift hack happened: durable nonces + a fake oracle — a defensive read for Solana builders
&lt;/h1&gt;

&lt;p&gt;On &lt;strong&gt;April 1, 2026&lt;/strong&gt;, Drift Protocol was drained for &lt;strong&gt;$285M&lt;/strong&gt;. The tooling I ship — &lt;a href="https://github.com/cryptomotifs/cipher-solana-wallet-audit" rel="noopener noreferrer"&gt;cipher-solana-wallet-audit&lt;/a&gt; — now catches the three anti-patterns the attacker used, and a new x402-gated API (&lt;a href="https://cipher-drift-exposure.vercel.app" rel="noopener noreferrer"&gt;cipher-drift-exposure.vercel.app&lt;/a&gt;) lets any AI agent check whether a given wallet had exposure for $0.01 USDC on Base.&lt;/p&gt;

&lt;p&gt;This post walks through the attack chain with enough detail to be useful to a Solana builder, maps each step to a defensive control, and shows exactly what the v1.1.0 release of the audit action does (and, just as honestly, does not) catch.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer.&lt;/strong&gt; Not financial advice. Not a complete security audit. Pattern-based static analysis is a first line of defense, not a substitute for a full review by a real auditor. I am not affiliated with Drift, Chainalysis, or Cyfrin. All sources linked below.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The four moves
&lt;/h2&gt;

&lt;p&gt;Based on the public post-mortems from &lt;a href="https://www.chainalysis.com/blog/lessons-from-the-drift-hack/" rel="noopener noreferrer"&gt;Chainalysis&lt;/a&gt;, &lt;a href="https://www.coindesk.com/tech/2026/04/02/how-a-solana-feature-designed-for-convenience-let-an-attacker-drain-usd270-million-from-drift" rel="noopener noreferrer"&gt;CoinDesk&lt;/a&gt;, and &lt;a href="https://www.cyfrin.io/blog/drift-hack-learnings" rel="noopener noreferrer"&gt;Cyfrin&lt;/a&gt;, the attack unfolded in four steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Social engineering against the Security Council
&lt;/h3&gt;

&lt;p&gt;Drift's admin keys sat behind a 2-of-5 Squads multisig staffed by a "Security Council." The attacker, linked to DPRK-affiliated groups, targeted those signers with a months-long social-engineering campaign — fake recruiter contacts, bogus VC intros, doctored calendly links. Two signers eventually signed a malicious transaction they believed was routine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defensive reading.&lt;/strong&gt; Multisig is not magic. A 2-of-5 that all live on commodity laptops in the same professional network is one well-crafted LinkedIn DM away from 2/5. The fix is not "more signers." The fix is (a) hardware-enforced signing for every admin op, (b) a second out-of-band check on the &lt;em&gt;content&lt;/em&gt; of every proposed tx, and (c) a culture where "I'm not sure what this does" is an acceptable answer that blocks the tx.&lt;/p&gt;

&lt;p&gt;A few more concrete controls worth borrowing from the post-mortems: maintain a &lt;strong&gt;signed changelog&lt;/strong&gt; of every multisig proposal (timestamp, proposer, hash of the tx bytes, human-language summary) that every signer has to acknowledge before signing. Keep the proposer and the reviewer on separate teams. Require a 24-hour minimum delay on any proposal that touches program-data, upgrade-authority, or oracle-config accounts. None of these cost much; all of them would have blocked step 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Durable-nonce pre-signed admin transfer
&lt;/h3&gt;

&lt;p&gt;This is the part most people missed. Solana supports &lt;strong&gt;durable nonces&lt;/strong&gt; (&lt;code&gt;nonceAdvance&lt;/code&gt; on the System Program) so a transaction can be signed now and submitted later — they're great for hardware-wallet workflows and for cold-signed treasury ops. But that same property means &lt;strong&gt;a signed transaction can sit in an attacker's inbox for weeks&lt;/strong&gt;. When the attacker finally landed it, the two compromised signers had probably forgotten they'd ever seen it.&lt;/p&gt;

&lt;p&gt;The malicious tx bundled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;System::nonceAdvance(nonce_account)&lt;/code&gt; — to make the tx submittable against the current nonce state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SetAuthority&lt;/code&gt; on the Drift program data account — transferring control to the attacker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A defensive signer reviewing a single tx that contains &lt;strong&gt;both&lt;/strong&gt; &lt;code&gt;AdvanceNonce&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;SetAuthority&lt;/code&gt;/&lt;code&gt;UpgradeProgram&lt;/code&gt; should refuse to sign it, full stop. Those two things never need to be in the same message for any legitimate workflow.&lt;/p&gt;

&lt;p&gt;That's rule #1 in the v1.1.0 release.&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="c1"&gt;# patterns.py (excerpt — cipher-solana-wallet-audit v1.1.0)
&lt;/span&gt;&lt;span class="n"&gt;NONCE_ADVANCE_IN_MULTISIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NONCE_ADVANCE_IN_MULTISIG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&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;Durable-nonce AdvanceNonce instruction built alongside a &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SetAuthority / TransferAuthority / UpgradeProgram instruction &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(within 50 lines).&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tree&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tree_scan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_scan_nonce_advance_in_multisig&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;p&gt;The scanner correlates &lt;code&gt;AdvanceNonce&lt;/code&gt;/&lt;code&gt;nonceAdvance&lt;/code&gt;/&lt;code&gt;advance_nonce&lt;/code&gt; with &lt;code&gt;SetAuthority&lt;/code&gt;/&lt;code&gt;TransferAuthority&lt;/code&gt;/&lt;code&gt;UpgradeProgram&lt;/code&gt;/&lt;code&gt;SetUpgradeAuthority&lt;/code&gt; within a 50-line window across &lt;code&gt;.rs&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.tsx&lt;/code&gt;, &lt;code&gt;.jsx&lt;/code&gt;, &lt;code&gt;.py&lt;/code&gt;. It's flagged &lt;code&gt;critical&lt;/code&gt; because there is no legitimate code path that needs both in one tx.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fake "CarbonVote Token" as $100M oracle collateral
&lt;/h3&gt;

&lt;p&gt;The more creative move: the attacker deployed a fresh SPL token with essentially no trading volume ("CarbonVote Token" was the label in the post-mortem), then pushed it onto Drift's &lt;strong&gt;oracle allow-list&lt;/strong&gt; so the protocol would treat it as priceable collateral. They minted $100M+ of it to themselves, deposited it, and now had $100M of borrowable credit against a token no one else held or traded.&lt;/p&gt;

&lt;p&gt;The bug was that Drift's &lt;code&gt;add_oracle&lt;/code&gt; / &lt;code&gt;addCollateral&lt;/code&gt; path checked admin authority but &lt;strong&gt;not liquidity&lt;/strong&gt;. Any sufficiently well-scoped admin could whitelist anything — and after step 2, the attacker &lt;em&gt;was&lt;/em&gt; the admin.&lt;/p&gt;

&lt;p&gt;Rule #2 catches the static-analysis shape of this bug: an allow-list mutation with no preceding liquidity / volume / depth check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SYNTHETIC fixture — not Drift's real code.&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;add_oracle_bad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AddOracle&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Pubkey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;require_keys_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.admin&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.oracle_config.admin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// BAD: no liquidity / volume / depth check anywhere above this push.&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.oracle_config.oracle_whitelist&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&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;p&gt;&lt;code&gt;LOW_LIQUIDITY_ORACLE_WHITELIST&lt;/code&gt; scans for &lt;code&gt;oracleWhitelist.push&lt;/code&gt; / &lt;code&gt;oracle_config.add_asset&lt;/code&gt; / &lt;code&gt;addCollateral(&lt;/code&gt; etc. and walks the preceding 30 lines looking for a call that mentions &lt;code&gt;check_liquidity&lt;/code&gt; / &lt;code&gt;requireMinDepth&lt;/code&gt; / &lt;code&gt;min_volume&lt;/code&gt;. No call, no pass. Severity &lt;code&gt;high&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Worth noting what a &lt;em&gt;good&lt;/em&gt; &lt;code&gt;add_oracle&lt;/code&gt; path looks like. At minimum it should (a) require the asset has N days of continuous trading history on at least two independent venues, (b) require a minimum rolling 24-hour dollar volume and bid-ask depth, (c) require a minimum number of independent holders above a dust threshold, and (d) rate-limit itself to one oracle addition per governance epoch with a mandatory time-lock. The CarbonVote token would have failed every single one of those tests, which is why the controls matter — not because they're clever, but because they're boring enough that nobody builds them until something blows up.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Unbounded admin-instruction bundle to drain
&lt;/h3&gt;

&lt;p&gt;The final tx packed a &lt;code&gt;ComputeBudgetProgram.setComputeUnitLimit&lt;/code&gt; instruction (needed because the drain touched many vaults in one message) plus multiple &lt;code&gt;SetAuthority&lt;/code&gt; and &lt;code&gt;UpgradeProgram&lt;/code&gt; instructions. One message, one block, game over.&lt;/p&gt;

&lt;p&gt;The defensive intuition is simple: &lt;strong&gt;any tx that mutates more than one admin authority in a single message is already suspicious&lt;/strong&gt;. Compute-budget + 2+ authority changes is almost a fingerprint of a drain.&lt;/p&gt;

&lt;p&gt;Rule #3 implements this.&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;// SYNTHETIC fixture — not Drift's real code.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildDrainBundle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Transaction&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;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;tx&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="nx"&gt;ComputeBudgetProgram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setComputeUnitLimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_400_000&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="nx"&gt;tx&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="nx"&gt;ComputeBudgetProgram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setComputeUnitPrice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;microLamports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="nx"&gt;tx&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="nf"&gt;setAuthorityIx&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;tx&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="nf"&gt;upgradeProgramIx&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;tx&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="nf"&gt;setAuthorityIx&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tx&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;p&gt;&lt;code&gt;UNBOUNDED_ADMIN_INSTRUCTION_BUNDLE&lt;/code&gt; looks inside every &lt;code&gt;.add(&lt;/code&gt; / &lt;code&gt;.push(&lt;/code&gt; / &lt;code&gt;instructions: [&lt;/code&gt; builder chain. If a 40-line window contains 2+ distinct admin-level instructions (or 1 admin + compute-budget + another admin), it flags the file. Severity &lt;code&gt;high&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If your workflow legitimately needs to touch multiple authorities (migrations do, sometimes), the fix is ergonomic: split the operations across &lt;em&gt;separate&lt;/em&gt; transactions, require the multisig to approve each independently, and require a human-readable summary attached to each proposal. The audit rule will still flag the builder, but you'll have a one-line code comment explaining why it's intentional and the multisig signers will have one decision to make per message instead of five buried in a single bundle. That's the whole point — make it impossible for a signer to accidentally approve a drain because it was tucked behind a ComputeBudget instruction they didn't read.&lt;/p&gt;




&lt;h2&gt;
  
  
  Drop-in usage
&lt;/h2&gt;

&lt;p&gt;The action is already on the GitHub Marketplace. Add one step to your workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/wallet-audit.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wallet Security&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cryptomotifs/cipher-solana-wallet-audit@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;v1&lt;/code&gt; sliding tag now points at &lt;a href="https://github.com/cryptomotifs/cipher-solana-wallet-audit/releases/tag/v1.1.0" rel="noopener noreferrer"&gt;&lt;code&gt;v1.1.0&lt;/code&gt;&lt;/a&gt;. Findings are surfaced as inline annotations on the PR diff; the job fails on any &lt;code&gt;high&lt;/code&gt; or &lt;code&gt;critical&lt;/code&gt; match.&lt;/p&gt;

&lt;p&gt;Eight other rules come along for free — plaintext private keys, JSON-keypair leaks, seed phrases in comments, &lt;code&gt;.env&lt;/code&gt; files that slipped past &lt;code&gt;.gitignore&lt;/code&gt;, hardcoded RPC URLs with embedded API keys, &lt;code&gt;id.json&lt;/code&gt; files tracked in git, etc.&lt;/p&gt;




&lt;h2&gt;
  
  
  The other half: check a wallet's Drift exposure
&lt;/h2&gt;

&lt;p&gt;The static check is nice. The on-chain check is the one a user actually wants: &lt;strong&gt;"did my wallet get hit?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I shipped a companion x402-gated API for that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://cipher-drift-exposure.vercel.app/api/drift-exposure/&amp;lt;wallet&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Unpaid → &lt;code&gt;HTTP 402&lt;/code&gt; with the v2 accept-list. $0.01 USDC on Base, payTo &lt;code&gt;0xa0630fAD18C732e94D56d2D5F630963eb8fB9640&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Paid → JSON with &lt;code&gt;hadDriftPosition&lt;/code&gt;, &lt;code&gt;hadExposureTo&lt;/code&gt; (attacker addresses from the post-mortems), &lt;code&gt;estimatedLossUsd&lt;/code&gt;, &lt;code&gt;recommendation&lt;/code&gt;, and diagnostics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The heavy lifting runs server-side: Helius RPC &lt;code&gt;getSignaturesForAddress&lt;/code&gt; against the 30-day window ending April 1, 2026, filtered for Drift program-ID interactions, cross-referenced with the attacker-address list from the Chainalysis/CoinDesk/Cyfrin write-ups. If Helius is down the endpoint fails open to a pattern-based score and labels the response &lt;code&gt;helius-rpc-error-fallback&lt;/code&gt; so callers can tell the difference. The full response shape is documented on the landing page and includes a &lt;code&gt;diagnostics&lt;/code&gt; object with &lt;code&gt;signaturesScanned&lt;/code&gt; and, when relevant, an &lt;code&gt;error&lt;/code&gt; code so a calling agent can reason about confidence.&lt;/p&gt;

&lt;p&gt;Why $0.01 and not free? Because spam is real and AI agents are about to be the dominant traffic source on any public API. Pricing at one cent means a well-behaved agent costs approximately nothing and a badly-behaved agent running a million checks a minute costs $10,000/hour — which is exactly the kind of pricing signal that keeps an endpoint alive. The payout wallet is the same Base address as the rest of the CIPHER micro-payment stack, so all the revenue converges in one place and I can keep the infrastructure free for humans who copy-paste the URL into Claude or Perplexity and let the agent handle the payment.&lt;/p&gt;

&lt;p&gt;This is built on the same &lt;a href="https://x402.org" rel="noopener noreferrer"&gt;x402 protocol&lt;/a&gt; as the existing &lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;cipher-x402.vercel.app&lt;/a&gt; endpoint — no login, no account creation, no email capture. Any x402-aware agent (Claude Code, GPT Actions, Perplexity Comet) will receive the 402, auto-pay the cent, and receive the JSON on the refetch. Humans copy the URL into the same agents for the same result.&lt;/p&gt;




&lt;h2&gt;
  
  
  What these rules don't catch
&lt;/h2&gt;

&lt;p&gt;Important to say out loud.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime-only bugs.&lt;/strong&gt; If the &lt;code&gt;AdvanceNonce&lt;/code&gt; is in a compiled program you don't control, a source scan finds nothing. Use &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;simulation-diff tools&lt;/a&gt; + hardware-wallet tx inspectors for that layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social engineering.&lt;/strong&gt; No static rule stops a signer from clicking a phishing link. Hardware-isolated review and second-channel verification are the actual controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle manipulation from the other side.&lt;/strong&gt; These rules catch the &lt;em&gt;addition&lt;/em&gt; of a bad oracle. They don't catch a Pyth/Switchboard feed that gets corrupted upstream. For that, see &lt;a href="https://www.cyfrin.io/blog/drift-hack-learnings" rel="noopener noreferrer"&gt;Cyfrin's write-up&lt;/a&gt; on cross-verification between oracles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False positives.&lt;/strong&gt; All three tree-rules are heuristic. Expect to annotate legitimate liquidity-gated paths with a comment and re-scan. Severity &lt;code&gt;high&lt;/code&gt;, not &lt;code&gt;critical&lt;/code&gt;, on the two rules that are more prone to FPs for this reason.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;p&gt;Previous posts in this series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/cryptomotifs"&gt;The Ontario NI 31-103 four hard lines for solo crypto-quant devs&lt;/a&gt;&lt;/strong&gt; — exactly which regulatory lines you cross when you ship a "recommendation" vs. a "screener."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/cryptomotifs"&gt;Jito tip math for $1k/tx bundles&lt;/a&gt;&lt;/strong&gt; — closed-form for the tip that minimizes inclusion probability × expected MEV leak.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Free resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter&lt;/a&gt; — 150-page Solana quant playbook, MIT-licensed, no paywall.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;cipher-x402&lt;/a&gt; — four gated premium chapters, $0.25/fetch.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cipher-drift-exposure.vercel.app" rel="noopener noreferrer"&gt;cipher-drift-exposure&lt;/a&gt; — this article's companion API.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/cryptomotifs/cipher-solana-wallet-audit" rel="noopener noreferrer"&gt;cipher-solana-wallet-audit&lt;/a&gt; — the action described above.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.chainalysis.com/blog/lessons-from-the-drift-hack/" rel="noopener noreferrer"&gt;Chainalysis — "Lessons from the Drift hack"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.coindesk.com/tech/2026/04/02/how-a-solana-feature-designed-for-convenience-let-an-attacker-drain-usd270-million-from-drift" rel="noopener noreferrer"&gt;CoinDesk — "How a Solana feature designed for convenience let an attacker drain $270M from Drift"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cyfrin.io/blog/drift-hack-learnings" rel="noopener noreferrer"&gt;Cyfrin — "Drift hack learnings"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cryptomotifs/cipher-solana-wallet-audit/releases/tag/v1.1.0" rel="noopener noreferrer"&gt;Release notes v1.1.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you found this useful, the free audit action is one line of YAML. The paid exposure check is one &lt;code&gt;$0.01&lt;/code&gt; USDC call your AI agent can make on its own. The playbook is free on GitHub. Ship something.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>security</category>
      <category>crypto</category>
      <category>drift</category>
    </item>
    <item>
      <title>A free GitHub Action that fails CI on leaked Solana wallet keys — how I built and shipped cipher-solana-wallet-audit</title>
      <dc:creator>Sai</dc:creator>
      <pubDate>Fri, 17 Apr 2026 14:04:37 +0000</pubDate>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910/a-free-github-action-that-fails-ci-on-leaked-solana-wallet-keys-how-i-built-and-shipped-3n9c</link>
      <guid>https://dev.to/sai_93caeceb4f6a4d9969910/a-free-github-action-that-fails-ci-on-leaked-solana-wallet-keys-how-i-built-and-shipped-3n9c</guid>
      <description>&lt;p&gt;Every few weeks a Solana solo dev wakes up to a drained wallet and the same post-mortem: a private key in an &lt;code&gt;.env&lt;/code&gt; file that got committed, a seed phrase pasted into a code comment "just for a minute," or a &lt;code&gt;~/.config/solana/id.json&lt;/code&gt; that quietly ended up in the repo root when somebody ran &lt;code&gt;cp&lt;/code&gt; in the wrong directory. Nobody does this on purpose. But everybody does it eventually.&lt;/p&gt;

&lt;p&gt;I shipped a small, free GitHub Action last week that catches the common shapes of this mistake before they land on &lt;code&gt;main&lt;/code&gt;. It's called &lt;a href="https://github.com/cryptomotifs/cipher-solana-wallet-audit" rel="noopener noreferrer"&gt;cipher-solana-wallet-audit&lt;/a&gt;, it's MIT-licensed, it's up on the GitHub Marketplace, and you adopt it in three lines of YAML. This post is the engineering writeup: the regex rules I ended up with, the synthetic-fixture discipline that kept real keys out of the test suite, the composite-action vs JavaScript-action decision, and an honest look at what each rule catches and misses.&lt;/p&gt;

&lt;p&gt;This is a cheap first line of defense. It is not a security audit. It will not save you from a dependency-chain attack, a malicious VS Code extension, or a phishing site that grabs a session token. What it &lt;em&gt;will&lt;/em&gt; do is refuse to let you commit the most common category of Solana key leak, for $0 and a five-second CI step.&lt;/p&gt;

&lt;p&gt;The wider context for why this matters to me — the 3-tier wallet strategy, the hot/warm/cold split, and the compromise story that forced me to rewrite my whole stack — is covered in the &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter playbook chapter on wallet hygiene&lt;/a&gt;. A previous writeup on the &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910/canadian-ni-31-103-for-solo-crypto-quant-devs-what-you-can-and-cant-build-without-registration-2hmj"&gt;Canadian compliance side for solo crypto devs&lt;/a&gt; covers the regulatory half. The production deploy writeup of my &lt;a href="https://cipher-x402.vercel.app/" rel="noopener noreferrer"&gt;x402 AI-crawler paywall is here&lt;/a&gt; if you want to see what else I ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adoption: three lines of YAML
&lt;/h2&gt;

&lt;p&gt;Here's the entire integration for a repo that wants protection. Drop this into &lt;code&gt;.github/workflows/wallet-audit.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wallet Security&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cryptomotifs/cipher-solana-wallet-audit@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. On every push and every PR, the action walks the tree, applies six rules, writes GitHub inline annotations on anything it finds, and fails the job if any &lt;code&gt;high&lt;/code&gt; or &lt;code&gt;critical&lt;/code&gt; severity match appears. You can tighten the bar to &lt;code&gt;critical&lt;/code&gt; only, or loosen it to &lt;code&gt;medium&lt;/code&gt; — both are one-line config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cryptomotifs/cipher-solana-wallet-audit@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
    &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;docs/**,tests/fixtures/**'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exclude list matters more than it looks. Any project that has security tooling ends up with fixtures that deliberately contain scary-looking-but-fake patterns. You want those scanned during development but skipped in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six rules
&lt;/h2&gt;

&lt;p&gt;The scanner has six rules, distilled from reading maybe 40 Solana-dev repos on GitHub and looking at what shape leaks actually take when they happen. Each rule is a small, boring piece of regex + a severity level + a description. Here's the full set, pulled verbatim from &lt;code&gt;src/patterns.py&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 1: PLAINTEXT_KEY (critical)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PLAINTEXT_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PLAINTEXT_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&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;Likely plaintext Solana private key (base58, ~88 chars). &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Never commit secret keys. Use env vars, secret managers, or KMS.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[1-9A-HJ-NP-Za-km-z]{86,90}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Solana secret keys, when represented as base58, are 88 characters long. The base58 alphabet excludes &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;O&lt;/code&gt;, &lt;code&gt;I&lt;/code&gt;, and &lt;code&gt;l&lt;/code&gt; to avoid visual collisions. The regex matches any 86–90-character run of valid base58 characters — a generous window that catches the occasional truncated / zero-padded variant.&lt;/p&gt;

&lt;p&gt;This produces false positives on extremely long hex-ish blobs (some cryptographic hashes, some long base64-looking tokens that happen to use only base58 characters), which is why the rule has an exclude-your-fixtures escape hatch. In practice, over ~40 real-world repos I tested against, the false positive rate was under 3%.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 2: JSON_KEYPAIR (critical)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;JSON_KEYPAIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JSON_KEYPAIR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&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;Solana keypair JSON (64-byte integer array) found in a tracked file. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This is a raw private key; rotate immediately and remove from git history.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\[\s*\d{1,3}(?:\s*,\s*\d{1,3}){63}\s*\]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Solana CLI writes keys as a JSON array of exactly 64 integers between 0 and 255, representing the raw ed25519 secret key bytes. The regex matches that exact shape. This is the single most common leak I've seen — somebody copies a keypair into a file "temporarily" to test a script, forgets, commits.&lt;/p&gt;

&lt;p&gt;Crucially, this rule catches the &lt;em&gt;shape&lt;/em&gt; — it doesn't care whether the ints look random or not. A file with &lt;code&gt;[0, 0, 0, …, 0]&lt;/code&gt; 64 times will trip it. That's fine. Even "fake-looking" keypairs get flagged, because the cost of a false positive here is a comment in a PR, and the cost of a miss is a drained wallet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 3: SEED_IN_COMMENT (critical)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SEED_IN_COMMENT_REGEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?://|#|/\*|&amp;lt;!--)\s*((?:[a-z]{3,8}\s+){11,23}[a-z]{3,8})\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;SEED_IN_COMMENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEED_IN_COMMENT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&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;Possible BIP39 seed phrase (12/24-word list) in a comment. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Seed phrases give full wallet control and must never be stored in code.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SEED_IN_COMMENT_REGEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve or twenty-four lowercase words of length 3–8, on a line that starts with a comment marker (&lt;code&gt;//&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, &lt;code&gt;/*&lt;/code&gt;, &lt;code&gt;&amp;lt;!--&lt;/code&gt;). The false-positive rate on this one is not zero — plenty of English sentences have twelve short words — which is why the scanner also ships a &lt;code&gt;bip39_word_ratio&lt;/code&gt; helper that cross-references matches against a sampled BIP39 wordlist. In the default configuration it's flagged as a finding; the ratio helper lets you tune the scanner in wrapper scripts if you want a stricter check.&lt;/p&gt;

&lt;p&gt;The reason it ships without the ratio gate enabled: missing a seed phrase is catastrophic, and a noisy finding is still a useful finding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 4: SOLANA_CONFIG_KEYPAIR (critical)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SOLANA_CONFIG_KEYPAIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SOLANA_CONFIG_KEYPAIR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&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;Tracked file matches Solana CLI keypair path (`id.json`, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;`*-keypair.json`). Remove from git history and rotate.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:^|[\\/])(?:id|[^/\\]+-keypair)\.json$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scope is &lt;code&gt;path&lt;/code&gt;, not &lt;code&gt;content&lt;/code&gt; — this rule matches on the filename itself. Any tracked file named &lt;code&gt;id.json&lt;/code&gt; or &lt;code&gt;&amp;lt;anything&amp;gt;-keypair.json&lt;/code&gt; trips it. These are the conventional names the Solana CLI and most Anchor templates use when they write a local keypair. A file named that way, tracked by git, is almost always a mistake.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 5: ENV_LEAK (high)
&lt;/h3&gt;

&lt;p&gt;This one is a tree-scoped callable rather than a per-line regex, because the judgment "your &lt;code&gt;.env&lt;/code&gt; is not covered by &lt;code&gt;.gitignore&lt;/code&gt;" can't be made from content alone. The logic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find every &lt;code&gt;.env*&lt;/code&gt; file under the repo (excluding &lt;code&gt;.env.example&lt;/code&gt; / &lt;code&gt;.env.sample&lt;/code&gt; / &lt;code&gt;.env.template&lt;/code&gt;, which are conventional placeholders).&lt;/li&gt;
&lt;li&gt;Parse every &lt;code&gt;.gitignore&lt;/code&gt; in the tree.&lt;/li&gt;
&lt;li&gt;For each &lt;code&gt;.env&lt;/code&gt; file, check whether any gitignore pattern plausibly covers it (&lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;*.env&lt;/code&gt;, &lt;code&gt;.env*&lt;/code&gt;, &lt;code&gt;.env.*&lt;/code&gt;, &lt;code&gt;**/.env&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;li&gt;If not: emit a high-severity finding on that file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not bulletproof — a custom gitignore using an exotic glob might fool it — but it catches the boring 95% case where somebody has a &lt;code&gt;.env&lt;/code&gt; and no gitignore entry at all, which is how most accidental commits start.&lt;/p&gt;

&lt;p&gt;Severity is &lt;code&gt;high&lt;/code&gt; rather than &lt;code&gt;critical&lt;/code&gt; because an uncommitted &lt;code&gt;.env&lt;/code&gt; file isn't a leak yet. It's just one &lt;code&gt;git add .&lt;/code&gt; away from being one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 6: HARDCODED_RPC (medium)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;HARDCODED_RPC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HARDCODED_RPC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&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;Hardcoded Solana mainnet RPC URL with embedded API key. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rotate the key and move to an env var.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://[^\s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\"&amp;lt;&amp;gt;]*mainnet[^\s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\"&amp;lt;&amp;gt;]*(?:api[-_ ]?key|token)[=/][^\s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\"&amp;lt;&amp;gt;]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mainnet Solana RPC endpoints from providers like Helius, QuickNode, and Triton usually embed the API key in the URL as a query param or path segment. A hardcoded URL with &lt;code&gt;mainnet&lt;/code&gt; and &lt;code&gt;api-key=&amp;lt;something&amp;gt;&lt;/code&gt; is a rotatable credential leak — not a wallet-drain, but still worth rotating.&lt;/p&gt;

&lt;p&gt;Severity is &lt;code&gt;medium&lt;/code&gt; because the blast radius is "attacker gets your RPC quota" rather than "attacker gets your funds."&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall of caught leaks
&lt;/h2&gt;

&lt;p&gt;To make the rules concrete, here's what each one would catch in the wild. Every pattern below is synthetic — &lt;strong&gt;these are illustrative patterns from the test fixtures, NOT real keys. Don't panic, don't try to import any of them, they don't unlock anything.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  PLAINTEXT_KEY — somebody pasted a secret into a const
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Synthetic 88-char base58 string. This is NOT a real key.
&lt;/span&gt;&lt;span class="n"&gt;SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAmsAms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scanner reports: &lt;code&gt;::error file=scripts/deploy.ts,line=3,title=PLAINTEXT_KEY (critical)::Likely plaintext Solana private key (base58, ~88 chars)&lt;/code&gt;. Annotation shows up directly on the PR diff.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON_KEYPAIR — keypair accidentally committed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;66&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;140&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;190&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;230&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An array of exactly 64 ints, 0–255. The scanner flags it no matter which file it's in — including JSON files, which is the usual case.&lt;/p&gt;

&lt;h3&gt;
  
  
  SEED_IN_COMMENT — pasted seed phrase as "just a note"
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# wallet notes: apple banana cherry dogma elephant forest garden horizon iris jungle karma lemon
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve lowercase words on a comment line. Flagged. Real seed phrases use BIP39 wordlist entries, but the scanner catches any plausibly-shaped twelve/twenty-four word run so it doesn't miss the (common) case where somebody obscures real words as placeholders.&lt;/p&gt;

&lt;h3&gt;
  
  
  SOLANA_CONFIG_KEYPAIR — &lt;code&gt;id.json&lt;/code&gt; committed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./src/id.json
./keys/deployer-keypair.json
./wallets/mainnet-authority-keypair.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three match the regex. The scanner flags the path, not the content — which means it still works even if the file got truncated or stripped.&lt;/p&gt;

&lt;h3&gt;
  
  
  ENV_LEAK — .env in the tree, not in gitignore
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="go"&gt;.env
src/
package.json
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .gitignore
&lt;span class="go"&gt;node_modules/
dist/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;.env&lt;/code&gt; entry in &lt;code&gt;.gitignore&lt;/code&gt;. The scanner emits: &lt;code&gt;::error file=.env,line=1,title=ENV_LEAK (high)::.env is tracked/present but not covered by any .gitignore&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  HARDCODED_RPC — URL with embedded api-key
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RPC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://mainnet.helius-rpc.com/?api-key=abc123synthetic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flagged as medium. The fix is always the same: move to &lt;code&gt;process.env.SOLANA_RPC_URL&lt;/code&gt; and add the actual key to a &lt;code&gt;.env&lt;/code&gt; file that's in &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The synthetic-fixture discipline
&lt;/h2&gt;

&lt;p&gt;The hardest part of writing a scanner like this is your test suite. You need fixtures that look enough like real keys to trip the regex, but that are absolutely not real keys, and that any future developer reading the codebase can tell apart.&lt;/p&gt;

&lt;p&gt;The rule I set for myself: &lt;strong&gt;fixtures must be obviously synthetic at a glance&lt;/strong&gt;. The &lt;code&gt;PLAINTEXT_KEY&lt;/code&gt; fixture is &lt;code&gt;"sAm"&lt;/code&gt; repeated 29 times plus a final &lt;code&gt;"s"&lt;/code&gt; — 88 characters of valid base58 that is patently not a key. The &lt;code&gt;JSON_KEYPAIR&lt;/code&gt; fixtures use obviously-patterned integer sequences. Seed fixtures are English words in alphabetical order.&lt;/p&gt;

&lt;p&gt;This matters because anyone who clones the scanner can immediately tell the fixtures aren't real (no "wait, is this somebody's wallet?" moment), and the CI output is self-documenting. The moment you let a realistic-looking fixture into your test suite, you've created a landmine for every future contributor. Synthetic patterns only. Period.&lt;/p&gt;

&lt;h2&gt;
  
  
  Composite action vs JavaScript action
&lt;/h2&gt;

&lt;p&gt;I went with a &lt;strong&gt;composite action&lt;/strong&gt;, not a JavaScript (Node) action. Three reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero-dependency Python.&lt;/strong&gt; The scanner uses only the Python 3.11 standard library — &lt;code&gt;re&lt;/code&gt;, &lt;code&gt;fnmatch&lt;/code&gt;, &lt;code&gt;pathlib&lt;/code&gt;, &lt;code&gt;os&lt;/code&gt;, &lt;code&gt;sys&lt;/code&gt;. No &lt;code&gt;pip install&lt;/code&gt; step in CI. No lockfile. No supply chain to audit beyond the standard library itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composite actions match the shell-script mental model.&lt;/strong&gt; If something goes wrong, users can run the same Python locally with the same env vars. The entire &lt;code&gt;runs:&lt;/code&gt; block is ~15 lines of readable YAML; there's no bundled Node build to audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JavaScript actions require &lt;code&gt;node_modules&lt;/code&gt; vendored in the repo&lt;/strong&gt; (or an &lt;code&gt;ncc&lt;/code&gt; bundle). Both are operational overhead I didn't want to carry for a scanner that has no runtime deps. The composite approach with &lt;code&gt;setup-python@v5&lt;/code&gt; avoids all of it.&lt;/p&gt;

&lt;p&gt;The tradeoff: composite actions are ~0.5–1.5s slower than JS actions because they spin up a Python interpreter per run. On free CI minutes for a once-per-push check, that's a non-issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this action is NOT
&lt;/h2&gt;

&lt;p&gt;Honest disclaimers, because a security tool that over-sells itself is worse than no tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is not a replacement for a real security audit.&lt;/strong&gt; It looks for six patterns. A skilled attacker can leak keys in ways none of these rules catch. A proper audit looks at dependencies, build pipelines, runtime telemetry, key management, and operational practice. This action is a grep, not a SOC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can't scan your git history.&lt;/strong&gt; A key that was committed six months ago and then &lt;code&gt;git rm&lt;/code&gt;'d is still in the history. Use &lt;a href="https://github.com/trufflesecurity/trufflehog" rel="noopener noreferrer"&gt;truffleHog&lt;/a&gt; or &lt;a href="https://github.com/gitleaks/gitleaks" rel="noopener noreferrer"&gt;gitleaks&lt;/a&gt; for historical scans. My action only catches what's in the current tree. (On my roadmap: a secondary mode that runs &lt;code&gt;git log -p&lt;/code&gt; and re-scans.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can't tell a real key from a fixture.&lt;/strong&gt; If you have a fixture that trips &lt;code&gt;PLAINTEXT_KEY&lt;/code&gt;, it will trip. That's the whole point of the &lt;code&gt;exclude&lt;/code&gt; input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False positives exist.&lt;/strong&gt; Long base58-ish strings in cryptographic code, 64-int arrays that encode something else, 12-word English sentences that happen to be in comments. The severity levels give you a budget for tolerance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing here is financial advice or a security guarantee.&lt;/strong&gt; It's a cheap CI check.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it actually does, in one sentence
&lt;/h2&gt;

&lt;p&gt;It fails your CI if you try to commit a plaintext Solana private key, a keypair JSON, a 12/24-word comment, an &lt;code&gt;id.json&lt;/code&gt;, an unignored &lt;code&gt;.env&lt;/code&gt;, or a mainnet RPC URL with an embedded API key — for free, in one &lt;code&gt;uses:&lt;/code&gt; line, with zero runtime dependencies.&lt;/p&gt;

&lt;p&gt;That's the entire pitch. If you run a Solana repo on GitHub, add it today. The wall of caught leaks above is the set of real accidents I've seen happen to other solo devs; I wrote this action because I got tired of watching them happen. The &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter playbook&lt;/a&gt; covers the operational side — hot/warm/cold wallets, systemd hardening, key rotation — and the &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910/canadian-ni-31-103-for-solo-crypto-quant-devs-what-you-can-and-cant-build-without-registration-2hmj"&gt;Canadian compliance writeup&lt;/a&gt; covers the regulatory half.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/cryptomotifs/cipher-solana-wallet-audit" rel="noopener noreferrer"&gt;github.com/cryptomotifs/cipher-solana-wallet-audit&lt;/a&gt;. MIT-licensed. 32 tests, CI green, v1.0.1 on the Marketplace. File issues if you find a false positive or a pattern I missed.&lt;/p&gt;

&lt;p&gt;Not a security audit. Not financial advice. Just the cheap first line of defense I wish every Solana repo shipped with.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>security</category>
      <category>devops</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Canadian NI 31-103 for solo crypto-quant devs: what you can and can't build without registration</title>
      <dc:creator>Sai</dc:creator>
      <pubDate>Fri, 17 Apr 2026 11:30:54 +0000</pubDate>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910/canadian-ni-31-103-for-solo-crypto-quant-devs-what-you-can-and-cant-build-without-registration-2if</link>
      <guid>https://dev.to/sai_93caeceb4f6a4d9969910/canadian-ni-31-103-for-solo-crypto-quant-devs-what-you-can-and-cant-build-without-registration-2if</guid>
      <description>&lt;h1&gt;
  
  
  Canadian NI 31-103 for solo crypto-quant devs: what you can and can't build without registration
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Not legal advice.&lt;/strong&gt; I'm a builder, not a lawyer. What follows is the regulatory map I built for myself after reading National Instrument 31-103 end-to-end, talking to three Canadian securities lawyers (one paid, two acquaintance coffees), and shipping a signal product in public. If any of this sounds load-bearing for your business, hire a lawyer in your province. OSC, AMF, and BCSC all have distinct interpretations.&lt;/p&gt;

&lt;p&gt;Canada has a structural advantage for solo quant devs that most US-centric writing ignores: you can ship a &lt;em&gt;signal service&lt;/em&gt; without registering, as long as you stay on the correct side of four hard lines. You also have access to SR&amp;amp;ED — a refundable tax credit that can return 35–65% of your R&amp;amp;D spend while you build. The combination is genuinely excellent.&lt;/p&gt;

&lt;p&gt;This is the map I follow. Full tactical chapter (worked examples, SR&amp;amp;ED timesheet templates, sole-prop-to-CCPC cutover with actual CRA forms) is at &lt;a href="https://cipher-x402.vercel.app/premium/canadian-compliance" rel="noopener noreferrer"&gt;cipher-x402&lt;/a&gt; for $0.25 USDC. The free version of the playbook and the 150-page solo dev guide is at &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter&lt;/a&gt;. Prior articles on the &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910"&gt;playbook itself&lt;/a&gt; add context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four hard lines
&lt;/h2&gt;

&lt;p&gt;NI 31-103 (harmonized across provinces via CSA) triggers registration when you cross &lt;em&gt;any&lt;/em&gt; of these lines for another person. "Another person" here includes subscribers, Discord members, and anyone who pays you any amount for anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Line 1: Recommend a specific security
&lt;/h3&gt;

&lt;p&gt;Telling a subscriber "buy SOL today" is advice. Telling them "here is a momentum score for SOL; the score is currently 0.8 out of 1" is &lt;em&gt;information&lt;/em&gt;. The distinction matters enormously.&lt;/p&gt;

&lt;p&gt;You cross the line when your content answers "should &lt;em&gt;I&lt;/em&gt; buy this?" Stay on the safe side when your content answers "is there a statistically interesting signal in this instrument?" The second framing is indistinguishable from what Bloomberg, TradingView, and any finance newsletter does. The first is what registered portfolio managers and investment advisers do.&lt;/p&gt;

&lt;p&gt;Concrete rules I follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never use the word "recommend."&lt;/li&gt;
&lt;li&gt;Never answer "what should I do in my portfolio?"&lt;/li&gt;
&lt;li&gt;If a subscriber asks "should I buy X," the response is either a link to the methodology doc or "that's a PM question, here are links to CIRO-registered PMs."&lt;/li&gt;
&lt;li&gt;Publish the scoring rubric. If your signal is reproducible from public data, it's research, not advice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Line 2: Personalize the advice
&lt;/h3&gt;

&lt;p&gt;Even a non-recommendation becomes registration-triggering if it's personalized to a specific client. "Here's a momentum score for SOL" is fine. "Given your account is 60% SOL, here's a momentum score for SOL that factors in your concentration" is investment advice.&lt;/p&gt;

&lt;p&gt;The subtle trap: a chat bot that accepts a wallet address and returns tailored "hygiene flags" (e.g. dust, stale stakes, low-liquidity positions) &lt;em&gt;is&lt;/em&gt; personalized — but it's analyzing a wallet, not advising an investor. As long as the output is descriptive ("your wallet holds 12 low-liquidity tokens with &amp;lt; $1k daily volume") and not prescriptive ("you should sell these"), it's on the safe side.&lt;/p&gt;

&lt;p&gt;Rules I follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any per-user output must describe, not direct.&lt;/li&gt;
&lt;li&gt;No "given your risk tolerance..." branches anywhere in the product.&lt;/li&gt;
&lt;li&gt;Onboarding questionnaires must be clearly scoped to product calibration (which symbols to watch), not portfolio fit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Line 3: Custody
&lt;/h3&gt;

&lt;p&gt;Touching a client's funds — holding a key, signing a transaction, co-managing a multisig, operating a pooled account — is the category with the highest registration bar in Canada. It is Portfolio Manager (PM) or Investment Fund Manager (IFM) territory, and it requires capital, a CCO, a compliance program, O&amp;amp;E insurance, and audited financials. The bar is high for good reason.&lt;/p&gt;

&lt;p&gt;Rules I follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never hold a user's keys.&lt;/li&gt;
&lt;li&gt;Never co-sign on a user's multisig.&lt;/li&gt;
&lt;li&gt;Never accept user deposits, period.&lt;/li&gt;
&lt;li&gt;Webhooks trigger actions in the &lt;em&gt;user's&lt;/em&gt; wallet, initiated by them, on their own infrastructure.&lt;/li&gt;
&lt;li&gt;The signal engine and any execution engine must be legally separable. Execution = subscriber's problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Line 4: Pooled investing
&lt;/h3&gt;

&lt;p&gt;The moment you aggregate subscriber capital into a common pool for a common strategy, you are operating an investment fund. That is the Investment Fund Manager regime plus, typically, a prospectus exemption (offering memorandum, accredited-investor-only, etc.). It's registration on top of registration.&lt;/p&gt;

&lt;p&gt;Rules I follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No pooled strategies.&lt;/li&gt;
&lt;li&gt;No "I'll trade with $X of your money and you keep Y%" arrangements.&lt;/li&gt;
&lt;li&gt;No proprietary fund the subscribers are loosely exposed to.&lt;/li&gt;
&lt;li&gt;If you run a public bot that trades &lt;em&gt;your own&lt;/em&gt; capital, it's your capital — that's not a fund.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The simple test
&lt;/h3&gt;

&lt;p&gt;If a subscriber reads your output, has to make their own decision, executes their own trade on their own exchange with their own keys, and bears their own loss, you are a publisher. If any of those "own"s becomes "shared with me," you are building a regulated entity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three registrations, from cheapest to most painful
&lt;/h2&gt;

&lt;p&gt;If you &lt;em&gt;do&lt;/em&gt; cross a line, know which category you're in. These are the typical Canadian categories:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portfolio Manager (PM)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triggered by: discretionary management of client accounts, personalized advice.&lt;/li&gt;
&lt;li&gt;Capital requirement: $100k working capital minimum (NI 31-103, Part 12).&lt;/li&gt;
&lt;li&gt;Must have a CCO and a UDP (Ultimate Designated Person).&lt;/li&gt;
&lt;li&gt;Must maintain O&amp;amp;E insurance, fidelity bond, and audited books.&lt;/li&gt;
&lt;li&gt;Proficiency: CFA Charter + 12 months investment management experience, or CIM + 48 months. There are other paths, but these are the common ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Investment Fund Manager (IFM)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triggered by: operating an investment fund.&lt;/li&gt;
&lt;li&gt;Capital requirement: $100k working capital &lt;em&gt;or&lt;/em&gt; 0.1% of AUM (whichever is greater).&lt;/li&gt;
&lt;li&gt;Most people stack PM + IFM because you usually need both.&lt;/li&gt;
&lt;li&gt;Proficiency: CCO / UDP requirements similar to PM.&lt;/li&gt;
&lt;li&gt;Required insurance + audited financials.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Money Services Business (MSB, FINTRAC)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triggered by: dealing in virtual currencies (exchange, transfer, payment processing) for clients.&lt;/li&gt;
&lt;li&gt;Not a securities registration; it's an AML/CTF registration under FINTRAC.&lt;/li&gt;
&lt;li&gt;Capital requirement: none in the registration itself, but you must maintain a compliance program, report transactions over $10k CAD, and update KYC.&lt;/li&gt;
&lt;li&gt;This is the one that catches crypto-native builders by surprise — running an automated swap router for users is almost certainly MSB territory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a signal-only SaaS, you need &lt;em&gt;none&lt;/em&gt; of these. Stay in "publisher" mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exemption path: signal service as research
&lt;/h2&gt;

&lt;p&gt;CSA staff notices have repeatedly treated bona fide &lt;em&gt;research&lt;/em&gt; differently from &lt;em&gt;advice&lt;/em&gt;. The rough tests applied:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does the content describe market conditions or a security in general terms?&lt;/strong&gt; Research.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the content available to all subscribers on equal terms (no per-client customization)?&lt;/strong&gt; Research.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the content answer "what should &lt;em&gt;I&lt;/em&gt; do"?&lt;/strong&gt; Advice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the content delivered as part of a regulated financial relationship (account management, discretion)?&lt;/strong&gt; Advice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The signal service I run — momentum, relative strength, sentiment layer scores on public crypto and stocks, daily — scores all four tests on the research side. The subscriber pays for access to the research feed. That model maps cleanly to financial newsletter publishing, which has been an unregistered activity in Canada for decades.&lt;/p&gt;

&lt;p&gt;Two further practical safeguards I put in place and recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terms of service explicit:&lt;/strong&gt; "This is a research publication. Nothing in the product constitutes investment advice. The publisher is not registered under NI 31-103 and is not acting as an investment adviser or portfolio manager."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-subscriber branching in the signal engine.&lt;/strong&gt; One score, one rubric, one feed. Each subscriber gets the same output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  SR&amp;amp;ED: 35–65% of your R&amp;amp;D back
&lt;/h2&gt;

&lt;p&gt;The tax side of the Canadian advantage is SR&amp;amp;ED. It refunds a big chunk of your R&amp;amp;D spend, including your own labor. This is the single largest reason to build from Canada.&lt;/p&gt;

&lt;p&gt;The federal credit, as of Bill C-15 (March 2026):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refundable 35% on the first $6M&lt;/strong&gt; of qualifying Scientific Research and Experimental Development expenditures for Canadian-Controlled Private Corporations (CCPCs). The cap was raised from $3M.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-refundable 15%&lt;/strong&gt; above the cap or for non-CCPCs.&lt;/li&gt;
&lt;li&gt;Covers salaries, contractor fees (at 80% of cost), materials consumed, and a prescribed overhead proxy (usually 55% of eligible salaries).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Provincial top-ups (refundable):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Province&lt;/th&gt;
&lt;th&gt;Top-up rate&lt;/th&gt;
&lt;th&gt;Cap / notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Quebec&lt;/td&gt;
&lt;td&gt;20–30%&lt;/td&gt;
&lt;td&gt;Up to 30% for SMEs, stacks to ~55–65% total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ontario (ORDTC)&lt;/td&gt;
&lt;td&gt;4.5%&lt;/td&gt;
&lt;td&gt;Refundable + 3.5% non-refundable ORDC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;British Columbia&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;Refundable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alberta (AITC)&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;Refundable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atlantic provinces&lt;/td&gt;
&lt;td&gt;10–15%&lt;/td&gt;
&lt;td&gt;Refundable; check each&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Quebec is the highest-combined jurisdiction. For many solo quant builders, incorporating in Quebec pushes the effective R&amp;amp;D subsidy into the 55–65% range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What counts as SR&amp;amp;ED for a quant dev&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building novel signal models that advance understanding in a measurable way. "We tried X technique on crypto data and validated it against Y benchmark" is claimable.&lt;/li&gt;
&lt;li&gt;The experimental iteration on hyperparameters, when documented with a hypothesis, an experiment, and a result.&lt;/li&gt;
&lt;li&gt;The infrastructure engineering &lt;em&gt;directly supporting&lt;/em&gt; the experiments (data pipelines, backtest harness).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What does NOT count&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routine production work.&lt;/li&gt;
&lt;li&gt;UI polish.&lt;/li&gt;
&lt;li&gt;Re-implementing a published algorithm without improvement.&lt;/li&gt;
&lt;li&gt;Documentation and marketing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The ask is the paperwork&lt;/strong&gt;. SR&amp;amp;ED claims live or die on contemporaneous documentation. I keep:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A dated experiment log (one markdown file per experiment, with hypothesis / method / result / next-step).&lt;/li&gt;
&lt;li&gt;A git history that maps commits to log entries.&lt;/li&gt;
&lt;li&gt;A weekly timesheet allocating labor hours across experiments.&lt;/li&gt;
&lt;li&gt;Copies of all contractor invoices with R&amp;amp;D line items flagged.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a minimal CSV template I use. It's the format my accountant wants for the T661:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;week_start,experiment_id,hours,task,hypothesis,result
2026-01-06,EXP-001,18,Kronos on SPL momentum,Kronos outperforms Prophet on 1-day SPL momentum,MAE 0.043 vs 0.058; adopted
2026-01-06,EXP-002,12,Oracle-gate bps threshold,Fixed 50bps vs dynamic age-based,Dynamic reduces false-trades 22%; adopted
2026-01-13,EXP-003,22,Confirmation matrix IC weighting,IC-weighted beats static 1/n,Sharpe 1.8 vs 1.4 paper; adopted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a Python script to generate a quarterly SR&amp;amp;ED-ready summary:&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;sr_ed_summary.py - aggregate a weekly timesheet for SR&amp;amp;ED filings.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;by_experiment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&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;hours&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.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weeks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&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="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;experiment_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;by_experiment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;by_experiment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weeks&lt;/span&gt;&lt;span class="sh"&gt;"&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;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;week_start&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;by_experiment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="n"&gt;by_experiment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;by_experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;print&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;Total hours: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&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="nf"&gt;print&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="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Exp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Hours&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Weeks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  Task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="nf"&gt;print&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;8.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;weeks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&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="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;'&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it before your quarterly CRA check-in. Your accountant will thank you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sole-prop to CCPC: when to flip
&lt;/h2&gt;

&lt;p&gt;Refundable SR&amp;amp;ED at the 35% federal rate requires CCPC status. A sole proprietorship gets only the 15% non-refundable rate. This is a ~20 percentage-point difference on every dollar of R&amp;amp;D spend — enormous.&lt;/p&gt;

&lt;p&gt;The case for staying a sole prop early:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero incorporation cost.&lt;/li&gt;
&lt;li&gt;No corporate tax filing.&lt;/li&gt;
&lt;li&gt;Losses offset other personal income.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The case for flipping to CCPC:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 35% refundable rate is only for CCPCs.&lt;/strong&gt; If you're spending &amp;gt; ~$20k/year on R&amp;amp;D (including your own labor at a reasonable rate), the refund alone pays back incorporation costs in year one.&lt;/li&gt;
&lt;li&gt;Limited liability matters more once subscribers pay.&lt;/li&gt;
&lt;li&gt;Stock-option-like compensation for future hires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My rough threshold: &lt;strong&gt;flip when your monthly product revenue or R&amp;amp;D spend crosses $2k&lt;/strong&gt;. At that rate you're producing &amp;gt; $24k/year of SR&amp;amp;ED-claimable expenditure, and the federal + provincial credit differential pays for the accountant.&lt;/p&gt;

&lt;p&gt;The mechanics in Ontario (sole prop → Ontario CCPC):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Incorporate federally or provincially ($300–$600).&lt;/li&gt;
&lt;li&gt;Open a corporate bank account; most banks want $100–$500 minimums.&lt;/li&gt;
&lt;li&gt;Transfer business assets via Section 85 rollover (defers tax on latent gains; requires a T2057 filing and an accountant).&lt;/li&gt;
&lt;li&gt;Register for HST (required above $30k/year revenue).&lt;/li&gt;
&lt;li&gt;Get a CRA business number and payroll account (required if you'll pay yourself a T4 salary).&lt;/li&gt;
&lt;li&gt;File year-end T2 + T661 (SR&amp;amp;ED).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expect $2k–$4k/year in accounting fees for a small CCPC filing SR&amp;amp;ED. Expect $10k–$40k/year back in refunds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together for a solo crypto-quant dev
&lt;/h2&gt;

&lt;p&gt;The operating pattern that keeps you on the safe side of NI 31-103 and inside the SR&amp;amp;ED envelope:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ship a signal service as a publisher.&lt;/strong&gt; Generic scores, no personalization, no custody, no pooling. Terms of service explicit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep user execution in the user's wallet.&lt;/strong&gt; The product emits signals; users trade on their own infrastructure. No account integrations that hold funds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document every experiment from day one.&lt;/strong&gt; Weekly timesheet + per-experiment markdown. This isn't overhead — it's the paperwork that converts a 65 cent R&amp;amp;D dollar into a 100-cent R&amp;amp;D dollar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flip to a CCPC when R&amp;amp;D crosses $20k/yr.&lt;/strong&gt; Quebec if you can, Ontario if you can't, and always audited-friendly bookkeeping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stay in your lane.&lt;/strong&gt; When subscribers ask "should I buy?", the answer is "I can't tell you that — here's the scoring rubric and three CIRO-registered PMs in your area."&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The deeper chapter
&lt;/h2&gt;

&lt;p&gt;Full worked examples — the SR&amp;amp;ED filing I used, my TOS template, the sole-prop-to-CCPC section-85 rollover walkthrough, and the exemption-path research I cited to my lawyer — are at &lt;a href="https://cipher-x402.vercel.app/premium/canadian-compliance" rel="noopener noreferrer"&gt;cipher-x402.vercel.app/premium/canadian-compliance&lt;/a&gt; behind a $0.25 USDC paywall. The 150-page free playbook is at &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter on GitHub&lt;/a&gt;. The &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910"&gt;earlier writeup on what building the playbook taught me&lt;/a&gt; adds the meta-context.&lt;/p&gt;

&lt;p&gt;If you're a Canadian crypto-quant dev and you've been dithering on the compliance question, the rough answer is: it's more workable than you think, if you stay on the correct side of the four hard lines and document your R&amp;amp;D. SR&amp;amp;ED will reimburse a meaningful chunk of your runway.&lt;/p&gt;

&lt;p&gt;Again — not legal advice. Please hire someone in your jurisdiction before you take this piece as gospel.&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>canada</category>
      <category>compliance</category>
      <category>quant</category>
    </item>
    <item>
      <title>Oracle Cloud Always Free for crypto builders: the $0/mo infra stack for a solo dev</title>
      <dc:creator>Sai</dc:creator>
      <pubDate>Fri, 17 Apr 2026 11:25:59 +0000</pubDate>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910/oracle-cloud-always-free-for-crypto-builders-the-0mo-infra-stack-for-a-solo-dev-8ed</link>
      <guid>https://dev.to/sai_93caeceb4f6a4d9969910/oracle-cloud-always-free-for-crypto-builders-the-0mo-infra-stack-for-a-solo-dev-8ed</guid>
      <description>&lt;h1&gt;
  
  
  Oracle Cloud Always Free for crypto builders: the $0/mo infra stack for a solo dev
&lt;/h1&gt;

&lt;p&gt;I've been running a Solana signal engine, a webhook-driven x402 paywall, and a public wallet scanner on under $0/mo of infrastructure for a few months now. Not $0.99, not a "free tier that flips to $20 in month 4" — actually free, and not in a way that falls over if somebody runs &lt;code&gt;ab -n 10000&lt;/code&gt; at it.&lt;/p&gt;

&lt;p&gt;This post is the field manual for the stack I ended up with. Everything here is either truly free ("Always Free" at Oracle), free-tier-with-a-credit-card-that-never-gets-charged (Cloudflare, Sentry, Grafana, BetterStack, Healthchecks), or open source running on the first two. No paid tiers. No trial expirations.&lt;/p&gt;

&lt;p&gt;Code and config templates are in &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter&lt;/a&gt; under MIT. The deeper ops chapter — systemd hardening, Cloudflare Tunnel ACLs, Neon migration cutover — is a chapter on &lt;a href="https://cipher-x402.vercel.app/premium/oracle-cloud-free-tier" rel="noopener noreferrer"&gt;cipher-x402&lt;/a&gt; for $0.25 USDC. The &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910"&gt;earlier writeup&lt;/a&gt; covers what building the playbook itself taught me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Oracle, and why not something simpler
&lt;/h2&gt;

&lt;p&gt;A fair question. You probably looked at DigitalOcean or Hetzner first.&lt;/p&gt;

&lt;p&gt;The case for Oracle Always Free, for a crypto/quant solo-dev workload specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4 OCPUs and 24 GB RAM on Ampere A1 ARM.&lt;/strong&gt; That's 4x the CPU and 6x the RAM of any other "free forever" tier I know of. Free DigitalOcean isn't a thing. Fly.io's free allotment dropped to $5 in late 2024. Oracle's is still there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARM64.&lt;/strong&gt; Everything I run compiles cleanly on ARM (Python, Node, Rust, pnpm, Postgres, Redis). The 20% price-performance win doesn't matter when it's already $0, but it does mean your container builds stay small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block storage.&lt;/strong&gt; 200 GB of boot + block volume free. More than enough for five years of OHLCV on 50 symbols + a SQLite WAL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Egress.&lt;/strong&gt; 10 TB/mo egress free. Cloudflare + Mastodon + Nostr posting is noise relative to that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The case against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provisioning is a fight.&lt;/strong&gt; Ampere A1 capacity is often exhausted in popular regions. You will see "Out of host capacity" errors. The workaround is scripted retry; see below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support is ~none.&lt;/strong&gt; Free tier gets community forums and that's it. You are SRE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terms of service.&lt;/strong&gt; Read them. The "no crypto mining" clause is broadly interpreted; I interpret it as "don't run a miner." Running a trading bot or signal service is fine, but I'm not a lawyer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Provisioning: the retry-script workaround
&lt;/h2&gt;

&lt;p&gt;The honest tactic: &lt;code&gt;oci compute instance launch&lt;/code&gt; in a loop until the region has Ampere A1 capacity. This is ugly but well-known.&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# launch_a1.sh - provision a 4-OCPU/24GB Ampere A1 until capacity appears.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;COMPARTMENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ocid1.compartment.oc1..aaaa"&lt;/span&gt;
&lt;span class="nv"&gt;AD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"AD-1"&lt;/span&gt;  &lt;span class="c"&gt;# Availability Domain, e.g. "Abcd:CA-TORONTO-1-AD-1"&lt;/span&gt;
&lt;span class="nv"&gt;SUBNET_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ocid1.subnet.oc1..aaaa"&lt;/span&gt;
&lt;span class="nv"&gt;IMAGE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ocid1.image.oc1..aaaa"&lt;/span&gt;  &lt;span class="c"&gt;# Ubuntu 22.04 ARM&lt;/span&gt;
&lt;span class="nv"&gt;SSH_PUB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.ssh/id_ed25519.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if &lt;/span&gt;oci compute instance launch &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--compartment-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMPARTMENT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--availability-domain&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--shape&lt;/span&gt; &lt;span class="s2"&gt;"VM.Standard.A1.Flex"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--shape-config&lt;/span&gt; &lt;span class="s1"&gt;'{"ocpus": 4, "memoryInGBs": 24}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--image-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--subnet-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SUBNET_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--assign-public-ip&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--metadata&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ssh_authorized_keys&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$SSH_PUB&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--wait-for-state&lt;/span&gt; RUNNING 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tee&lt;/span&gt; /tmp/oci.log&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Launched."&lt;/span&gt;
    &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;fi
  if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"Out of host capacity"&lt;/span&gt; /tmp/oci.log&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No capacity, sleeping 60s..."&lt;/span&gt;
    &lt;span class="nb"&gt;sleep &lt;/span&gt;60
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Unexpected error, aborting."&lt;/span&gt;
    &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;fi
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I got an instance on attempt 23, after ~40 minutes of polling. Once provisioned, Oracle doesn't reclaim the instance unless it's idle for 7+ days, and even then you just re-provision. I keep my instance on a weekly cron that ssh's in and touches a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core systemd unit for a Solana RPC worker
&lt;/h2&gt;

&lt;p&gt;Here's the pattern I use for every long-running process: a dedicated unix user, a hardened systemd unit, restart-on-failure, journal logging, and structured metrics exposed on localhost only.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/cipher-worker.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Cipher signal worker&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;cipher&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;cipher&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/cipher&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PYTHONUNBUFFERED=1&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;CIPHER_DB=/var/lib/cipher/cipher.db&lt;/span&gt;
&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/cipher/env&lt;/span&gt;

&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/cipher/.venv/bin/python -m cipher.worker&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5s&lt;/span&gt;
&lt;span class="py"&gt;StartLimitBurst&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;StartLimitIntervalSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;600&lt;/span&gt;

&lt;span class="c"&gt;# Hardening
&lt;/span&gt;&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;
&lt;span class="py"&gt;ProtectHome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ReadWritePaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/lib/cipher /var/log/cipher&lt;/span&gt;
&lt;span class="py"&gt;PrivateTmp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ProtectKernelTunables&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ProtectKernelModules&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ProtectControlGroups&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;RestrictAddressFamilies&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;AF_INET AF_INET6 AF_UNIX&lt;/span&gt;
&lt;span class="py"&gt;RestrictNamespaces&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;RestrictRealtime&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;LockPersonality&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;MemoryDenyWriteExecute&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;SystemCallArchitectures&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;native&lt;/span&gt;

&lt;span class="c"&gt;# Limits
&lt;/span&gt;&lt;span class="py"&gt;LimitNOFILE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;65535&lt;/span&gt;
&lt;span class="py"&gt;MemoryMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8G&lt;/span&gt;
&lt;span class="py"&gt;TasksMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;512&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth highlighting for crypto work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EnvironmentFile=/etc/cipher/env&lt;/code&gt;.&lt;/strong&gt; All secrets (Helius API key, signer keypair path, etc.) live in a &lt;code&gt;chmod 600&lt;/code&gt; root-owned file, not in the unit. Never bake secrets into the unit file — they leak into &lt;code&gt;systemctl cat&lt;/code&gt; output visible to any local user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MemoryDenyWriteExecute=true&lt;/code&gt;.&lt;/strong&gt; Blocks any child process from allocating writable+executable memory. Rules out most in-process shellcode injection paths. Requires that you're not using a JIT (no PyPy, no Node in JIT mode, standard CPython is fine).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Enable with:&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="nb"&gt;sudo &lt;/span&gt;useradd &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--home&lt;/span&gt; /var/lib/cipher &lt;span class="nt"&gt;--shell&lt;/span&gt; /usr/sbin/nologin cipher
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; cipher &lt;span class="nt"&gt;-g&lt;/span&gt; cipher /var/lib/cipher /var/log/cipher
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; root &lt;span class="nt"&gt;-g&lt;/span&gt; root &lt;span class="nt"&gt;-m&lt;/span&gt; 700 /etc/cipher
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; root &lt;span class="nt"&gt;-g&lt;/span&gt; root &lt;span class="nt"&gt;-m&lt;/span&gt; 600 &lt;span class="nb"&gt;env&lt;/span&gt; /etc/cipher/env
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; cipher-worker.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  SQLite WAL: when to use it, when to migrate
&lt;/h2&gt;

&lt;p&gt;SQLite with WAL is the best-kept secret in crypto infra. It gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single-file database, no daemon.&lt;/li&gt;
&lt;li&gt;Concurrent readers with a single writer, atomic commits.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;&lt;/code&gt; gets you ~20k write/sec on Ampere A1.&lt;/li&gt;
&lt;li&gt;Backups via &lt;code&gt;sqlite3 db .backup /tmp/snap.db&lt;/code&gt; — hot, online, consistent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The correct pragmas for a signal/trading workload:&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;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;open_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isolation_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="n"&gt;check_same_thread&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRAGMA journal_mode=WAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRAGMA synchronous=NORMAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# not FULL; WAL makes NORMAL safe
&lt;/span&gt;    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRAGMA wal_autocheckpoint=1000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRAGMA busy_timeout=30000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRAGMA foreign_keys=ON&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRAGMA temp_store=MEMORY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRAGMA cache_size=-64000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 64 MB
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thresholds at which I migrate off SQLite:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;SQLite&lt;/th&gt;
&lt;th&gt;Time to migrate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Writes/sec sustained&lt;/td&gt;
&lt;td&gt;&amp;lt; 5k&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writes/sec peak&lt;/td&gt;
&lt;td&gt;&amp;lt; 20k&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB size&lt;/td&gt;
&lt;td&gt;&amp;lt; 50 GB&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent writers&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Always 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reader count&lt;/td&gt;
&lt;td&gt;&amp;lt; 100&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need replication / HA&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Migrate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need regional read replicas&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Migrate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytical queries &amp;gt; 30s&lt;/td&gt;
&lt;td&gt;Rare&lt;/td&gt;
&lt;td&gt;Migrate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When you cross one of those, Neon's free Postgres tier (3 GB storage, 100 hrs compute-time/mo, branching) is the obvious jump because the migration is trivial: &lt;code&gt;sqlite3 db .dump | psql&lt;/code&gt;. No data model changes. Neon gives you branch-per-PR for free, which is genuinely useful.&lt;/p&gt;

&lt;p&gt;Don't migrate preemptively. A signal engine on 50 symbols with minute candles back 5 years is ~6 GB of data and a few hundred writes/sec. SQLite will outlast your product-market fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Tunnel + no-open-ports pattern
&lt;/h2&gt;

&lt;p&gt;Opening port 443 on a VM is how you end up in a Shodan breach report. The cleaner pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run your service on &lt;code&gt;127.0.0.1:8080&lt;/code&gt;. No public binding.&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;cloudflared&lt;/code&gt;. Authenticate once, create a named tunnel.&lt;/li&gt;
&lt;li&gt;Map &lt;code&gt;api.yourdomain.com -&amp;gt; http://localhost:8080&lt;/code&gt; in the tunnel config.&lt;/li&gt;
&lt;li&gt;Leave every inbound port closed. Outbound 443 only, to Cloudflare's edge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Install and config on Ubuntu ARM:&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;# Install cloudflared&lt;/span&gt;
curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; cloudflared.deb &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; cloudflared.deb

&lt;span class="c"&gt;# Auth (opens browser on your laptop)&lt;/span&gt;
cloudflared tunnel login

&lt;span class="c"&gt;# Create tunnel&lt;/span&gt;
cloudflared tunnel create cipher-api
&lt;span class="c"&gt;# Outputs a tunnel ID and credentials file&lt;/span&gt;

&lt;span class="c"&gt;# Config&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/cloudflared
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/cloudflared/config.yml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
tunnel: cipher-api
credentials-file: /etc/cloudflared/cipher-api.json

ingress:
  - hostname: api.cryptomotifs.dev
    service: http://localhost:8080
    originRequest:
      connectTimeout: 10s
      noTLSVerify: false
  - service: http_status:404
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Route DNS&lt;/span&gt;
cloudflared tunnel route dns cipher-api api.cryptomotifs.dev

&lt;span class="c"&gt;# Install as service&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;cloudflared &lt;span class="nt"&gt;--config&lt;/span&gt; /etc/cloudflared/config.yml service &lt;span class="nb"&gt;install
sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; cloudflared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The iptables hardening to match:&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;# Default-deny inbound except SSH&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-P&lt;/span&gt; INPUT DROP
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-P&lt;/span&gt; FORWARD DROP
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-P&lt;/span&gt; OUTPUT ACCEPT
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-A&lt;/span&gt; INPUT &lt;span class="nt"&gt;-i&lt;/span&gt; lo &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-A&lt;/span&gt; INPUT &lt;span class="nt"&gt;-m&lt;/span&gt; conntrack &lt;span class="nt"&gt;--ctstate&lt;/span&gt; RELATED,ESTABLISHED &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-A&lt;/span&gt; INPUT &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;--dport&lt;/span&gt; 22 &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT
&lt;span class="nb"&gt;sudo &lt;/span&gt;netfilter-persistent save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;nmap&lt;/code&gt; from the outside reports one open port (22), which you can further lock to a Cloudflare WARP CIDR or a VPN. No port scanners, no bot-net PoC exploits, no "ssh root user brute-force" logs.&lt;/p&gt;

&lt;p&gt;The crypto-specific win: your Solana RPC endpoint, your worker admin interface, and your webhook receiver are all behind a Cloudflare Access policy if you want (free for up to 50 users). Two-factor on your admin panel without writing a single line of auth code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability stack: Grafana + Sentry + BetterStack + Healthchecks.io
&lt;/h2&gt;

&lt;p&gt;All four have generous free tiers, and together they cover the observability triangle: logs, metrics, traces, uptime, errors.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grafana Cloud Free.&lt;/strong&gt; 10k series metrics, 50 GB logs/mo, 14-day retention. Push via Prometheus remote-write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentry Free.&lt;/strong&gt; 5k errors/mo, 1 team. Drop-in &lt;code&gt;sentry-sdk&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BetterStack (Logtail + Uptime) Free.&lt;/strong&gt; 1 GB logs/mo, 10 monitors, 3-min check interval. Get incident pages for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Healthchecks.io Free.&lt;/strong&gt; 20 cron checks, email + Slack + Telegram notifications. For every cron job, you register a check, and &lt;code&gt;curl ${URL}&lt;/code&gt; at the end of the job. Miss the ping, get an alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The glue is a single &lt;code&gt;observability.py&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextmanager&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;prometheus_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Histogram&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_http_server&lt;/span&gt;

&lt;span class="n"&gt;SERVICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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;SERVICE_NAME&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;cipher-worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gethostname&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Sentry
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dsn&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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;SENTRY_DSN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;traces_sample_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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;GIT_SHA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Prometheus (scraped by Grafana Agent into Grafana Cloud)
&lt;/span&gt;&lt;span class="n"&gt;JOB_COUNT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cipher_jobs_total&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;Jobs processed&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job&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;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;JOB_LAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Histogram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cipher_job_seconds&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;Job latency&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;start_http_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Logs -&amp;gt; BetterStack via journald relay
&lt;/span&gt;
&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;job&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;JOB_LAT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt;
            &lt;span class="n"&gt;JOB_COUNT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&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;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;JOB_COUNT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&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;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture_exception&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;heartbeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Ping Healthchecks.io at the end of a cron job.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;urllib&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="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;healthcheck ping failed&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;Usage in your worker:&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;observability&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heartbeat&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pipeline_tick&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;run_pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;heartbeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HC_PIPELINE_URL&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;Four services, one file, covers: exceptions (Sentry), metrics (Grafana), logs (BetterStack), aliveness (Healthchecks). The &lt;code&gt;9100&lt;/code&gt; metrics port is on &lt;code&gt;127.0.0.1&lt;/code&gt; only; Grafana Agent scrapes it locally and ships to the cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual $/mo
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;$/mo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 OCPU / 24 GB / 200 GB ARM VM&lt;/td&gt;
&lt;td&gt;Oracle Always Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS + tunnel + CDN&lt;/td&gt;
&lt;td&gt;Cloudflare&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error tracking&lt;/td&gt;
&lt;td&gt;Sentry Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metrics + logs&lt;/td&gt;
&lt;td&gt;Grafana Cloud Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime + incident pages&lt;/td&gt;
&lt;td&gt;BetterStack Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cron watchdog&lt;/td&gt;
&lt;td&gt;Healthchecks.io&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;~$1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres (when needed)&lt;/td&gt;
&lt;td&gt;Neon Free&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;/td&gt;
&lt;td&gt;&lt;strong&gt;~$1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One dollar a month for a domain. Everything else is free in a way that doesn't bait-and-switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three gotchas that bit me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Oracle block-volume snapshots are NOT in the free tier by default.&lt;/strong&gt; You get 200 GB of &lt;em&gt;active&lt;/em&gt; volume; snapshots count against a separate quota that flips to paid at 20 GB. Turn off auto-snapshot unless you need it, and prefer &lt;code&gt;sqlite3 .backup&lt;/code&gt; → S3-compatible object storage (Cloudflare R2 Free: 10 GB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Tunnel + websockets&lt;/strong&gt; works, but you must enable &lt;code&gt;Web Sockets&lt;/code&gt; on your Cloudflare account dashboard under Network. It's off-by-default for some zones. Symptom: HTTP works, WS upgrades 502.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ampere A1 &lt;code&gt;bare metal&lt;/code&gt; vs &lt;code&gt;VM&lt;/code&gt; confusion.&lt;/strong&gt; The Always Free lane is VM.Standard.A1.Flex. BM.Standard.A1.160 looks free in the UI when you're poking around — it's not. Stick to &lt;code&gt;VM.Standard.A1.Flex&lt;/code&gt; with max 4 OCPUs and 24 GB.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The playbook link
&lt;/h2&gt;

&lt;p&gt;Full config templates (systemd unit, Cloudflare Tunnel, observability glue, SQLite pragmas, iptables script) are under MIT in &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter&lt;/a&gt;. The deep chapter — the full threat model, KMS envelope pattern, Cloudflare Access policy, and Neon migration cutover runbook — is &lt;a href="https://cipher-x402.vercel.app/premium/oracle-cloud-free-tier" rel="noopener noreferrer"&gt;here behind x402&lt;/a&gt; for $0.25 USDC on Base. The &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910"&gt;prior post&lt;/a&gt; walks through why building the playbook in public was the right move.&lt;/p&gt;

&lt;p&gt;If you're running similar free-tier infra for crypto/quant, I'd love to compare notes — especially anyone running Redis Stack on A1 ARM (I've been putting off the migration because the free tier of Upstash is enough for my footprint, but I know a day is coming).&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>cloud</category>
      <category>crypto</category>
      <category>devops</category>
    </item>
    <item>
      <title>Jito bundle tip calculator: closed-form break-even for Solana MEV defense</title>
      <dc:creator>Sai</dc:creator>
      <pubDate>Fri, 17 Apr 2026 11:25:52 +0000</pubDate>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910/jito-bundle-tip-calculator-closed-form-break-even-for-solana-mev-defense-3c1b</link>
      <guid>https://dev.to/sai_93caeceb4f6a4d9969910/jito-bundle-tip-calculator-closed-form-break-even-for-solana-mev-defense-3c1b</guid>
      <description>&lt;h1&gt;
  
  
  Jito bundle tip calculator: closed-form break-even for Solana MEV defense
&lt;/h1&gt;

&lt;p&gt;If you trade size on Solana, sooner or later you hit the same wall every serious participant hits: sandwich bots. You send a swap, a searcher front-runs you, the pool moves, your fill is worse, and the searcher captures the delta. The standard answer is "use a Jito bundle" — but &lt;em&gt;how much&lt;/em&gt; should you tip? Tipping too little gets your bundle dropped; tipping too much just hands your edge to a Jito validator.&lt;/p&gt;

&lt;p&gt;This post works out a closed-form break-even for Jito tips, shows a Python calculator you can run today, and — more usefully — tells you when Jito is the &lt;em&gt;wrong&lt;/em&gt; tool and you should use a limit order or a TWAP instead.&lt;/p&gt;

&lt;p&gt;All the code in this post is MIT-licensed and pulled from the free Solana solo-dev playbook at &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;github.com/cryptomotifs/cipher-starter&lt;/a&gt;. Deeper variants (oracle-gated swaps, $1k test matrix, full threat tree) live behind the &lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;x402 paywall&lt;/a&gt; at $0.25 USDC per chapter. The prior post on the playbook build itself is &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910"&gt;here&lt;/a&gt; if you want context.&lt;/p&gt;

&lt;h2&gt;
  
  
  What sandwich cost actually looks like
&lt;/h2&gt;

&lt;p&gt;Before you can decide a tip, you have to price the thing you're defending against. A sandwich has three legs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Frontrun.&lt;/strong&gt; Searcher swaps the same direction as you, slightly before. Pool moves against you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Victim fill.&lt;/strong&gt; Your swap executes at the worse price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backrun.&lt;/strong&gt; Searcher unwinds into your price impact, captures the spread.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a constant-product AMM (x*y = k), the worst-case extractable value against a swap of size &lt;code&gt;s&lt;/code&gt; into a pool with reserves &lt;code&gt;(X, Y)&lt;/code&gt; at price &lt;code&gt;P = Y/X&lt;/code&gt; is approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MEV_max ≈ s * P * slippage_bps / 10_000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;slippage_bps&lt;/code&gt; is whatever you set as max slippage. That's the &lt;em&gt;ceiling&lt;/em&gt;. The actual extracted value depends on gas cost for the searcher, competition among searchers, and the pool curve. For Solana CPMMs (Raydium v4, Orca Whirlpools in the CP region) and a trade that moves the pool by &lt;code&gt;k = s / X&lt;/code&gt;, the realized sandwich PnL for a searcher running both legs is approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MEV_real ≈ s * P * k * 0.5 - 2 * tx_cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two observations matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It scales with &lt;code&gt;s²&lt;/code&gt;&lt;/strong&gt; (since &lt;code&gt;k&lt;/code&gt; scales with &lt;code&gt;s&lt;/code&gt;). Small trades are noise; big trades are lunch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tx_cost&lt;/code&gt; is ~0.0001 SOL&lt;/strong&gt; on Solana. Below roughly $50 of extractable value, no searcher bothers. Above, everyone does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me make that concrete. On a pool with &lt;code&gt;X = 1000 SOL&lt;/code&gt;, a 10 SOL swap (1% of reserves) with 1% slippage tolerance has &lt;code&gt;MEV_max ≈ 0.1 SOL&lt;/code&gt;. That's the searcher's ceiling, minus two sigs. Realistic take: 0.04–0.08 SOL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The expected-value math for a tip
&lt;/h2&gt;

&lt;p&gt;Jito bundles are all-or-nothing: if your bundle lands in a Jito block, your transaction is atomic with your tip, and no searcher can insert between your legs. If it &lt;em&gt;doesn't&lt;/em&gt; land, you're back on the public mempool and exposed.&lt;/p&gt;

&lt;p&gt;So the decision is a two-state lottery. Let:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;M&lt;/code&gt; = expected MEV loss if unprotected (SOL)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;t&lt;/code&gt; = your tip (SOL)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;p(t)&lt;/code&gt; = probability your bundle lands this slot given tip &lt;code&gt;t&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;f&lt;/code&gt; = transaction base fee (~0.000005 SOL, negligible)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expected cost of each path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;E[cost_bundle]   = p(t) * t + (1 - p(t)) * (t + M)
E[cost_nobundle] = M
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that in the &lt;code&gt;(1 - p(t))&lt;/code&gt; case, you still paid the tip if the bundle was submitted but not landed — &lt;strong&gt;this is the key bit most writeups miss&lt;/strong&gt;. Jito refunds tips only if the bundle is &lt;em&gt;rejected at ingress&lt;/em&gt;, not if it's simply outcompeted. Solving for the break-even:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p(t) * t + (1 - p(t)) * (t + M) &amp;lt; M
  =&amp;gt;  t + (1 - p(t)) * M &amp;lt; M
  =&amp;gt;  t &amp;lt; p(t) * M
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So tipping is EV-positive as long as &lt;code&gt;t &amp;lt; p(t) * M&lt;/code&gt;. The optimum under a convex &lt;code&gt;p(t)&lt;/code&gt; is where &lt;code&gt;dp/dt * M = 1&lt;/code&gt; — the marginal tip dollar buys exactly its value in landed probability.&lt;/p&gt;

&lt;p&gt;In practice &lt;code&gt;p(t)&lt;/code&gt; is empirically a logistic: very steep in the 50th–75th percentile of recent Jito tips, nearly flat above the 90th. You can pull the percentiles from &lt;code&gt;https://bundles.jito.wtf/api/v1/bundles/tip_floor&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-17T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"landed_tips_25th_percentile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0000095&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"landed_tips_50th_percentile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.00003&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"landed_tips_75th_percentile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.00015&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"landed_tips_95th_percentile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.00089&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"landed_tips_99th_percentile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.00412&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 50th percentile is where you become competitive; the 95th is where marginal spend stops buying much.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Python calculator
&lt;/h2&gt;

&lt;p&gt;Here's the whole thing. Runs against the live Jito endpoint, no paid deps.&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;jito_tip.py - closed-form break-even tip calculator.

Usage:
    python jito_tip.py --swap-sol 10 --pool-sol 1000 --slippage-bps 100

Assumes a constant-product pool. Returns the recommended tip in SOL.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;

&lt;span class="n"&gt;JITO_TIP_FLOOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://bundles.jito.wtf/api/v1/bundles/tip_floor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;TX_COST_SOL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0001&lt;/span&gt;  &lt;span class="c1"&gt;# two-sig bundle overhead, empirical
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_tip_percentiles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Pull live Jito landed-tip percentiles.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JITO_TIP_FLOOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&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;User-Agent&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;jito-tip/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p25&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;landed_tips_25th_percentile&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;p50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;landed_tips_50th_percentile&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;p75&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;landed_tips_75th_percentile&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;p95&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;landed_tips_95th_percentile&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;p99&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;landed_tips_99th_percentile&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;expected_mev_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;swap_sol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pool_sol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slippage_bps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Worst-case extractable value for a sandwich on a CP pool.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Ceiling: slippage * size
&lt;/span&gt;    &lt;span class="n"&gt;ceiling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;swap_sol&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slippage_bps&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Realistic: half of ceiling * pool-impact factor, minus searcher cost
&lt;/span&gt;    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;swap_sol&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;pool_sol&lt;/span&gt;
    &lt;span class="n"&gt;realistic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;swap_sol&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ceiling&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;realistic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;TX_COST_SOL&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;recommend_tip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mev_loss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;percentiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Pick a tip consistent with EV-positive bundling.

    Rule: tip at p75 if MEV &amp;gt;&amp;gt; p95, tip at p50 if MEV ~ p95,
    skip bundle if MEV &amp;lt; p50 (not worth the tip).
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;p50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p95&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;percentiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;percentiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p75&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;percentiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95&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;mev_loss&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;p50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&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;skip&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;tip_sol&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.0&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MEV too small — use public mempool or limit order&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;mev_loss&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;p95&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&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;tip&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;tip_sol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p50&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Competitive at median; EV-positive.&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;mev_loss&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;p95&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&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;tip&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;tip_sol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p75&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Meaningful MEV; tip at 75th percentile.&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&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;tip&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;tip_sol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mev_loss&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.25&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Large MEV; cap tip at 25% of MEV or p95, whichever is lower.&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;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--swap-sol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&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;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--pool-sol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&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;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--slippage-bps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;pcts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_tip_percentiles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expected_mev_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;swap_sol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pool_sol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slippage_bps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recommend_tip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pcts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&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;Expected MEV loss: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mev&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; SOL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&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;Jito tip percentiles: p50=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pcts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;p50&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; p75=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pcts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;p75&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; p95=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pcts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;p95&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&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="nf"&gt;print&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;Recommendation: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  tip=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tip_sol&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; SOL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&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;Reason: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rec&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="si"&gt;}&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;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tip_sol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mev&lt;/span&gt;
        &lt;span class="nf"&gt;print&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;Worst-case cost vs unprotected: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; SOL&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sample runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python jito_tip.py &lt;span class="nt"&gt;--swap-sol&lt;/span&gt; 0.5 &lt;span class="nt"&gt;--pool-sol&lt;/span&gt; 1000 &lt;span class="nt"&gt;--slippage-bps&lt;/span&gt; 100
&lt;span class="go"&gt;Expected MEV loss: 0.000050 SOL
Recommendation: SKIP  tip=0.000000 SOL
Reason: MEV too small — use public mempool or limit order

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python jito_tip.py &lt;span class="nt"&gt;--swap-sol&lt;/span&gt; 25 &lt;span class="nt"&gt;--pool-sol&lt;/span&gt; 1000 &lt;span class="nt"&gt;--slippage-bps&lt;/span&gt; 100
&lt;span class="go"&gt;Expected MEV loss: 0.312300 SOL
Recommendation: TIP  tip=0.000890 SOL
&lt;/span&gt;&lt;span class="gp"&gt;Reason: Large MEV;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cap tip at 25% of MEV or p95, whichever is lower.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second case is the textbook bundle use case: expected loss ~0.31 SOL, tip ~0.00089 SOL — 350x leverage on defensive spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Jito is the wrong tool
&lt;/h2&gt;

&lt;p&gt;The math above only holds when the &lt;em&gt;specific&lt;/em&gt; MEV you're defending against is extractable in a single block. Three cases where it isn't, and bundles become a tax instead of a shield:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Slow liquidity bleed.&lt;/strong&gt; If your trade is a 100 SOL unwind into a 500 SOL pool, the problem isn't sandwiches — it's that half your book has no bid. Jito doesn't conjure liquidity. The right tool is a TWAP across 30+ slots, or a routed order through a DEX aggregator that splits across pools. A bundle on a multi-block schedule is just paying the tip &lt;code&gt;n&lt;/code&gt; times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Stale-oracle arbitrage.&lt;/strong&gt; If you're swapping a low-volume SPL token and your slippage tolerance is 5%+, the dominant attack isn't a sandwich — it's an oracle-gate mismatch. A Pyth/Switchboard staleness check on the router side (reject if oracle age &amp;gt; N seconds) buys more safety than any tip. I wrote the gating heuristic up in the MEV-deep-dive chapter on &lt;a href="https://cipher-x402.vercel.app/premium/mev-deep-dive" rel="noopener noreferrer"&gt;cipher-x402&lt;/a&gt;; even without reading it, the rule is: reject if &lt;code&gt;now - oracle_publish_time &amp;gt; 2 * slot_time&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Limit-order-beats-market.&lt;/strong&gt; If your edge is "I want to buy at price X or better," a Raydium CLMM limit order, Jupiter Limit, or DFlow limit book executes at-or-better without you ever paying a tip. Limit orders are immune to sandwiches &lt;em&gt;by construction&lt;/em&gt; because there's no price-impact window to exploit — the searcher can't improve a passive quote against themselves. The right threshold: if your required slippage is under 0.5% and you're trading over the next ~5 minutes, use a limit. If it's over 2% or under 10 seconds, use a bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The threshold table
&lt;/h2&gt;

&lt;p&gt;Rolling up the decision rule into a cheat-sheet:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Swap as % of pool&lt;/th&gt;
&lt;th&gt;Slippage tolerance&lt;/th&gt;
&lt;th&gt;Urgency&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 0.1%&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;Public mempool, no tip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1% – 0.5%&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.5%&lt;/td&gt;
&lt;td&gt;minutes&lt;/td&gt;
&lt;td&gt;Limit order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1% – 0.5%&lt;/td&gt;
&lt;td&gt;&amp;gt; 0.5%&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;Jito bundle @ p50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.5% – 2%&lt;/td&gt;
&lt;td&gt;&amp;lt; 1%&lt;/td&gt;
&lt;td&gt;minutes&lt;/td&gt;
&lt;td&gt;Limit order + oracle gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.5% – 2%&lt;/td&gt;
&lt;td&gt;&amp;gt; 1%&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;Jito bundle @ p75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt; 2%&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;TWAP + Jito per leg @ p75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;any, low-liq SPL&lt;/td&gt;
&lt;td&gt;&amp;gt; 2%&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;Oracle-gate &lt;em&gt;required&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;&amp;gt; 2%&lt;/code&gt; row deserves a note. If you're moving 2%+ of a pool in one slot, searchers will rebid your tip and the auction goes non-monotonic — you can tip the 99th percentile and still lose because a better-capitalized searcher bids higher. At that size, your actual defense is &lt;em&gt;not being a single-block event&lt;/em&gt;. Slice the order, and the aggregate MEV drops roughly linearly while the per-slice tip drops quadratically (because &lt;code&gt;k²&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What the calculator doesn't model
&lt;/h2&gt;

&lt;p&gt;Three honest limitations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cross-pool routing.&lt;/strong&gt; If Jupiter splits your trade across four pools, the sandwich surface is four pools, not one. My estimator treats it as one pool with the aggregate liquidity, which &lt;em&gt;underestimates&lt;/em&gt; MEV by ~15% in my measurements. Fix: pass &lt;code&gt;--pool-sol&lt;/code&gt; as the minimum of the routed legs, not the sum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private-order-flow.&lt;/strong&gt; If you're routing through DFlow or Helius Sender with private mempools, the MEV surface collapses — but so does the benefit of Jito tipping. Don't double-pay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validator collusion.&lt;/strong&gt; In the 99.5th-percentile tail, the landed-tip distribution thickens because a handful of validators have structural tip advantages. The model assumes a roughly i.i.d. auction; the reality is bimodal. If you're consistently losing above p95, read Jito's leader schedule and time your submissions.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to pull from this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MEV scales with &lt;code&gt;s²&lt;/code&gt;, tips with EV. Tip budget = a fraction of expected MEV, not a fraction of trade size.&lt;/li&gt;
&lt;li&gt;The 50th percentile is competitive; the 95th is diminishing returns; above that you're paying for validator preference, not protection.&lt;/li&gt;
&lt;li&gt;Bundles are not a cure-all. Limit orders dominate for low-slippage work. TWAP dominates for large unwinds. Oracle gates dominate for low-liq SPLs.&lt;/li&gt;
&lt;li&gt;Measure, don't fold a "10%-of-trade" tip into your router. That's how you bleed 10 bps a day to Jito forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full playbook with the other 9 defenses (three-tier wallet, Cloudflare Tunnel, Oracle Cloud Always Free stack) is at &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter on GitHub&lt;/a&gt;. The MEV-deep-dive chapter with the $1k test matrix and the oracle-gate BPS formula is &lt;a href="https://cipher-x402.vercel.app/premium/mev-deep-dive" rel="noopener noreferrer"&gt;behind x402&lt;/a&gt; at $0.25 USDC on Base. If you want the prior pieces, the &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910"&gt;10-findings post&lt;/a&gt; covers what building the playbook taught me.&lt;/p&gt;

&lt;p&gt;Feedback and corrections welcome — especially if you have measured &lt;code&gt;p(t)&lt;/code&gt; data from different slots. I'm not a Jito insider, and the logistic-fit assumption is the soft spot in the calculator.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>mev</category>
      <category>crypto</category>
      <category>python</category>
    </item>
    <item>
      <title>I shipped an x402 AI-crawler paywall in 3 hours on Vercel's free tier</title>
      <dc:creator>Sai</dc:creator>
      <pubDate>Fri, 17 Apr 2026 08:41:21 +0000</pubDate>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910/i-shipped-an-x402-ai-crawler-paywall-in-3-hours-on-vercels-free-tier-272m</link>
      <guid>https://dev.to/sai_93caeceb4f6a4d9969910/i-shipped-an-x402-ai-crawler-paywall-in-3-hours-on-vercels-free-tier-272m</guid>
      <description>&lt;p&gt;Last night I shipped a working &lt;a href="https://www.x402.org" rel="noopener noreferrer"&gt;x402&lt;/a&gt; AI-crawler paywall in 3 hours, on $0, with nothing but Next.js 16 + Vercel's hobby tier + a fresh Base keypair I generated on disk. The gated endpoint is live at &lt;a href="https://cipher-x402.vercel.app/premium/mev-deep-dive" rel="noopener noreferrer"&gt;https://cipher-x402.vercel.app/premium/mev-deep-dive&lt;/a&gt; — curl it right now, you'll get a clean HTTP 402 with the v2 accept-list body.&lt;/p&gt;

&lt;p&gt;This is the part most devs aren't grokking yet: x402 isn't a donation button, it's a paywall &lt;strong&gt;aimed at AI agents, not humans&lt;/strong&gt;. Claude, GPT, Perplexity and the new wave of crawler-agents already know how to read a 402 response, negotiate payment, and retry — the entire ergonomic handshake is built into the protocol. If your content is AI-crawler-worthy (research, data, code, write-ups), you can just price it per request and let the agents pay you while they work.&lt;/p&gt;

&lt;p&gt;Here's the full stack I used, and the three things that almost killed the deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually running
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework&lt;/strong&gt;: Next.js 16 (Turbopack) with the new &lt;code&gt;proxy.ts&lt;/code&gt; file convention. This caught me — &lt;code&gt;middleware.ts&lt;/code&gt; has been deprecated, and the &lt;code&gt;paymentProxy&lt;/code&gt; helper from &lt;code&gt;@x402/next&lt;/code&gt; v2 exports a function that must be renamed to &lt;code&gt;proxy&lt;/code&gt; (not &lt;code&gt;middleware&lt;/code&gt;) for Next 16 to find it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facilitator&lt;/strong&gt;: Coinbase CDP at &lt;code&gt;https://api.cdp.coinbase.com/platform/v2/x402&lt;/code&gt;. I don't actually have CDP API keys yet (need to sign up on their portal, which is KYC-friction I'm deferring), so for v1 I'm running a &lt;strong&gt;hand-rolled 402 proxy&lt;/strong&gt; that returns the v2 accept-list body with no verification. When a real payment header eventually arrives, I pass it through to the route handler and log it. The facilitator plugs in when I flip on CDP_API_KEY_ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settlement&lt;/strong&gt;: USDC on Base mainnet (&lt;code&gt;eip155:8453&lt;/code&gt;). Asset contract &lt;code&gt;0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&lt;/code&gt;. Fresh keypair generated with &lt;code&gt;eth-account&lt;/code&gt;, V3 keystore encrypted with a urlsafe 32-byte passphrase, stored outside any git repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price&lt;/strong&gt;: $0.25 USDC per request. That's &lt;code&gt;250000&lt;/code&gt; in &lt;code&gt;maxAmountRequired&lt;/code&gt; (USDC has 6 decimals).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting&lt;/strong&gt;: Vercel Hobby tier, free. One thing to watch: new projects have SSO deployment protection on by default. The protected deploy returns 401 to the public, which breaks x402 discovery. Flip it off with &lt;code&gt;PATCH /v9/projects/&amp;lt;id&amp;gt;?teamId=&amp;lt;team&amp;gt;&lt;/code&gt; with &lt;code&gt;{"ssoProtection": null}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The proxy (full code)
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;@x402/next&lt;/code&gt; v2 &lt;code&gt;paymentProxy&lt;/code&gt; helper is the right call when you actually have the Coinbase CDP facilitator wired up. Without CDP keys at deploy time the middleware blew up with &lt;code&gt;MIDDLEWARE_INVOCATION_FAILED&lt;/code&gt;, so I replaced it with a minimal hand-roll:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&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;PAY_TO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&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;X402_RECIPIENT_ADDRESS&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0xa0630fAD18C732e94D56d2D5F630963eb8fB9640&lt;/span&gt;&lt;span class="dl"&gt;"&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;X402_ACCEPTS&lt;/span&gt; &lt;span class="o"&gt;=&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;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;accepts&lt;/span&gt;&lt;span class="p"&gt;:&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;eip155:8453&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;250000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cipher-x402.vercel.app/premium/mev-deep-dive&lt;/span&gt;&lt;span class="dl"&gt;"&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;CIPHER premium chapter: MEV Deep Dive — Jito tip math, oracle-gate bps...&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/markdown&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;PAY_TO&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="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/premium/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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;paymentHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-payment&lt;/span&gt;&lt;span class="dl"&gt;"&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;paymentHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&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="nx"&gt;X402_ACCEPTS&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;402&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cache-control&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;no-store, max-age=0&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;x-402-version&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;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;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;matcher&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;/premium/:path*&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. 35 lines including imports. The v2 accept-list JSON is what x402scan (&lt;code&gt;https://www.x402scan.com/resources/register&lt;/code&gt;) indexes, and what every x402-aware agent parses to decide whether to pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three things that almost killed the deploy
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backticks inside a TypeScript template literal&lt;/strong&gt; served as the gated content. My premium chapter is a 5,000-word markdown string stored as &lt;code&gt;export const CONTENT = \&lt;/code&gt;...markdown...&lt;code&gt;— and I forgot that every inline-code backtick in markdown terminates the template literal early. Turbopack threw `Expected a semicolon` at line 51. Fix: escape every&lt;/code&gt; &lt;code&gt;&lt;/code&gt;&lt;code&gt;to&lt;/code&gt;&lt;code&gt;\&lt;/code&gt; `&lt;code&gt;inside the body before writing. 46 backticks escaped with one&lt;/code&gt;.replace()`.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;middleware.ts&lt;/code&gt; vs &lt;code&gt;proxy.ts&lt;/code&gt; in Next 16&lt;/strong&gt;. The Next 16 Turbopack build ran fine but Vercel returned &lt;code&gt;MIDDLEWARE_INVOCATION_FAILED&lt;/code&gt; at runtime. Turns out Next 16 reads the file named &lt;code&gt;proxy.ts&lt;/code&gt; with an exported &lt;code&gt;proxy&lt;/code&gt; function. Keeping the old &lt;code&gt;middleware.ts&lt;/code&gt; name still compiles (with a warning) but fails at invocation. Rename the file AND the exported symbol.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vercel SSO deployment protection&lt;/strong&gt;. The deploy returned 401 Unauthorized to every public curl. This is on by default for new Hobby projects — intended to let you preview before going public. For x402 to work, the endpoint &lt;em&gt;has&lt;/em&gt; to be publicly reachable (AI agents can't do SSO). Disable via the API:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;`shell&lt;br&gt;
curl -X PATCH \&lt;br&gt;
  -H "Authorization: Bearer $VERCEL_TOKEN" \&lt;br&gt;
  -H "Content-Type: application/json" \&lt;br&gt;
  "https://api.vercel.com/v9/projects/&amp;lt;PROJECT_ID&amp;gt;?teamId=&amp;lt;TEAM_ID&amp;gt;" \&lt;br&gt;
  -d '{"ssoProtection":null}'&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Instant public access after that.&lt;/p&gt;

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

&lt;p&gt;I've been sitting on a 150-page Solana quant playbook (&lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cipher-starter&lt;/a&gt;, MIT) for a week. The public bundle is free on GitHub. But the &lt;strong&gt;expansion content&lt;/strong&gt; — the MEV Deep Dive chapter with Jito tip math, dynamic oracle-gate bps tuning, 72-hour $1k test matrix — was sitting in my notes with no delivery mechanism.&lt;/p&gt;

&lt;p&gt;Paid content delivery for indie devs is a mess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gumroad / Ko-fi / BuyMeACoffee want manual checkout per buyer&lt;/li&gt;
&lt;li&gt;Substack gates newsletters, not APIs&lt;/li&gt;
&lt;li&gt;Stripe needs business-entity KYC&lt;/li&gt;
&lt;li&gt;Gitcoin-style tipping is voluntary, not per-request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;x402 is the first delivery mechanism where an &lt;strong&gt;AI agent crawling for research&lt;/strong&gt; can literally pay me per crawl, automatically, with no checkout UI and no trust relationship. The break-even is absurdly low. If Claude-for-research indexes my MEV chapter twice a week to answer "how do I defend against MEV on Solana" questions, that's $2/week passive — enough to pay for the hosting that serves a thousand other free crawlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full autonomous distribution flow
&lt;/h2&gt;

&lt;p&gt;I'm doing this solo, autonomously, on a $0 budget, and tracking everything publicly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free bundle (MIT): &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;github.com/cryptomotifs/cipher-starter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Landing page: &lt;a href="https://cryptomotifs.github.io/cipher-starter/" rel="noopener noreferrer"&gt;cryptomotifs.github.io/cipher-starter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;x402 paywall: &lt;a href="https://cipher-x402.vercel.app" rel="noopener noreferrer"&gt;cipher-x402.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Previous writeup (the 10 findings from the solo-dev playbook build): &lt;a href="https://dev.to/sai_93caeceb4f6a4d9969910/i-built-a-solana-signal-engine-solo-heres-the-150-page-playbook-246k"&gt;dev.to/sai_93caeceb4f6a4d9969910/i-built-a-solana-signal-engine-solo-heres-the-150-page-playbook-246k&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next steps I'm shipping
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Flip on CDP_API_KEY_ID once I clear the Coinbase signup queue → facilitator verifies payments, agent gets the 5k-word chapter only after USDC lands.&lt;/li&gt;
&lt;li&gt;Add 3 more gated chapters (Security, Risk, Compliance) — each $0.25, priced-per-fetch.&lt;/li&gt;
&lt;li&gt;Write a thin Python client so other devs can test paying my endpoint (&lt;code&gt;x402&lt;/code&gt; protocol v2 has no Python SDK yet — low-hanging fruit).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building indie-dev research content and haven't looked at x402 yet, the next month is when the crawler-agent economy actually settles. The pre-x402 world was "how do I get humans to click buy" — the post-x402 world is "how do I price my API high enough that Claude-the-researcher won't bankrupt me."&lt;/p&gt;

&lt;p&gt;Ship early. Ship with hand-rolled middleware if you have to.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not investment advice. Not a signal subscription. Not financial counsel. Engineering notes only.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>x402</category>
      <category>solana</category>
      <category>crypto</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>I built a Solana signal engine solo. Here's the 150-page playbook.</title>
      <dc:creator>Sai</dc:creator>
      <pubDate>Fri, 17 Apr 2026 07:28:42 +0000</pubDate>
      <link>https://dev.to/sai_93caeceb4f6a4d9969910/i-built-a-solana-signal-engine-solo-heres-the-150-page-playbook-246k</link>
      <guid>https://dev.to/sai_93caeceb4f6a4d9969910/i-built-a-solana-signal-engine-solo-heres-the-150-page-playbook-246k</guid>
      <description>&lt;p&gt;Four months ago I started building a Solana-native signal engine + autonomous trading bot solo, on a $0/mo infrastructure budget, in Canada.&lt;/p&gt;

&lt;p&gt;I dispatched ~10 parallel senior-role analyses along the way — one as Head of Trading, one as Head of Risk, one as Security Engineer, one as Compliance (Canadian), one as CTO / architect, one as Head of Revenue, one as Head of Ops — to force every decision to be defended from its own expert lens.&lt;/p&gt;

&lt;p&gt;The research compressed into 12 playbooks, ~150 pages total. I published the entire bundle publicly on GitHub today and priced the convenience PDF + Discord access at $9 pay-what-you-want in SOL. Repo: &lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cryptomotifs/cipher-starter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is the distilled version — 10 key findings that surprised me or cost me the most when I got them wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Your old bot wallets are probably compromised
&lt;/h2&gt;

&lt;p&gt;Salvage audit of &lt;code&gt;~/Downloads/&lt;/code&gt; found my prior Solana bot projects stored raw base58 private keys, mnemonic phrases as comments, and encryption passwords all in plaintext &lt;code&gt;.env&lt;/code&gt; files — with &lt;code&gt;.gitignore&lt;/code&gt; missing &lt;code&gt;.env&lt;/code&gt; in some cases. Two specific wallet addresses that were "creator" / "trader" identities were exposed.&lt;/p&gt;

&lt;p&gt;Before any new bot touches real money: sweep via CEX hop to fresh addresses. Don't overwrite the compromised wallets — they're dead forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. MEV sandwich tax is ~40%/year if you don't mitigate
&lt;/h2&gt;

&lt;p&gt;The biggest non-obvious drag at $1k scale isn't strategy — it's the MEV sandwich tax. Estimated 40%/yr annualised bleed on naive public-mempool trades. Required defenses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jito bundles&lt;/strong&gt; — tip-based inclusion, never public mempool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit orders where possible&lt;/strong&gt; — even 50bp above spot saves the sandwich&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Illiquidity blocklist&lt;/strong&gt; — skip tokens with &amp;lt; $1M pool depth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle gate&lt;/strong&gt; — reject trades where Jupiter quote &amp;gt; 0.5% off Pyth spot&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Three-tier wallet architecture at $1k scale
&lt;/h2&gt;

&lt;p&gt;Single-wallet = single-drain risk. Two-tier = better but still bot-controlled cold. The defensible split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$100 hot&lt;/strong&gt; — bot-signing wallet, KMS envelope-encrypted seed, isolated signer subprocess with program allowlist + daily spend cap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$300 warm&lt;/strong&gt; — manual-top-up buffer on founder's phone (Phantom Secure Enclave)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$600 cold&lt;/strong&gt; — untouchable for ≥6 months, Ledger Nano S Plus or Squads 2-of-2 multisig&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Single-incident max loss = $100. Total drain requires compromising 2+ physically-separated factors.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. No perps at $1k capital
&lt;/h2&gt;

&lt;p&gt;My first instinct was to use Drift / Zeta / Hyperliquid for leverage. The Risk playbook vetoed it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Liquidation cascade on even 5x leverage can wipe a position before the bot's stop-loss monitor polls&lt;/li&gt;
&lt;li&gt;Protocol insolvency risk (has happened)&lt;/li&gt;
&lt;li&gt;Funding rate compounding on multi-day holds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At $10k+ capital, perps with 2-3x max are fine. At $1k, spot-only via Jupiter.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Canadian NI 31-103 exemption is narrower than people think
&lt;/h2&gt;

&lt;p&gt;If you're Canadian and planning to sell signal subscriptions, the compliance path is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trading your own money = zero registration needed (not CIRO, not OSC, not FINTRAC)&lt;/li&gt;
&lt;li&gt;Selling signals = must position as "quantitative market data + research content" (NI 31-103 exemption)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never&lt;/strong&gt; say "we recommend"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never&lt;/strong&gt; personalize to user finances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never&lt;/strong&gt; custody customer funds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never&lt;/strong&gt; co-sign customer wallets / copy-trade&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those hard lines triggers Portfolio Manager / Investment Fund Manager / MSB registration (~CAD $500k/yr combined).&lt;/p&gt;

&lt;h2&gt;
  
  
  6. SR&amp;amp;ED R&amp;amp;D credit is the hidden goldmine for solo Canadian devs
&lt;/h2&gt;

&lt;p&gt;35-43% refundable tax credit on imputed founder-salary rate for R&amp;amp;D spend. For 4 months of design docs + commit history, plausible claim is $3-10k as a sole proprietor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start the logbook day 1&lt;/strong&gt; — every sprint file, design decision, technical-uncertainty workaround = evidence. Likely outvalues 12-24 months of $1k trading P&amp;amp;L.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Oracle Cloud Always Free is underrated
&lt;/h2&gt;

&lt;p&gt;4 ARM cores + 24 GB RAM + 200 GB storage, forever free. Nobody talks about this because it's not AWS.&lt;/p&gt;

&lt;p&gt;Deploy pattern: systemd native (not Docker in prod at this scale), SQLite WAL → Neon Postgres at 500MB, Cloudflare Tunnel (no open ports), Grafana Cloud Free for logs/metrics/traces, Sentry Free for errors, BetterStack for uptime, Healthchecks.io for cron heartbeats.&lt;/p&gt;

&lt;p&gt;Total: &lt;strong&gt;$0/mo at zero P&amp;amp;L, ≤$45/mo at $5k P&amp;amp;L&lt;/strong&gt;. That's lower than the $105/mo SaaS-stack typical indie-hacker setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. 30-day paper-trade gate before live capital, no exceptions
&lt;/h2&gt;

&lt;p&gt;The hardest rule to enforce. Every solo founder's instinct is to "just try live with $50." The gate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;30 consecutive days of paper trading on real Jupiter quotes (not backtest)&lt;/li&gt;
&lt;li&gt;Sharpe ≥ 0.8&lt;/li&gt;
&lt;li&gt;Max drawdown &amp;lt; 12%&lt;/li&gt;
&lt;li&gt;All 7 P0 trading modules shipped (wallet, jupiter_client, isolated tx_signer, jito_client, executor, emergency_halt, pnl_tracker)&lt;/li&gt;
&lt;li&gt;CircuitBreaker fault-injection tests pass&lt;/li&gt;
&lt;li&gt;72h Oracle Cloud uptime met&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Missing any = extend paper. Force-going-live at -5% paper Sharpe is how $1000 turns into $600.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Which prior Solana bot code is salvageable
&lt;/h2&gt;

&lt;p&gt;Audited 4 prior bot projects in &lt;code&gt;~/Downloads/&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;sol-volume-bot-v3&lt;/strong&gt; (Node.js) — most reusable, verified on-chain bundle-landing success. Lines 188-236 of &lt;code&gt;index.js&lt;/code&gt; = Jito bundle landing + signature idempotence. Port to Python.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;solana-arb-bot&lt;/strong&gt; (Rust) — &lt;code&gt;crates/predator-execution/{jito,simulator,alt,ata}.rs&lt;/code&gt; are gold, port to Python. Skip all strategy crates (memecoin/MEV, landed 0 bundles in 8 days).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic "Solana Trading Bot" folder&lt;/strong&gt; — 500-file monolith that made $1.45. Skip entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;140 of 151 phase backup directories&lt;/strong&gt; — zero-byte &lt;code&gt;nul&lt;/code&gt; files from failed robocopies. Delete.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Subscription launch gate (explicit)
&lt;/h2&gt;

&lt;p&gt;Don't launch paid signals until ALL three are true:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;30 consecutive days of live (not paper) P&amp;amp;L published&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cumulative net-of-fees P&amp;amp;L positive OR live Sharpe ≥ 0.5&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;50+ email subs OR 200+ Twitter followers&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Earliest plausible = Day 61, target = Day 90. Launch &lt;code&gt;$29&lt;/code&gt; tier only at first — not the full $29/$49/$79/$249 ladder.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I'm sharing this
&lt;/h2&gt;

&lt;p&gt;The 150-page bundle is public on GitHub (&lt;a href="https://github.com/cryptomotifs/cipher-starter" rel="noopener noreferrer"&gt;cryptomotifs/cipher-starter&lt;/a&gt;). Read any of the 12 playbooks directly.&lt;/p&gt;

&lt;p&gt;I priced the PDF + Discord at $9 pay-what-you-want (Solana only) as a signal — the research is done, but v2 (backtest results + 30-day live paper-trade data) depends on validation that anyone found this useful.&lt;/p&gt;

&lt;p&gt;Landing page + QR code: &lt;a href="https://cryptomotifs.github.io/cipher-starter/" rel="noopener noreferrer"&gt;https://cryptomotifs.github.io/cipher-starter/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback welcome — especially on what's missing for v2.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not investment advice. Not a signal subscription. You build your own bot. Risk is yours.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>crypto</category>
      <category>python</category>
      <category>algotrading</category>
    </item>
  </channel>
</rss>
