<?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: Gen.ishinabe</title>
    <description>The latest articles on DEV Community by Gen.ishinabe (@gen_ishinabe).</description>
    <link>https://dev.to/gen_ishinabe</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%2F3809317%2Fd8a410ae-a833-4a8f-9164-6cf4683dcade.png</url>
      <title>DEV Community: Gen.ishinabe</title>
      <link>https://dev.to/gen_ishinabe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gen_ishinabe"/>
    <language>en</language>
    <item>
      <title>Adding Solana Payments to ElizaOS: What I Learned About SSRF, Floating-Point, and IPv6</title>
      <dc:creator>Gen.ishinabe</dc:creator>
      <pubDate>Fri, 06 Mar 2026 07:56:21 +0000</pubDate>
      <link>https://dev.to/gen_ishinabe/adding-solana-payments-to-elizaos-what-i-learned-about-ssrf-floating-point-and-ipv6-15kh</link>
      <guid>https://dev.to/gen_ishinabe/adding-solana-payments-to-elizaos-what-i-learned-about-ssrf-floating-point-and-ipv6-15kh</guid>
      <description>&lt;p&gt;I spent the past couple of weeks adding Solana USDC payment support to ElizaOS via the x402 protocol. The payment flow itself was straightforward. The security and edge cases were not. This post is mostly about the latter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/elizaos/eliza" rel="noopener noreferrer"&gt;ElizaOS&lt;/a&gt; is an AI agent framework popular in the Solana ecosystem. The &lt;a href="https://www.x402.org/" rel="noopener noreferrer"&gt;x402 protocol&lt;/a&gt; (by Coinbase) lets HTTP clients pay for API calls automatically — when a server responds with &lt;code&gt;402 Payment Required&lt;/code&gt;, the client signs a USDC transfer and retries.&lt;/p&gt;

&lt;p&gt;There was already an EVM-only x402 plugin for ElizaOS (&lt;code&gt;@elizaos/plugin-x402&lt;/code&gt;). Coinbase had also shipped &lt;code&gt;@x402/svm&lt;/code&gt; — a Solana client implementation. But nobody had connected the two. So I did.&lt;/p&gt;

&lt;p&gt;The plugin itself is about 500 lines across 6 files. The interesting part wasn't wiring up the payment — it was everything that could go wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IPv6 hex normalization problem
&lt;/h2&gt;

&lt;p&gt;An ElizaOS agent takes URLs from conversation. That makes it an SSRF vector. A prompt injection could tell the agent to fetch &lt;code&gt;https://169.254.169.254/latest/meta-data/&lt;/code&gt; (the AWS metadata endpoint), or any internal service.&lt;/p&gt;

&lt;p&gt;Blocking private IP ranges sounds simple. It isn't.&lt;/p&gt;

&lt;p&gt;Node.js normalizes IPv4-mapped IPv6 addresses into hex. If someone passes &lt;code&gt;::ffff:127.0.0.1&lt;/code&gt;, Node resolves it and stores the IP as &lt;code&gt;::ffff:7f00:1&lt;/code&gt;. A regex checking for &lt;code&gt;127.&lt;/code&gt; never sees the string "127" — it's been rewritten to &lt;code&gt;7f00:1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix: parse the resolved address, detect &lt;code&gt;::ffff:&lt;/code&gt; prefixed hex, extract the four hex bytes, convert back to dotted decimal, and then run the private-range check against that.&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;// ::ffff:7f00:1 → extract 7f00 and 0001 → 127.0.0.1&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;::ffff:&lt;/span&gt;&lt;span class="dl"&gt;"&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="c1"&gt;// "7f00:1"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&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="mi"&gt;16&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;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&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="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&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;ipv4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches &lt;code&gt;::ffff:7f00:1&lt;/code&gt;, &lt;code&gt;::ffff:a9fe:a9fe&lt;/code&gt; (169.254.x.x), &lt;code&gt;::ffff:ac10:fe01&lt;/code&gt; (172.16.x.x), and every other mapped private address.&lt;/p&gt;

&lt;h2&gt;
  
  
  The floating-point trap at exactly $0.005
&lt;/h2&gt;

&lt;p&gt;USDC on Solana has 6 decimal places. To convert dollars to micro-units:&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.005&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Expected: 5000. Actual: 4999.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IEEE 754 represents &lt;code&gt;0.005&lt;/code&gt; as &lt;code&gt;0.00499999999999999...&lt;/code&gt;. &lt;code&gt;Math.floor&lt;/code&gt; truncates it to &lt;code&gt;4999&lt;/code&gt;. This means a payment policy with a &lt;code&gt;$0.005&lt;/code&gt; max would silently reject legitimate &lt;code&gt;$0.005&lt;/code&gt; charges — which happens to be the most common price point for x402 APIs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Math.round&lt;/code&gt; fixes it. But it's the kind of bug that passes every test unless you specifically test the boundary value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redirect chains bypass domain allowlists
&lt;/h2&gt;

&lt;p&gt;The plugin has a domain allowlist. But if &lt;code&gt;allowed-domain.com&lt;/code&gt; returns a &lt;code&gt;302&lt;/code&gt; to &lt;code&gt;evil.com&lt;/code&gt;, the default &lt;code&gt;fetch&lt;/code&gt; behavior follows the redirect transparently. The domain check passed on the original URL; the actual request hits a completely different host.&lt;/p&gt;

&lt;p&gt;The fix is &lt;code&gt;redirect: "error"&lt;/code&gt; in the fetch options. Any redirect becomes an exception instead of being followed silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't trust Content-Length
&lt;/h2&gt;

&lt;p&gt;A malicious server can set &lt;code&gt;Content-Length: 42&lt;/code&gt; and then stream gigabytes. If you allocate a buffer based on the header and then read the body, you're at the mercy of the server.&lt;/p&gt;

&lt;p&gt;The response body reader ignores &lt;code&gt;Content-Length&lt;/code&gt; entirely. It reads chunks incrementally with a hard 4MB cap. If the stream exceeds that, it aborts.&lt;/p&gt;

&lt;h2&gt;
  
  
  USDC-only payment policy
&lt;/h2&gt;

&lt;p&gt;The x402 protocol is token-agnostic — a server can request payment in any SPL token. Without validation, a compromised or malicious server could request 1 unit of some high-value token instead of USDC. The payment policy rejects anything that isn't USDC on Solana mainnet:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asset&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;USDC_SOLANA_MAINNET&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Only USDC payments accepted&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;h2&gt;
  
  
  @solana/kit v2 vs @solana/web3.js keypair formats
&lt;/h2&gt;

&lt;p&gt;ElizaOS uses &lt;code&gt;@solana/web3.js&lt;/code&gt; v1, where a keypair is a &lt;code&gt;Uint8Array(64)&lt;/code&gt; (32 bytes secret + 32 bytes public). The &lt;code&gt;@x402/svm&lt;/code&gt; client expects &lt;code&gt;@solana/kit&lt;/code&gt; (the v2 rewrite), where a &lt;code&gt;CryptoKeyPair&lt;/code&gt; is an opaque object created via &lt;code&gt;createKeyPairFromBytes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The migration isn't just a type change — &lt;code&gt;createKeyPairFromBytes&lt;/code&gt; is async, the key extraction methods are async, and the underlying representation uses the Web Crypto API. Getting the two to coexist required converting at the boundary rather than trying to share a single keypair type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circular references and state isolation
&lt;/h2&gt;

&lt;p&gt;ElizaOS plugins share a single runtime. If &lt;code&gt;client.ts&lt;/code&gt; imports from &lt;code&gt;index.ts&lt;/code&gt; to access initialization state, and &lt;code&gt;index.ts&lt;/code&gt; imports from &lt;code&gt;client.ts&lt;/code&gt; to set up the client, you get a circular dependency that silently resolves to &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix was extracting shared state into &lt;code&gt;state.ts&lt;/code&gt; using a &lt;code&gt;WeakMap&lt;/code&gt; keyed by the runtime instance. Both files import from &lt;code&gt;state.ts&lt;/code&gt;; neither imports from each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @hugen/plugin-x402-solana
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"@hugen/plugin-x402-solana"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"secrets"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"SOLANA_PRIVATE_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-base58-keypair"&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;span class="p"&gt;}&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 agent can then call any x402 API that accepts Solana USDC. When the server responds with 402, the plugin handles the payment and retry automatically.&lt;/p&gt;

&lt;p&gt;Code is MIT licensed: &lt;a href="https://github.com/bartonguestier1725-collab/eliza-plugin-x402-solana" rel="noopener noreferrer"&gt;github.com/bartonguestier1725-collab/eliza-plugin-x402-solana&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're building x402 integrations and hit similar edge cases, I'm happy to compare notes in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
