<?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: Suliman Mokhtar</title>
    <description>The latest articles on DEV Community by Suliman Mokhtar (@sulimanmukhtar).</description>
    <link>https://dev.to/sulimanmukhtar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1307777%2F32838a04-093e-419d-8f20-92bb2143ec84.jpeg</url>
      <title>DEV Community: Suliman Mokhtar</title>
      <link>https://dev.to/sulimanmukhtar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sulimanmukhtar"/>
    <language>en</language>
    <item>
      <title>Zero-Latency DeFi: Parsing Raw Solana AMM Accounts in Rust</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Tue, 01 Sep 2026 06:55:03 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/zero-latency-defi-parsing-raw-solana-amm-accounts-in-rust-c2g</link>
      <guid>https://dev.to/sulimanmukhtar/zero-latency-defi-parsing-raw-solana-amm-accounts-in-rust-c2g</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/zero-latency-defi-parsing" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In high-frequency Web3 infrastructure, relying on a TypeScript SDK or a third-party pricing API means you are &lt;strong&gt;already too late.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are building an arbitrage bot, a sniper, or a real-time indexer on Solana, reading data through abstracted REST endpoints introduces hundreds of milliseconds of latency. To build institutional-grade infrastructure, you have to bypass the middleman. You need to pull the raw binary state of the Automated Market Maker (AMM) directly from the RPC node and deserialize it natively in memory.&lt;/p&gt;

&lt;p&gt;Here is how to reverse-engineer Solana DeFi pools and parse raw account data in Rust at &lt;strong&gt;microsecond speed.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Anatomy of a Solana Account &amp;amp; The Anchor Discriminator
&lt;/h2&gt;

&lt;p&gt;Beneath the abstractions of the Solana ecosystem, an account's data is fundamentally just a continuous array of bytes (&lt;code&gt;&amp;amp;[u8]&lt;/code&gt;). When a smart contract writes to an account, it serializes its state into this raw byte buffer.&lt;/p&gt;

&lt;p&gt;If the AMM was built using the &lt;strong&gt;Anchor framework&lt;/strong&gt; — which the vast majority of modern Solana DeFi protocols are — the account data doesn't just start with the struct variables. Anchor prepends an &lt;strong&gt;8-byte discriminator&lt;/strong&gt; to the beginning of the data payload.&lt;/p&gt;

&lt;p&gt;This discriminator is calculated using the first 8 bytes of the SHA256 hash of the string &lt;code&gt;"account:StructName"&lt;/code&gt;. It acts as a safety check: if you try to deserialize an AMM pool account but the first 8 bytes don't match the expected hash, the program knows you passed the wrong account type and immediately aborts.&lt;/p&gt;

&lt;p&gt;Bytes 0–7 (Anchor Discriminator) → Bytes 8–N (Raw struct data (Borsh-serialized))&lt;/p&gt;

&lt;p&gt;To parse the account data yourself, your first step is always identifying and &lt;strong&gt;slicing off those first 8 bytes.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Reverse-Engineering the AMM Struct
&lt;/h2&gt;

&lt;p&gt;You cannot parse binary data without knowing its exact memory layout. We need to map the byte layout of the DeFi pool — such as a Raydium CPMM or a pump.fun bonding curve — into a tightly packed Rust &lt;code&gt;struct&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of paying the "Borsh tax" (the CPU overhead of standard deserialization), we will engineer this for &lt;strong&gt;zero-copy deserialization&lt;/strong&gt;. By using the &lt;code&gt;bytemuck&lt;/code&gt; crate and &lt;code&gt;#[repr(C)]&lt;/code&gt;, we force the Rust compiler to lay out our struct exactly as it appears in the raw memory buffer.&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="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;bytemuck&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Pod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Zeroable&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;solana_program&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;pubkey&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="c1"&gt;// #[repr(C)] guarantees predictable memory alignment&lt;/span&gt;
&lt;span class="c1"&gt;// matching the on-chain byte array.&lt;/span&gt;
&lt;span class="nd"&gt;#[repr(C)]&lt;/span&gt;
&lt;span class="nd"&gt;#[derive(Clone,&lt;/span&gt; &lt;span class="nd"&gt;Copy,&lt;/span&gt; &lt;span class="nd"&gt;Debug,&lt;/span&gt; &lt;span class="nd"&gt;Pod,&lt;/span&gt; &lt;span class="nd"&gt;Zeroable)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;BondingCurveState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;virtual_token_reserves&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;virtual_sol_reserves&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;real_token_reserves&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;real_sol_reserves&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;token_total_supply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;               &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// u8 instead of bool for predictable byte sizing&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By deriving &lt;code&gt;Pod&lt;/code&gt; (Plain Old Data) and &lt;code&gt;Zeroable&lt;/code&gt;, we are making a strict contractual promise to the Rust compiler: this struct contains no pointers, no dynamic strings, and is simply a safe bag of bytes. The compiler enforces this at compile time — if you accidentally include a &lt;code&gt;Vec&lt;/code&gt; or a &lt;code&gt;String&lt;/code&gt;, it will refuse to compile.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; Every field in a &lt;code&gt;Pod&lt;/code&gt; struct must have a known, fixed size. Use &lt;code&gt;u8&lt;/code&gt; instead of &lt;code&gt;bool&lt;/code&gt;, and &lt;code&gt;[u8; 32]&lt;/code&gt; instead of &lt;code&gt;Pubkey&lt;/code&gt; if &lt;code&gt;Pubkey&lt;/code&gt; doesn't implement &lt;code&gt;Pod&lt;/code&gt;. Predictable sizing is the contract.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Deserialization at Microsecond Speed
&lt;/h2&gt;

&lt;p&gt;When you request an account from a Solana RPC node, the node returns the data as a base64-encoded string. A junior developer would decode the string, parse it into JSON, and map it to a TypeScript object. We are going to decode the base64 string, strip the 8-byte Anchor discriminator, and instantly &lt;strong&gt;cast the remaining bytes directly to our Rust struct memory&lt;/strong&gt; using a zero-copy pointer.&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="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Engine&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;general_purpose&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;STANDARD&lt;/span&gt;&lt;span class="p"&gt;};&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;parse_bonding_curve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BondingCurveState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Decode the base64 RPC response into a raw byte vector&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;raw_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;STANDARD&lt;/span&gt;
        &lt;span class="nf"&gt;.decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to decode base64"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Strip the 8-byte Anchor Discriminator&lt;/span&gt;
    &lt;span class="c1"&gt;//    (sha256("account:BondingCurveState")[..8])&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;account_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;raw_bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Zero-Copy Cast: reinterpret the bytes directly as the struct.&lt;/span&gt;
    &lt;span class="c1"&gt;//    This bypasses heap allocation entirely — nanosecond execution.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;curve_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;BondingCurveState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;bytemuck&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;curve_state&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because we aren't allocating new memory or reading field-by-field, &lt;strong&gt;this execution takes nanoseconds.&lt;/strong&gt; The &lt;code&gt;bytemuck::from_bytes&lt;/code&gt; call does not copy a single byte — it reinterprets the existing memory address as a pointer to our struct. We didn't move the library; we just pointed our code to the shelf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript SDK (REST + JSON parse)&lt;/strong&gt; — 200–800 ms&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rust + Borsh deserialization&lt;/strong&gt; — ~10–50 µs&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rust + bytemuck zero-copy cast&lt;/strong&gt; — ~100–500 ns ✓&lt;/p&gt;




&lt;h2&gt;
  
  
  Calculating Real-Time Price (The Business Value)
&lt;/h2&gt;

&lt;p&gt;Data engineering is useless without applying business logic. Now that the raw AMM state is held securely in our Rust backend's memory, we can calculate the real-time token price instantly. Standard AMMs use the &lt;strong&gt;constant product formula&lt;/strong&gt; (&lt;em&gt;x × y = k&lt;/em&gt;). By extracting the virtual reserves from our parsed struct, calculating the exact price natively in Rust becomes trivial.&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="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;BondingCurveState&lt;/span&gt; &lt;span class="p"&gt;{&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;get_token_price_in_sol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&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;f64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Prevent division-by-zero panics&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.virtual_token_reserves&lt;/span&gt; &lt;span class="o"&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="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Apply the constant product ratio: price = y / x&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sol_reserves&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.virtual_sol_reserves&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token_reserves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.virtual_token_reserves&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;sol_reserves&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;token_reserves&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 we hook this logic up to a NATS/WebSocket pipeline — as covered in the &lt;a href="https://xroot.dev/blog/solana-pipeline-part-2" rel="noopener noreferrer"&gt;Solana Firehose ingestion series&lt;/a&gt; — we can stream raw base64 account data, zero-copy deserialize it, calculate the price, and fire it to a trading algorithm &lt;strong&gt;before a standard SDK even finishes opening its HTTP connection.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For real-world use, you should validate the discriminator manually before calling &lt;code&gt;bytemuck::from_bytes&lt;/code&gt;. A mismatched account type will cause a panic in debug mode and undefined behavior in release. Always assert: &lt;code&gt;&amp;amp;raw_bytes[..8] == EXPECTED_DISCRIMINATOR&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion &amp;amp; System Impact
&lt;/h2&gt;

&lt;p&gt;By reverse-engineering the binary state of Solana accounts and leveraging Rust's &lt;code&gt;bytemuck&lt;/code&gt;, we achieve a fundamental infrastructure advantage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Zero RPC Pricing Overhead:&lt;/strong&gt; We don't pay for third-party price feed APIs. We read directly from chain truth.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Microsecond Deserialization:&lt;/strong&gt; Dropping standard JSON/Borsh parsing reduces CPU overhead to near zero — from milliseconds to nanoseconds per event.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Direct Chain Truth:&lt;/strong&gt; We read the exact mathematical reserves of the pool, making our metrics immune to frontend lag or aggregator delays.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Composable with NATS Pipelines:&lt;/strong&gt; This parsing layer slots directly into the high-throughput pipeline described in the &lt;a href="https://xroot.dev/blog/solana-pipeline-part-1" rel="noopener noreferrer"&gt;3-part Solana data pipeline series&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>solana</category>
      <category>defi</category>
      <category>bytemuck</category>
    </item>
    <item>
      <title>Permissioned Tokens on Solana: How Token ACL Works — and How to Tell It From a Honeypot</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Sun, 30 Aug 2026 15:25:36 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/permissioned-tokens-on-solana-how-token-acl-works-and-how-to-tell-it-from-a-honeypot-4k73</link>
      <guid>https://dev.to/sulimanmukhtar/permissioned-tokens-on-solana-how-token-acl-works-and-how-to-tell-it-from-a-honeypot-4k73</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-permissioned-tokens-token-acl" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On-chain, a regulated fund token and a honeypot scam are &lt;strong&gt;the same shape.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both are Token-2022 mints. Both keep an active freeze authority. Both set &lt;code&gt;DefaultAccountState&lt;/code&gt; to &lt;code&gt;Frozen&lt;/code&gt;, so every new holder's account starts locked. One of them is a European money-market fund following its regulator's rules; the other is a trap built to let you buy and never sell. Every token scanner I tested reads them identically: red flags, high risk, score zero.&lt;/p&gt;

&lt;p&gt;The reason this now matters is &lt;strong&gt;sRFC-37, the Token ACL standard&lt;/strong&gt; — the Solana Foundation's official mechanism for permissioned tokens. It has been live on mainnet since March 2026, real institutional money already uses it, and the entire real-world-asset wave forming on Solana is going to ship in this exact shape. This post covers how it works end to end — and the structural check that separates a compliant token from a trap, verified against the chain rather than anyone's metadata.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Permissioned Tokens Exist at All
&lt;/h2&gt;

&lt;p&gt;A tokenized treasury fund, a regulated stablecoin, a security token — their issuers are not &lt;em&gt;allowed&lt;/em&gt; to let anyone hold them. KYC requirements, sanctions screening, court orders, investor-accreditation rules: the issuer must be able to control who holds the asset and stop specific wallets, or the asset cannot legally exist on a public chain.&lt;/p&gt;

&lt;p&gt;Solana had two ways to build that before, and both hurt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Transfer hooks&lt;/strong&gt; run issuer code on every transfer — but every DEX, wallet and protocol touching the token must implement the hook interface, so composability dies at exactly the venues that create liquidity.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Manual freeze-and-thaw&lt;/strong&gt; keeps standard transfers — but every new holder starts frozen and waits for the issuer to co-sign a thaw. Onboarding becomes a support ticket, and the issuer signs forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Token ACL is the third path: keep the freeze mechanism — the one lever the token program already enforces everywhere — but make thawing &lt;strong&gt;self-service against a published rulebook.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mechanics: Delegated Freeze, Permissionless Thaw
&lt;/h2&gt;

&lt;p&gt;Three pieces make a Token ACL mint, and each is verifiable on-chain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Default-frozen accounts.&lt;/strong&gt; The mint carries Token-2022's &lt;code&gt;DefaultAccountState&lt;/code&gt; extension set to &lt;code&gt;Frozen&lt;/code&gt; — every token account anyone creates starts locked.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Freeze authority delegated to a program.&lt;/strong&gt; The issuer hands the mint's freeze authority to a &lt;em&gt;MintConfig&lt;/em&gt; PDA owned by the Token ACL program (&lt;code&gt;TACLkU6…52TP&lt;/code&gt;). From then on, freeze and thaw run through code, not through the issuer's wallet.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A published gate.&lt;/strong&gt; The MintConfig points at a &lt;em&gt;Gate Program&lt;/em&gt; that answers one question: &lt;code&gt;can_thaw_permissionless(wallet)&lt;/code&gt;. The mint's metadata declares it under a &lt;code&gt;token_acl&lt;/code&gt; key so wallets and SDKs can discover it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The holder experience collapses to one instruction: create your (frozen) token account, call the permissionless thaw, and the Token ACL program asks the gate whether you qualify. If yes, your account unlocks in the same transaction — no issuer signature, no waiting. If the gate says no, you stay frozen. That is the whole trick: &lt;strong&gt;the issuer's compliance rules became a public, self-service API.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Foundation ships a reference gate — the Allow/Block List program (&lt;code&gt;GATEzz…iULz&lt;/code&gt;) — with four modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Who can thaw&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Allow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;only listed wallets (KYC allowlist)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Block&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;everyone except listed wallets (sanctions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AllowAllEoas&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;any normal wallet; program-owned accounts stay frozen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Composite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;allow list AND block list — block always wins&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And because sRFC-37 only specifies the &lt;em&gt;interface&lt;/em&gt; between Token ACL and gates, an issuer can replace the reference gate with anything — an on-chain KYC registry, an oracle-driven sanctions feed, an identity protocol — without touching the token itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Actually Live: the Chain, Not the Press Release
&lt;/h2&gt;

&lt;p&gt;The standard was proposed in &lt;strong&gt;October 2025&lt;/strong&gt;, the official guide with mainnet program addresses published &lt;strong&gt;January 12, 2026&lt;/strong&gt; — and the program's own transaction history says the rest. Its first mainnet transaction landed on &lt;strong&gt;March 6, 2026&lt;/strong&gt;; its most recent, three days before this post. Total transactions ever: &lt;strong&gt;514.&lt;/strong&gt; This is genuinely early.&lt;/p&gt;

&lt;p&gt;Early — but not theoretical. Pull a recent transaction and decode the mint it touches, and you find real institutional money:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fact&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Token&lt;/td&gt;
&lt;td&gt;Spiko Digital Assets Cash and Carry Fund (eurSPKCC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default state&lt;/td&gt;
&lt;td&gt;frozen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freeze authority&lt;/td&gt;
&lt;td&gt;a PDA owned by the Token ACL program&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata.token_acl&lt;/td&gt;
&lt;td&gt;the official Allow/Block-List gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A regulated French tokenized fund, running the textbook sRFC-37 setup — plus the rest of the institutional Token-2022 stack: pausable transfers, a permanent delegate, permissioned burn, a mint close authority. Keep that list in mind; it is the second half of this story.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Honeypot Problem — and the Check That Actually Works
&lt;/h2&gt;

&lt;p&gt;Here is the collision. &lt;code&gt;DefaultAccountState: Frozen&lt;/code&gt; plus an active freeze authority is the &lt;em&gt;classic honeypot signature&lt;/em&gt; — the pattern scam tokens use so buyers can receive but never sell. Every scanner rightly screams at it. But it is also, byte for byte, the shape of every compliant sRFC-37 token. As RWA issuance grows, scanners face a choice: learn the standard, or misread an entire asset class as scams — and train users to ignore the warning that actually matters.&lt;/p&gt;

&lt;p&gt;The tempting shortcut is to trust the metadata: if the mint declares &lt;code&gt;token_acl&lt;/code&gt;, call it permissioned. That shortcut is a vulnerability. Metadata is free-form — &lt;strong&gt;any scammer can paste a &lt;code&gt;token_acl&lt;/code&gt; field into a honeypot&lt;/strong&gt; and inherit the standard's legitimacy. The claim costs nothing.&lt;/p&gt;

&lt;p&gt;What cannot be faked is account ownership. The verification that holds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;1 — The mint is default-frozen&lt;/strong&gt; (the extension is on and set to frozen).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;2 — The freeze authority is an account OWNED by the Token ACL program.&lt;/strong&gt; Not "matches a claimed address" — you fetch the freeze authority's account and check its owner. Only the Token ACL program can own its MintConfig PDAs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;3 — The metadata declares the gate&lt;/strong&gt;, so you can tell the holder &lt;em&gt;which&lt;/em&gt; rulebook decides their thaw — the official Allow/Block-List gate, or a custom program worth reading before you buy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The two failure modes write themselves:&lt;/strong&gt; structure without the claim is still verifiably permissioned; a claim without the structure is a scam wearing the standard's clothes — and deserves a &lt;em&gt;harder&lt;/em&gt; flag than an ordinary honeypot, not a pass.&lt;/p&gt;

&lt;p&gt;I shipped exactly this three-condition check into &lt;a href="https://app.xroot.dev/token-audit" rel="noopener noreferrer"&gt;xroot's free token audit&lt;/a&gt; this week. On eurSPKCC it now reads "permissioned token — freeze runs through the Token ACL standard, thaw is self-service through the official gate" where every other tool still prints the honeypot warning. On a mint that merely &lt;em&gt;claims&lt;/em&gt; the standard, it prints the harder flag.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest Reading: the Standard Doesn't Vouch for Everything
&lt;/h2&gt;

&lt;p&gt;One thing a good audit must &lt;em&gt;not&lt;/em&gt; do is let Token ACL launder the rest of the mint. Remember eurSPKCC's other extensions — pausable transfers, permanent delegate, permissioned burn. On an institutional token those exist for regulators: seize on a court order, pause in an incident, burn-and-reissue on a recovery. They are disclosed design. They are also, mechanically, &lt;strong&gt;total issuer power over your balance&lt;/strong&gt; — and Token ACL verifies the &lt;em&gt;freeze&lt;/em&gt; mechanism only. A scammer can run a real, verified Token ACL setup &lt;em&gt;and&lt;/em&gt; keep a permanent delegate that drains wallets.&lt;/p&gt;

&lt;p&gt;So the honest verdict for a permissioned token is two sentences, not one score: this token follows the official standard for who may hold it — &lt;em&gt;and&lt;/em&gt; its issuer can freeze, pause, seize, or burn your position, by design. Whether that is fine depends entirely on who the issuer is. For a regulated fund with a prospectus, it is the product working. For an anonymous team, it is the same red flag it always was.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Means From Here
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;For issuers:&lt;/strong&gt; permissioned tokens on Solana stopped being a custom-engineering project. Mint with default-frozen state, delegate freeze to Token ACL, pick a gate mode, declare it in metadata — and holders onboard themselves against your rulebook.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;For holders:&lt;/strong&gt; a frozen-by-default token is no longer automatically a scam — and no longer automatically safe. The question moved from "is there a freeze authority?" to "&lt;em&gt;who holds it, and through what?&lt;/em&gt;"&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;For tooling:&lt;/strong&gt; 514 transactions in, this is the cheapest moment there will ever be to learn the standard. The RWA wave will not wait for the scanners.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— Check Any Token, Free —&lt;/p&gt;

&lt;h3&gt;
  
  
  Permissioned standard, or honeypot wearing its clothes?
&lt;/h3&gt;

&lt;p&gt;Paste any Solana mint into xroot's free audit. It verifies Token ACL structurally — the freeze authority's owning program, never the metadata claim — names the gate that decides who can hold the token, and still tells you plainly about every seize, pause and burn power the issuer kept.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.xroot.dev/token-audit" rel="noopener noreferrer"&gt;Audit a Token ↗&lt;/a&gt;&lt;a href="https://app.xroot.dev/solana" rel="noopener noreferrer"&gt;All Solana Tools&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sources: the &lt;a href="https://solana.com/docs/tokenization/token-acl" rel="noopener noreferrer"&gt;Token ACL documentation&lt;/a&gt;, the &lt;a href="https://solana.com/developers/guides/advanced/acl" rel="noopener noreferrer"&gt;official sRFC-37 guide&lt;/a&gt;, the &lt;a href="https://github.com/solana-foundation/token-acl" rel="noopener noreferrer"&gt;token-acl&lt;/a&gt; and &lt;a href="https://github.com/solana-foundation/token-acl-gate" rel="noopener noreferrer"&gt;ABL gate&lt;/a&gt; repositories, the &lt;a href="https://forum.solana.com/t/srfc-37-efficient-block-allow-list-token-standard/4036" rel="noopener noreferrer"&gt;sRFC-37 forum thread&lt;/a&gt;, and the Token ACL program's own mainnet history and the eurSPKCC mint, read directly from the chain on August 30, 2026.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>web3</category>
      <category>security</category>
      <category>rust</category>
    </item>
    <item>
      <title>Solana's 4,096-Byte Transactions: What v1 Breaks and How to Fix It</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 28 Aug 2026 05:59:26 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/solanas-4096-byte-transactions-what-v1-breaks-and-how-to-fix-it-8a3</link>
      <guid>https://dev.to/sulimanmukhtar/solanas-4096-byte-transactions-what-v1-breaks-and-how-to-fix-it-8a3</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-transaction-v1" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Solana's transaction size limit — &lt;strong&gt;1,232 bytes&lt;/strong&gt;, unchanged since genesis — is being raised to &lt;strong&gt;4,096 bytes&lt;/strong&gt;. The catch: the extra room only exists inside a brand-new wire format, and the day its feature gate activates, code that merely &lt;em&gt;reads&lt;/em&gt; transactions can start failing.&lt;/p&gt;

&lt;p&gt;The upgrade ships as two proposals: &lt;strong&gt;SIMD-0296&lt;/strong&gt; raises the size ceiling, and &lt;strong&gt;SIMD-0385&lt;/strong&gt; defines the &lt;strong&gt;v1 transaction format&lt;/strong&gt; that carries it. Legacy and v0 transactions keep working exactly as they do today — if you never touch v1, nothing about &lt;em&gt;sending&lt;/em&gt; changes for you. But reading is a different story: one v1 transaction inside a block is enough to make an un-upgraded &lt;code&gt;getBlock&lt;/code&gt; call fail outright, and indexers that scan ComputeBudget instructions will silently record zeros for every v1 transaction they see.&lt;/p&gt;

&lt;p&gt;Here is what actually changes, why it changes, the three ways it breaks existing apps — loudly, silently, and sneakily — and the exact fix for each. Everything below is verified against the proposal texts, then exercised end-to-end against a local Agave 4.2 validator rather than taken from the headlines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why 1,232 Bytes Existed, and Why It Can Finally Move
&lt;/h2&gt;

&lt;p&gt;The old limit was never a design goal — it was plumbing. Transactions originally travelled as single UDP datagrams, so they had to fit the minimum IPv6 MTU of 1,280 bytes minus 48 bytes of headers: &lt;strong&gt;1,232 bytes&lt;/strong&gt; for everything — signatures, accounts, instructions, data. Solana's networking has since moved to QUIC, whose specification (RFC 9000) imposes no explicit stream size limit. The physical reason for the cap is gone; SIMD-0296 removes it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Old limit (legacy / v0)&lt;/td&gt;
&lt;td&gt;1,232 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New limit (v1 only)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,096 bytes (~3.3×)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What never fit in 1,232 bytes: zero-knowledge proofs (Confidential Transfers), Winternitz one-time signatures, nested institutional multisigs, BLS and other non-precompiled signature schemes. Teams worked around it with Jito bundles — which are not atomic at the protocol level. The proposal picked 4,096 by measuring real bundle traffic (roughly half of all bundles fit in 2,048 bytes, 65% in 6,144) and then aligning to the 4 KiB memory page validators already manage, so a transaction never spans multiple pages.&lt;/p&gt;

&lt;p&gt;Worth knowing: SIMD-0296 introduces &lt;strong&gt;no new per-byte fee&lt;/strong&gt;. The expectation instead is that schedulers will price large transactions via priority fees — a bigger envelope will cost more to land, just not on a published curve.&lt;/p&gt;




&lt;h2&gt;
  
  
  v1 Is a New Wire Format, Not a Bigger v0
&lt;/h2&gt;

&lt;p&gt;SIMD-0385 does not stretch the old envelope — it redesigns it. A v1 transaction announces itself with version byte &lt;code&gt;129&lt;/code&gt; (&lt;code&gt;0x81&lt;/code&gt;) at offset zero, and then diverges from v0 in four structural ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource requests become message fields.&lt;/strong&gt; Compute unit limit, loaded-accounts data size, heap size, and priority fee move out of ComputeBudget &lt;em&gt;instructions&lt;/em&gt; into a fixed-position &lt;code&gt;transactionConfig&lt;/code&gt; bitmask in the message itself. ComputeBudget instructions inside a v1 transaction still execute — as no-ops that burn compute units and configure nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Address Lookup Tables are gone.&lt;/strong&gt; All accounts are inline — up to 64 full 32-byte addresses, duplicates rejected at sanitization. Validators no longer load and deserialize ALT accounts before they can even parse a transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed-width instruction headers.&lt;/strong&gt; Each instruction's header (program index, account count, data length) is separated from its variable-length payload, so parsers compute instruction boundaries directly instead of walking the buffer sequentially.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signatures move to the tail.&lt;/strong&gt; The message comes first, signatures last with no length prefix — the count is derived from the header.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hard limits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;v1 limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transaction size&lt;/td&gt;
&lt;td&gt;4,096 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accounts&lt;/td&gt;
&lt;td&gt;64, inline only — no lookup tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instructions&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signatures&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accounts per instruction&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the config fields that replace ComputeBudget instructions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config field&lt;/th&gt;
&lt;th&gt;Encoding&lt;/th&gt;
&lt;th&gt;Default when unset&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Priority fee&lt;/td&gt;
&lt;td&gt;u64 LE, &lt;strong&gt;total lamports&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute unit limit&lt;/td&gt;
&lt;td&gt;u32 LE&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 — not 200k/instruction&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loaded accounts data size&lt;/td&gt;
&lt;td&gt;u32 LE&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0 — not 64 MiB&lt;/strong&gt; (cost model floors at 32 KiB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heap size&lt;/td&gt;
&lt;td&gt;u32 LE, 1 KiB multiples in [32 KiB, 256 KiB]&lt;/td&gt;
&lt;td&gt;32 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The unit change nobody will notice until their fee stats corrupt:&lt;/strong&gt; v0 priority fees are &lt;em&gt;micro-lamports per compute unit&lt;/em&gt;; the v1 priority fee is an &lt;em&gt;absolute total in lamports&lt;/em&gt;. Any pipeline that averages, compares, or estimates fees across versions must normalize first — the raw numbers are not even the same dimension.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The ALT removal is a real trade-off, and it lands hardest on DeFi routing. About &lt;strong&gt;62% of current v0 transactions reference at least one lookup table&lt;/strong&gt;. Converted to inline addresses, half of them grow by under 420 bytes and 90% by under 1,400 — comfortably inside the new envelope. But dense multi-table routes expand by 1,500+ bytes, and the &lt;strong&gt;64-account cap does not move&lt;/strong&gt;, so broad multi-pool aggregator strategies stay account-bound, not byte-bound. (A draft proposal, SIMD-0596, would raise the cap to 96.)&lt;/p&gt;




&lt;h2&gt;
  
  
  The Part That Breaks Your App: Reading
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The loud failure.&lt;/strong&gt; The moment the gate activates, &lt;code&gt;getTransaction&lt;/code&gt;, &lt;code&gt;getBlock&lt;/code&gt;, and &lt;code&gt;blockSubscribe&lt;/code&gt; return JSON-RPC error &lt;code&gt;-32015&lt;/code&gt; for anything v1 unless you pass &lt;code&gt;maxSupportedTransactionVersion: 1&lt;/code&gt;. It is the exact rerun of the v0 migration of 2022 — with the same nasty amplification: one v1 transaction anywhere in a block fails the &lt;em&gt;entire&lt;/em&gt; &lt;code&gt;getBlock&lt;/code&gt; response, not just that transaction.&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;// Un-upgraded reader — fails with -32015 once any v1 tx lands in the block&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;maxSupportedTransactionVersion&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Fixed — pass the integer 1 (the string "1" is rejected)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;maxSupportedTransactionVersion&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This parameter is backwards compatible — you can and should ship it &lt;em&gt;today&lt;/em&gt;, before activation. Responses for v1 transactions carry the new &lt;code&gt;transactionConfig&lt;/code&gt; object inside the message; v0 and legacy responses are unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The silent failure.&lt;/strong&gt; Every indexer I have seen extracts priority fees and compute limits by scanning for ComputeBudget instructions. Against a v1 transaction that scan finds nothing and returns &lt;strong&gt;zero — without an error&lt;/strong&gt;. Your pipeline keeps running and quietly writes wrong fee data, wrong CU data, and fee estimates skewed by transactions that appear free. The fix: read &lt;code&gt;transactionConfig&lt;/code&gt; when it is present, fall back to instruction scanning when it is not, and normalize the units before anything compares them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sneaky failure.&lt;/strong&gt; Geyser and gRPC streams have &lt;em&gt;no version gate at all&lt;/em&gt; — there is no parameter to reject v1, so the new transactions simply arrive. Yellowstone builds before 15.1.1 silently downgrade v1 to v0 on the wire, and protobuf stubs generated before &lt;code&gt;yellowstone-grpc-proto&lt;/code&gt; 12.6.0 have no &lt;code&gt;Message.config&lt;/code&gt; field to decode. Regenerate your stubs, and detect the version structurally: check for the presence of &lt;code&gt;config&lt;/code&gt; on the message &lt;em&gt;first&lt;/em&gt;, then the &lt;code&gt;versioned&lt;/code&gt; flag — in that order, because a v1 message is also "versioned".&lt;/p&gt;




&lt;h2&gt;
  
  
  Sending v1: Zero Defaults and New Units
&lt;/h2&gt;

&lt;p&gt;Nothing forces you to send v1 — legacy and v0 stay valid indefinitely. Adopt it when you need the bytes. When you do, the sharpest edge is that &lt;strong&gt;v1 resource limits default to zero&lt;/strong&gt;. Legacy and v0 gave you 200k compute units per instruction and 64 MiB of loaded accounts for free; a v1 transaction that doesn't explicitly request a compute unit limit and a loaded-accounts data size &lt;strong&gt;fails at execution&lt;/strong&gt;, after you have already paid to land it.&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;// @solana/kit ≥ 8.0.0 — every limit is explicit in v1&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;createTransactionMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTransactionMessageComputeUnitLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&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;m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTransactionMessageLoadedAccountsDataSizeLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;262&lt;/span&gt;&lt;span class="nx"&gt;_144&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;// 32 KiB pages&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTransactionMessagePriorityFeeLamports&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;_000n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;// TOTAL lamports, not per-CU&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The working recipe: &lt;strong&gt;simulate with both limits maxed out&lt;/strong&gt;, read the consumed values from the simulation, then set the real limits from the measurement — rounding the loaded-accounts size &lt;em&gt;up to the next 32 KiB page&lt;/em&gt;, because the cost model floors it at 32 KiB anyway. Remember there are no lookup tables to lean on: every account is inline, 64 at most. And send with &lt;code&gt;encoding: 'base64'&lt;/code&gt; — base58 encoding is hard-capped at 1,232 bytes, so a large v1 transaction cannot even be submitted through it.&lt;/p&gt;

&lt;p&gt;Two audiences with homework even if they never build a v1 transaction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-chain programs that introspect ComputeBudget.&lt;/strong&gt; No sysvar exposes the v1 message config to on-chain code, and ComputeBudget instructions in v1 are no-ops — a program gating behavior on introspected compute budget simply cannot see it for v1 callers. Stop gating on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fee sponsors and co-signers.&lt;/strong&gt; A fee cap enforced by scanning ComputeBudget instructions does not bind a v1 transaction at all. Decode the raw bytes, check byte zero for &lt;code&gt;0x81&lt;/code&gt;, and read the limits from &lt;code&gt;transactionConfig&lt;/code&gt; before you sign.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Upgrade Matrix, and Where the Rollout Stands
&lt;/h2&gt;

&lt;p&gt;Minimum versions that understand v1 — upgrading past these is the single highest-leverage preparation step:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Minimum version&lt;/th&gt;
&lt;th&gt;Support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@solana/kit&lt;/code&gt; (TypeScript)&lt;/td&gt;
&lt;td&gt;8.0.0&lt;/td&gt;
&lt;td&gt;read + send&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@solana/web3.js&lt;/code&gt; 3.x&lt;/td&gt;
&lt;td&gt;rc-0.3 (upcoming)&lt;/td&gt;
&lt;td&gt;read + send&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@solana/web3.js&lt;/code&gt; 1.x&lt;/td&gt;
&lt;td&gt;1.99.0 (upcoming)&lt;/td&gt;
&lt;td&gt;read only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;solana-*&lt;/code&gt; crates (Rust)&lt;/td&gt;
&lt;td&gt;4.2.x&lt;/td&gt;
&lt;td&gt;read + send&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;solders&lt;/code&gt; (Python)&lt;/td&gt;
&lt;td&gt;0.29.0&lt;/td&gt;
&lt;td&gt;read + send&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;solana-go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.23.0 (or 2.0.0 on &lt;code&gt;/v2&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;read + send&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yellowstone-grpc-proto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12.6.0&lt;/td&gt;
&lt;td&gt;first stubs with &lt;code&gt;Message.config&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yellowstone geyser plugin&lt;/td&gt;
&lt;td&gt;15.1.1&lt;/td&gt;
&lt;td&gt;stops downgrading v1 → v0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@triton-one/yellowstone-grpc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6.0.0&lt;/td&gt;
&lt;td&gt;decodes the config field&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As of late August 2026 the feature gate is &lt;strong&gt;not yet active&lt;/strong&gt; on testnet, devnet, or mainnet. The format ships with Agave 4.2 — whose mainnet feature activations began rolling out the week of August 17 — and the Solana Foundation's stated expectation is mainnet activation within weeks. You can exercise v1 locally today with Solana CLI ≥ 4.2 or Surfpool ≥ 1.5, and check the gate yourself at any time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;solana &lt;span class="nt"&gt;-u&lt;/span&gt; m feature status txv1aq4pp281K9um3tnPgkfX8UqtFT6wcVW3hNezGLL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran both checks while writing this. The gate reports &lt;strong&gt;inactive&lt;/strong&gt; on mainnet, testnet, and devnet as of August 28. And on a local CLI 4.2.1 test validator — where the feature is already live — a &lt;code&gt;@solana/kit&lt;/code&gt; 8 transaction built exactly as in the sending section lands on-chain with byte zero &lt;code&gt;0x81&lt;/code&gt;, reads back as version 1 with its &lt;code&gt;transactionConfig&lt;/code&gt; populated, and fails with &lt;code&gt;-32015&lt;/code&gt; the moment it is fetched with &lt;code&gt;maxSupportedTransactionVersion: 0&lt;/code&gt;. The behavior in this post is reproduced, not paraphrased.&lt;/p&gt;

&lt;p&gt;Runnable end-to-end examples in TypeScript, Rust, Go, and Python live in the Solana Foundation's &lt;a href="https://github.com/solana-foundation/transaction-v1-examples" rel="noopener noreferrer"&gt;transaction-v1-examples&lt;/a&gt; repository.&lt;/p&gt;




&lt;h2&gt;
  
  
  If You're Here Because Something Already Broke
&lt;/h2&gt;

&lt;p&gt;Symptom to fix, in the order you are likely to hit them after activation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Error &lt;code&gt;-32015&lt;/code&gt; from &lt;code&gt;getTransaction&lt;/code&gt; / &lt;code&gt;getBlock&lt;/code&gt; / &lt;code&gt;blockSubscribe&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;maxSupportedTransactionVersion: 1&lt;/code&gt; — as an integer, not a string — to every call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whole blocks failing to fetch&lt;/td&gt;
&lt;td&gt;Same fix — one v1 transaction fails the entire &lt;code&gt;getBlock&lt;/code&gt; response until the parameter is raised&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority fees or CU limits suddenly reading zero&lt;/td&gt;
&lt;td&gt;Your ComputeBudget instruction scan cannot see v1 — read &lt;code&gt;transactionConfig&lt;/code&gt; when present, then fall back to scanning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fee stats or estimates off by orders of magnitude&lt;/td&gt;
&lt;td&gt;Unit mismatch: v0 is micro-lamports per CU, v1 is total lamports — normalize before aggregating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Transaction too large" on send despite v1&lt;/td&gt;
&lt;td&gt;You are on base58 (capped at 1,232 bytes) — send with &lt;code&gt;encoding: 'base64'&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1 transactions failing with compute errors&lt;/td&gt;
&lt;td&gt;Limits default to zero — explicitly set the compute unit limit and loaded-accounts data size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geyser/gRPC stream shows v1 traffic as v0, or missing config&lt;/td&gt;
&lt;td&gt;Upgrade Yellowstone plugin ≥ 15.1.1, regenerate protobuf stubs ≥ 12.6.0, detect on &lt;code&gt;Message.config&lt;/code&gt; presence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Bigger Envelopes, Sharper Edges
&lt;/h2&gt;

&lt;p&gt;The upgrade itself is easy to like: ZK proofs, big multisigs, and heavyweight signature schemes finally land as single atomic transactions instead of bundle acrobatics, and validators get a format they can parse without touching state. But it is the first new transaction format since v0 in 2022, and the damage pattern will be the same — not the teams sending new transactions, but the readers who never opted in.&lt;/p&gt;

&lt;p&gt;The preparation is cheap and safe to do today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upgrade your SDKs past the matrix above.&lt;/li&gt;
&lt;li&gt;Ship &lt;code&gt;maxSupportedTransactionVersion: 1&lt;/code&gt; now — it is backwards compatible.&lt;/li&gt;
&lt;li&gt;Audit anything that scans ComputeBudget instructions for fees or limits.&lt;/li&gt;
&lt;li&gt;Only then think about whether your own transactions want the extra 2,864 bytes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sources: the official &lt;a href="https://solana.com/upgrades/larger-transaction-sizes" rel="noopener noreferrer"&gt;Larger Transaction Sizes&lt;/a&gt; upgrade page, the &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0296-larger-transactions.md" rel="noopener noreferrer"&gt;SIMD-0296&lt;/a&gt; and &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0385-transaction-v1.md" rel="noopener noreferrer"&gt;SIMD-0385&lt;/a&gt; proposals, and Solana's &lt;a href="https://solana.com/news/transaction-v1-and-the-alt-trade-off" rel="noopener noreferrer"&gt;ALT trade-off analysis&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and operate real-time Solana data pipelines — Geyser/gRPC ingestion, decoding, and analytics at firehose scale. If your indexer, wallet backend, or trading system needs to survive this migration, &lt;a href="https://xroot.dev/#contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt; or read the &lt;a href="https://xroot.dev/blog/solana-pipeline-part-1" rel="noopener noreferrer"&gt;pipeline series&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Robinhood Chain: Three Things the Docs Don't Say</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Tue, 25 Aug 2026 13:18:05 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/robinhood-chain-three-things-the-docs-dont-say-4olb</link>
      <guid>https://dev.to/sulimanmukhtar/robinhood-chain-three-things-the-docs-dont-say-4olb</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/robinhood-chain-read-directly" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I spent a day reading Robinhood Chain with &lt;code&gt;eth_call&lt;/code&gt; instead of reading about it. Three things turned up that no documentation page mentions — and one of them is a mistake I made first.&lt;/p&gt;

&lt;p&gt;Robinhood Chain went live on 1 July 2026: an Arbitrum Orbit L2 settling to Ethereum, ETH for gas, ~0.02 gwei, and — unusually for a chain run by a regulated US brokerage — genuinely permissionless deployment. That much the docs say, and all of it checks out.&lt;/p&gt;

&lt;p&gt;What follows is the part that only shows up if you query the chain yourself. Each finding is also a lesson about a different way of trusting the wrong artefact: a marketing page, an ABI, and a getter. The through-line is the same one that runs through &lt;a href="https://xroot.dev/blog/zero-latency-defi-parsing" rel="noopener noreferrer"&gt;parsing raw AMM accounts instead of using an SDK&lt;/a&gt;: &lt;strong&gt;prefer evidence the subject cannot author.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Establishing Ground Truth in Three Calls
&lt;/h2&gt;

&lt;p&gt;Before trusting anything about a chain, ask the chain. Chain IDs get transcribed wrong, RPC URLs get stale, and "mainnet is live" is a claim with at least four distinct meanings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RPC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://rpc.mainnet.chain.robinhood.com

&lt;span class="c"&gt;# Chain ID: 0x1237 = 4663&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nv"&gt;$RPC&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'&lt;/span&gt;

&lt;span class="c"&gt;# Is this really a Nitro chain? Arbitrum precompiles answer 0xfe.&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nv"&gt;$RPC&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"eth_getCode",
       "params":["0x000000000000000000000000000000000000006b","latest"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An Arbitrum Nitro chain exposes its precompiles as accounts whose code is the single byte &lt;code&gt;0xfe&lt;/code&gt;. That one read is worth more than any "built on Arbitrum" badge: it is a property the chain cannot fake without actually being one.&lt;/p&gt;

&lt;p&gt;The same trick answers the question that decides whether a third party can build anything at all. Rather than trusting "permissionless", estimate gas for a &lt;em&gt;contract creation&lt;/em&gt; from an address the chain has never seen. A deployer allowlist rejects it. This one quotes a price.&lt;/p&gt;

&lt;p&gt;Chain ID4663 (0x1237)&lt;/p&gt;

&lt;p&gt;StackArbitrum Orbit / Nitro → Ethereum&lt;/p&gt;

&lt;p&gt;Gas tokenETH · ~0.023 gwei&lt;/p&gt;

&lt;p&gt;DeploymentPermissionless — no allowlist&lt;/p&gt;

&lt;p&gt;CREATE2's deterministic-deployment proxy, Multicall3, Permit2 and Safe v1.4.1 are all present at their canonical addresses. As EVM chains go, this one arrived furnished.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Chain Owner's Precompiles Are Public — and One Has Been Busy
&lt;/h2&gt;

&lt;p&gt;Arbitrum chains expose their own governance through &lt;code&gt;ArbOwnerPublic&lt;/code&gt; at &lt;code&gt;0x…006b&lt;/code&gt;. It is readable by anyone. Two calls tell you who controls the chain:&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;# getAllChainOwners()          -&amp;gt; selector 0x516b4e0f&lt;/span&gt;
&lt;span class="c"&gt;# getAllTransactionFilterers() -&amp;gt; selector 0x595fbb5a&lt;/span&gt;
&lt;span class="c"&gt;# (derive both with keccak256(signature)[0..4] — do not trust a table)&lt;/span&gt;

getAllChainOwners&lt;span class="o"&gt;()&lt;/span&gt;          -&amp;gt; &lt;span class="o"&gt;[&lt;/span&gt; 0x2a153c6a…005C09 &lt;span class="o"&gt;]&lt;/span&gt;   &lt;span class="c"&gt;# an UpgradeExecutor proxy&lt;/span&gt;
getAllTransactionFilterers&lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; &lt;span class="o"&gt;[&lt;/span&gt; 0xebDc18A1…24b7   &lt;span class="o"&gt;]&lt;/span&gt;   &lt;span class="c"&gt;# ← one authorised filterer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second address is worth understanding. ArbOS added protocol-level transaction screening for Orbit chains: an authorised filterer registers a transaction &lt;em&gt;hash&lt;/em&gt;, and from then on the state transition function forcibly fails it — including a transaction force-included through L1, which is normally the escape hatch that makes a rollup censorship-resistant.&lt;/p&gt;

&lt;p&gt;Most write-ups stop at "the capability exists". But whether a capability has been &lt;em&gt;used&lt;/em&gt; is a question an EOA answers for free, because its nonce is public and its history is indexed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eth_getTransactionCount&lt;span class="o"&gt;(&lt;/span&gt;0xebDc18A1…24b7&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 0x17cc   &lt;span class="c"&gt;# = 6,092 transactions&lt;/span&gt;

&lt;span class="c"&gt;# What are they? Page the explorer's txlist and look at the destination + selector.&lt;/span&gt;
to:       0x0000000000000000000000000000000000000074   &lt;span class="c"&gt;# the filter precompile&lt;/span&gt;
selector: 0xcb470491                                    &lt;span class="c"&gt;# addFilteredTransaction(bytes32)&lt;/span&gt;
status:   1                                             &lt;span class="c"&gt;# every one succeeded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6,092 transactions, and the ones I sampled are all filter calls.&lt;/strong&gt; The first landed &lt;strong&gt;30 June 2026 at 14:53 UTC&lt;/strong&gt; — the day &lt;em&gt;before&lt;/em&gt; the public mainnet launch. The most recent was 9 August. That is roughly 150 filtered transactions a day across the chain's first six weeks, with no published criteria, no volume disclosure, and no appeals process I could find.&lt;/p&gt;

&lt;p&gt;I want to be precise about what this does and does not establish. It shows the mechanism is operational rather than dormant. It does &lt;em&gt;not&lt;/em&gt; tell you what was filtered or why — the hashes reveal nothing about intent — and the filterer EOA carries no name or public tag, so attributing it to Robinhood is inference, not proof. The question I actually care about, and could not answer from the blocklist alone, is whether a &lt;em&gt;contract deployment&lt;/em&gt; has ever been filtered.&lt;/p&gt;

&lt;p&gt;If you are considering deploying something with a revenue stream on this chain, that is the risk to price: not a lawsuit, but a switch.&lt;/p&gt;




&lt;h2&gt;
  
  
  An ABI Is Not a Contract — I Got This Wrong First
&lt;/h2&gt;

&lt;p&gt;Robinhood's tokenized stocks are ordinary ERC-20s. Whether they restrict &lt;em&gt;transfers&lt;/em&gt; has been publicly disputed, so I went to settle it. I pulled the implementation behind the proxy, read its ABI, and searched the function list for the usual suspects — &lt;code&gt;canTransfer&lt;/code&gt;, &lt;code&gt;isWhitelisted&lt;/code&gt;, an identity registry, anything ERC-3643 shaped. Nothing. I concluded transfers were unrestricted apart from a global pause.&lt;/p&gt;

&lt;p&gt;That conclusion was wrong, and the reason is a detail worth carrying around:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Solidity &lt;code&gt;modifier&lt;/code&gt; is inlined into the functions it guards. It never appears as its own ABI entry.&lt;/strong&gt; An ABI enumerates what you can &lt;em&gt;call&lt;/em&gt;. It says nothing about what happens on the way in. You cannot prove the absence of a restriction from an ABI — only from source or bytecode.&lt;/p&gt;

&lt;p&gt;The verified source — 191,692 characters of it — settles the question immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modifier onlyNotBlocked(address account) {
    if (IAccessControlsRegistry(ACCESS_CONTROLLED_REGISTRY).isBlocked(account)) {
        revert Blocked(account);
    }
    _;
}

function transfer(address to, uint256 value) public override
    onlyNotPaused
    onlyNotBlocked(to)
    onlyNotBlocked(_msgSender())
    returns (bool)

function transferFrom(address from, address to, uint256 value) public override
    onlyNotPaused
    onlyNotBlocked(from)
    onlyNotBlocked(to)
    onlyNotBlocked(_msgSender())
    returns (bool)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;onlyNotBlocked&lt;/code&gt; appears fifteen times. &lt;code&gt;canTransfer&lt;/code&gt;, &lt;code&gt;whitelist&lt;/code&gt; and &lt;code&gt;allowlist&lt;/code&gt; appear zero times. So both halves of my original answer needed splitting apart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;No allowlist.&lt;/strong&gt; You do not need permission to receive one. That part was right, and it is why these tokens compose with ordinary DeFi at all.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;But a per-address blocklist,&lt;/strong&gt; checked on both sides of every transfer &lt;em&gt;and&lt;/em&gt; on the caller. This is the USDC/USDT model — default-open, revocable — not ERC-3643's default-closed model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction is the whole commercial question. Default-open means a wallet, a portfolio tracker or an AMM works normally. Default-closed would mean none of them do. It is a good outcome — it is simply not the one the documentation states, because the documentation does not mention the blocklist at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Identity: Ask for Evidence the Subject Cannot Author
&lt;/h2&gt;

&lt;p&gt;Search the chain's explorer for &lt;code&gt;TSLA&lt;/code&gt; and you get fifty results. One is the real tokenized Tesla. Among the rest is a token that copies the genuine name character for character, and a cluster whose addresses all end in the same few characters — the signature of a script, not an issuer.&lt;/p&gt;

&lt;p&gt;So how do you identify the real one? There is a getter that looks perfect for it: &lt;code&gt;ACCESS_CONTROLLED_REGISTRY()&lt;/code&gt;, which on a genuine stock token returns the registry address. Call it on the impostor and it reverts. Job done?&lt;/p&gt;

&lt;p&gt;No — because &lt;strong&gt;a getter is code, and the thing you are interrogating wrote it.&lt;/strong&gt; Any contract can implement that function to return whatever value makes it look legitimate. It happens to work here only because these particular fakes did not bother.&lt;/p&gt;

&lt;p&gt;The stronger check reads storage the contract cannot lie about. These tokens are beacon proxies, so the beacon address lives at the fixed &lt;a href="https://eips.ethereum.org/EIPS/eip-1967" rel="noopener noreferrer"&gt;EIP-1967&lt;/a&gt; slot — and &lt;code&gt;eth_getStorageAt&lt;/code&gt; bypasses contract code entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SLOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50  &lt;span class="c"&gt;# EIP-1967 beacon&lt;/span&gt;

eth_getStorageAt&lt;span class="o"&gt;(&lt;/span&gt;TSLA, SLOT&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 0x…e10b6f6b275de231345c20d14ab812db62151b00
eth_getStorageAt&lt;span class="o"&gt;(&lt;/span&gt;AAPL, SLOT&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 0x…e10b6f6b275de231345c20d14ab812db62151b00
eth_getStorageAt&lt;span class="o"&gt;(&lt;/span&gt;NVDA, SLOT&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 0x…e10b6f6b275de231345c20d14ab812db62151b00
eth_getStorageAt&lt;span class="o"&gt;(&lt;/span&gt;SPY,  SLOT&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 0x…e10b6f6b275de231345c20d14ab812db62151b00

eth_getStorageAt&lt;span class="o"&gt;(&lt;/span&gt;the impostor, SLOT&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 0x0000…0000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four genuine tokens, one beacon, one factory. The impostor returns nothing, because it is not a beacon proxy and has no such slot to populate. A storage read is not an opinion and cannot be forged by the contract being read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never match on name or symbol.&lt;/strong&gt; Memecoins on this chain have started appending the exact &lt;code&gt;• Robinhood Token&lt;/code&gt; suffix to their own names — there is one called "Hoodrat • Robinhood Token". Any pipeline that discovers assets by string match has an impersonation vector, not a check.&lt;/p&gt;




&lt;h2&gt;
  
  
  One Address Holds Identity, Transferability and the Kill Switch
&lt;/h2&gt;

&lt;p&gt;Follow the three findings and they converge on the same contract. The beacon that proves a token is genuine is the same registry the blocklist is read from — and the same registry that can halt every stock token at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function paused() public view returns (bool) {
    StockStorage storage $ = _getStockStorage();
    return $.paused || IAccessControlsRegistry(ACCESS_CONTROLLED_REGISTRY).paused();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that disjunction carefully. A stock token is paused if &lt;em&gt;its own&lt;/em&gt; flag is set &lt;strong&gt;or&lt;/strong&gt; if the registry's global flag is set. One transaction against &lt;code&gt;0xe10b…1b00&lt;/code&gt; freezes the entire tokenized equity market on this chain. The same registry answers &lt;code&gt;isBlocked&lt;/code&gt;, so an address blocked once is blocked on every stock token simultaneously.&lt;/p&gt;

&lt;p&gt;Both flags read &lt;code&gt;false&lt;/code&gt; today, and the multiplier that adjusts share counts for splits and dividends sits at exactly &lt;code&gt;1e18&lt;/code&gt;. Nothing is being exercised. That is rather the point: this is what the healthy state looks like, and the capabilities are invisible from the outside unless you go and read them.&lt;/p&gt;

&lt;p&gt;None of this is scandalous. A regulated tokenized security &lt;em&gt;needs&lt;/em&gt; a pause for corporate actions and a blocklist for sanctions compliance; an instrument that could not do those things could not legally represent a stock. The observation is narrower and, I think, more useful: &lt;strong&gt;on an ordinary token these powers are a red flag, and on a regulated one they are a requirement&lt;/strong&gt; — so any generic token scanner pointed at a tokenized stock will produce a confidently wrong verdict.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Take Away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Probe with a control.&lt;/strong&gt; Every "is X deployed" sweep should include an address you know is empty. If your method cannot produce a negative, it is not measuring anything.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Derive selectors, do not look them up.&lt;/strong&gt; Four bytes of keccak is cheaper than trusting a table, and it catches the case where the function you think you are calling does not exist.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Never infer absence from an ABI.&lt;/strong&gt; Modifiers are inlined. So are hooks. Presence is provable from an ABI; absence is not.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Prefer storage over getters, and nonces over announcements.&lt;/strong&gt; Rank your evidence by how hard it would be for the subject to fabricate.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Capability is not usage — but usage is usually measurable.&lt;/strong&gt; The gap between "a filter exists" and "it has run 6,092 times" was one nonce read away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Robinhood Chain is open, well-provisioned and easy to build on, and I would take everything above as an argument for reading carefully rather than an argument against the chain. It is the same habit that made the &lt;a href="https://xroot.dev/blog/solana-rent-cut-simd-0437" rel="noopener noreferrer"&gt;economics of Solana's rent cut&lt;/a&gt; read so differently from the headlines about it. The uncomfortable finding is not that a brokerage put controls on its own tokenized securities — it is how much of that only exists in storage slots and nonces, and how little of it exists in prose.&lt;/p&gt;

&lt;p&gt;— Check a Token Before You Trust It —&lt;/p&gt;

&lt;h3&gt;
  
  
  Fifty results for one ticker. One of them is real.
&lt;/h3&gt;

&lt;p&gt;I build free, read-only token reports that do these checks for you — identity from storage rather than from a name, and an honest "unknown" where the chain cannot answer. No wallet connection, no signature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.xroot.dev/token-audit" rel="noopener noreferrer"&gt;Run a Token Report ↗&lt;/a&gt;&lt;a href="https://app.xroot.dev/solana/revoke-authority" rel="noopener noreferrer"&gt;Lock Down a Token&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything here is reproducible against &lt;code&gt;https://rpc.mainnet.chain.robinhood.com&lt;/code&gt; and the chain's &lt;a href="https://robinhoodchain.blockscout.com" rel="noopener noreferrer"&gt;Blockscout instance&lt;/a&gt;. Reference: &lt;a href="https://docs.robinhood.com/chain/" rel="noopener noreferrer"&gt;Robinhood Chain docs&lt;/a&gt;, &lt;a href="https://docs.arbitrum.io/launch-arbitrum-chain/chain-config/validation/compliance-filtering" rel="noopener noreferrer"&gt;Arbitrum on transaction filtering&lt;/a&gt;, and &lt;a href="https://l2beat.com/scaling/projects/robinhood" rel="noopener noreferrer"&gt;L2BEAT's risk breakdown&lt;/a&gt;. Figures read on 25 August 2026; re-run them before relying on any of it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;xroot.dev is not affiliated with, endorsed by, or sponsored by Robinhood Markets, Inc. "Robinhood Chain" is used here only to name the public blockchain this article examines. Nothing here is financial advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>security</category>
    </item>
    <item>
      <title>Solana's 90% Rent Cut: The Economics of SIMD-0437</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Sat, 22 Aug 2026 07:19:09 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/solanas-90-rent-cut-the-economics-of-simd-0437-g18</link>
      <guid>https://dev.to/sulimanmukhtar/solanas-90-rent-cut-the-economics-of-simd-0437-g18</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-rent-cut-simd-0437" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every account on Solana carries a refundable SOL deposit — and the network just cut the price of that deposit by &lt;strong&gt;90%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SIMD-0437, shipping with Agave 4.2, is being covered as "Solana gets cheaper". That is true, and it matters for anyone creating accounts at scale. But the more interesting consequence runs the other way: because the change is strictly a relaxation, every account funded at the &lt;em&gt;old&lt;/em&gt; rate keeps every lamport it was funded with. The SOL already locked across millions of forgotten token accounts just became a fixed, non-replenishing inventory — worth ten times more per account than anything created after activation.&lt;/p&gt;

&lt;p&gt;Here is what rent actually is, exactly what SIMD-0437 changes, and what the cut means for reclaims, airdrops, ZK compression, and mint costs — verified against the proposal text rather than the headlines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rent Is a Refundable Deposit, Not a Fee
&lt;/h2&gt;

&lt;p&gt;Solana charges for on-chain storage by requiring every account to hold a minimum SOL balance — the &lt;strong&gt;rent-exempt minimum&lt;/strong&gt; — proportional to the account's size. Under the long-standing rate of &lt;code&gt;6,960 lamports per byte&lt;/code&gt;, a standard token account (ATA) requires a deposit of roughly &lt;strong&gt;0.002 SOL&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The deposit is not spent. Close the account, and the lamports return to whatever address you designate. The catch is that nothing closes accounts automatically: sell a token to zero and its empty ATA stays open — holding its deposit — indefinitely. One memecoin season commonly leaves a wallet with 50–200 dead token accounts. Tens of millions of empty accounts have already been closed by people who noticed; far more are still sitting open.&lt;/p&gt;




&lt;h2&gt;
  
  
  SIMD-0437: 90% Off, in Five Feature-Gated Steps
&lt;/h2&gt;

&lt;p&gt;SIMD-0437 lowers &lt;code&gt;lamports_per_byte&lt;/code&gt; from &lt;strong&gt;6,960 to 696&lt;/strong&gt; — not in one jump, but in five independently feature-gated reductions. It ships with Agave 4.2, whose mainnet feature activations began rolling out in late August 2026.&lt;/p&gt;

&lt;p&gt;Current rate6,960 lamports / byte&lt;/p&gt;

&lt;p&gt;Steps 1–46,333 → 5,080 → 2,575 → 1,322&lt;/p&gt;

&lt;p&gt;Step 5 — final rate696 lamports / byte (−90%)&lt;/p&gt;

&lt;p&gt;At the final rate, opening a token account costs about &lt;strong&gt;0.0002 SOL&lt;/strong&gt; instead of about 0.002 SOL. For anything that creates accounts in bulk, the math shifts by an order of magnitude: an airdrop to 10,000 fresh wallets used to immobilize roughly 20 SOL in recipient account rent; the same drop will soon immobilize about 2 SOL.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Asymmetry Nobody Prices In: Old Accounts Keep Their Old Deposit
&lt;/h2&gt;

&lt;p&gt;The detail buried in the proposal text: SIMD-0437 is &lt;strong&gt;strictly a relaxation of existing constraints.&lt;/strong&gt; Accounts are &lt;em&gt;allowed&lt;/em&gt; to hold less, never forced down to the new minimum. And &lt;code&gt;CloseAccount&lt;/code&gt; returns the account's &lt;em&gt;actual&lt;/em&gt; lamport balance — whatever was deposited at creation time — not the current rent minimum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence:&lt;/strong&gt; every token account created before activation still returns ~0.002 SOL on close — forever. Every account created after full activation returns ~0.0002 SOL. The reclaimable stock sitting on-chain today is a &lt;strong&gt;finite inventory that stops replenishing.&lt;/strong&gt; It does not expire — but it never grows again, and the accounts only get easier to forget.&lt;/p&gt;

&lt;p&gt;Where that inventory hides, in rough order of how often people are surprised:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Empty token accounts.&lt;/strong&gt; Every token sold (or rugged) to zero left one behind at ~0.002 SOL. Active wallets routinely hold 0.1–0.4 SOL in dead ATAs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Failed program deploy buffers.&lt;/strong&gt; An interrupted &lt;code&gt;solana program deploy&lt;/code&gt; strands a buffer account holding rent for the &lt;em&gt;entire binary&lt;/em&gt; — often 1–5 SOL per failed attempt. Developers lose more here than traders do in token accounts.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Wrapped SOL accounts.&lt;/strong&gt; wSOL left over from DEX interactions is both a balance and a rent deposit, and unwraps back to plain SOL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this requires trusting a third party with keys. Closing an account is a standard instruction your own wallet signs, with the refund directed to an address you choose.&lt;/p&gt;




&lt;h2&gt;
  
  
  Second-Order Effects for Builders
&lt;/h2&gt;

&lt;p&gt;Two knock-on effects are worth re-running your numbers for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ZK compression loses most of its rent argument.&lt;/strong&gt; Compressed accounts' rent advantage falls from roughly 400× to roughly 40×. Still decisive for six-figure recipient lists; no longer decisive for a 500-wallet drop. If compression was on your roadmap purely for rent savings, the math has changed underneath it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed protocol fees become the dominant cost.&lt;/strong&gt; With storage nearly free, per-mint costs are increasingly whatever the protocol on top charges — Metaplex's 0.0015 SOL fee becomes the largest line item of a Core NFT mint, not the rent beneath it.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Cheaper Chain, and a Closing Window
&lt;/h2&gt;

&lt;p&gt;The rent cut is unambiguously good for Solana: cheaper onboarding, cheaper airdrops, cheaper mints. But it also quietly finalizes a ledger: the SOL locked under the old rate is the most valuable per-account reclaim the network will ever offer, and every account created from here on is worth a tenth as much to close.&lt;/p&gt;

&lt;p&gt;— Find Out What Your Wallets Are Holding —&lt;/p&gt;

&lt;h3&gt;
  
  
  See the exact total before you sign anything.
&lt;/h3&gt;

&lt;p&gt;I built a scanner for exactly this: it lists every empty token account, wSOL balance, and stranded deploy buffer in a wallet, and shows the exact SOL total you would get back — before you approve a single transaction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.xroot.dev/solana/reclaim-sol" rel="noopener noreferrer"&gt;Scan Your Wallet ↗&lt;/a&gt;&lt;a href="https://app.xroot.dev/solana/recover-failed-deploys" rel="noopener noreferrer"&gt;Recover Failed Deploys&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sources: the &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0437-incremental-rent-reduction.md" rel="noopener noreferrer"&gt;SIMD-0437 proposal&lt;/a&gt; and &lt;a href="https://www.helius.dev/blog/agave-v4-2" rel="noopener noreferrer"&gt;Helius's Agave 4.2 overview&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>web3</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Zero-Data-Loss Analytics: Scaling NATS and ClickHouse for Web3</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 14 Aug 2026 09:41:20 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/zero-data-loss-analytics-scaling-nats-and-clickhouse-for-web3-5ab</link>
      <guid>https://dev.to/sulimanmukhtar/zero-data-loss-analytics-scaling-nats-and-clickhouse-for-web3-5ab</guid>
      <description>&lt;p&gt;In &lt;strong&gt;Part 1: Economics &amp;amp; System Design&lt;/strong&gt;, we established our cloud economics and architecture blueprint. In &lt;strong&gt;Part 2: Ingesting the Solana Firehose with Rust&lt;/strong&gt;, we built a resilient, zero-copy Rust ingestion service that decodes raw transactions and pumps structured swap events into NATS.&lt;/p&gt;

&lt;p&gt;Now, we have to solve the final piece of the puzzle: &lt;strong&gt;Storage and Real-Time Delivery.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Piping millions of events into a traditional relational database like PostgreSQL will instantly lock tables, bloat disk space, and crash under heavy read/write concurrency. To analyze billions of historical swaps while simultaneously pushing live market feeds to end users with sub-second latency, we need a two-tier consumption layer: ClickHouse for analytical storage, and Axum WebSockets for live event fanout.&lt;/p&gt;




&lt;h2&gt;
  
  
  01. Consuming NATS JetStream &amp;amp; ClickHouse Schema Design
&lt;/h2&gt;

&lt;p&gt;When consuming high-velocity data streams, database maintenance, schema migrations, or unexpected container restarts are inevitable. If your consumer reads directly from an in-memory queue, restarting your database means &lt;strong&gt;dropping millions of incoming swap events.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  NATS JetStream: Persistence as a Safety Net
&lt;/h3&gt;

&lt;p&gt;To guarantee zero data loss, we configure NATS with &lt;strong&gt;JetStream persistence&lt;/strong&gt;. JetStream writes incoming messages to an encrypted, distributed disk stream. If our database goes offline for 15 minutes, NATS retains the stream and seamlessly replays every missing message the moment the database re-establishes its connection.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💾 &lt;strong&gt;JetStream vs. core NATS:&lt;/strong&gt; Core NATS is fire-and-forget. JetStream adds durable, persistent, replayable streams with consumer acknowledgements. For a financial data pipeline, JetStream is non-negotiable. A consumer crash with core NATS means a data gap; with JetStream it means a brief lag before catch-up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The ClickHouse Schema
&lt;/h3&gt;

&lt;p&gt;For storage, ClickHouse is the clear winner. As a column-oriented DBMS, ClickHouse compresses data by up to &lt;strong&gt;80%&lt;/strong&gt; and can execute aggregate queries across billions of rows in milliseconds — the exact performance profile a swap analytics dashboard demands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Production ClickHouse Schema for Solana Swaps&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;solana_swaps&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;signature&lt;/span&gt;      &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;slot&lt;/span&gt;           &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;block_time&lt;/span&gt;     &lt;span class="nb"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;CODEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DoubleDelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ZSTD&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;program_id&lt;/span&gt;     &lt;span class="n"&gt;LowCardinality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;signer&lt;/span&gt;         &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;token_in&lt;/span&gt;       &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;token_out&lt;/span&gt;      &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount_in&lt;/span&gt;      &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount_out&lt;/span&gt;     &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;     &lt;span class="nb"&gt;DateTime&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&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;MergeTree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="n"&gt;index_granularity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By using the MergeTree engine, partitioning by month, and selecting an ordering key optimised for token pair filtering — (program_id, token_in, token_out, block_time) — ClickHouse skips 99% of the disk data during typical analytical queries.&lt;/p&gt;

&lt;p&gt;⚡ Key insight: Using specialised codecs like DoubleDelta and ZSTD on timestamp columns dramatically reduces disk footprint, allowing billions of swap records to run on low-cost NVMe storage. LowCardinality(String) on program_id further reduces the column's on-disk size by up to 90%.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;High-Throughput Batch Ingestion
The single most common mistake engineers make with ClickHouse is executing single-row INSERT statements for every incoming event. ClickHouse is designed for bulk writes; single-row inserts create millions of tiny data parts on disk, triggering severe write amplification and bringing the cluster to its knees.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Dual-Threshold Flushing with tokio::select!&lt;br&gt;
Our Rust consumer service pools incoming NATS messages into memory buffers and executes bulk inserts using two thresholds:&lt;br&gt;
Threshold 1 (Batch size): ≥ 10,000 rows → flush immediately&lt;br&gt;
Threshold 2 (Time interval): every 1.0 s → flush if buffer non-empty&lt;br&gt;
tokio::select! races both futures concurrently. Whichever fires first triggers the flush. This means during peak volume the pipeline flushes at 10k-row boundaries (maximising throughput), and during quiet periods it flushes every second (minimising latency for dashboards).&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="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;clickhouse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;run_clickhouse_consumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;nats_sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;JetStreamSubscription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;Client&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;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_capacity&lt;/span&gt;&lt;span class="p"&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="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&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;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;select!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Priority 1: Fill buffer from the NATS stream&lt;/span&gt;
            &lt;span class="nf"&gt;Some&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nats_sub&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;bincode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;deserialize&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DecodedSwap&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="py"&gt;.payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;buffer&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;swap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="nf"&gt;.ack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&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;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;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="nf"&gt;flush_to_clickhouse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&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="c1"&gt;// Priority 2: Flush periodically during low volume&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="nf"&gt;.tick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;flush_to_clickhouse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&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="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;By decoupling row-by-row arrivals into structured bulk inserts, we maintain near-zero CPU usage on the database cluster while keeping end-to-end swap visibility latency under one second at all volume levels.&lt;/p&gt;

&lt;h2&gt;
  
  
  03. The Real-Time Interface: Decoupled WebSocket Fanout
&lt;/h2&gt;

&lt;p&gt;Frontend applications — live trading dashboards, arbitrage bots, portfolio trackers — need to see swaps the &lt;strong&gt;exact millisecond they happen&lt;/strong&gt;. Polling ClickHouse repeatedly to check for new rows creates unnecessary read pressure on your analytical storage and introduces artificial latency.&lt;/p&gt;

&lt;p&gt;Instead, we build a dedicated, lightweight WebSocket service using &lt;code&gt;Axum&lt;/code&gt; that listens directly to the NATS subject — completely independent of the ClickHouse consumer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Helius RPC ──&amp;gt; Rust Ingester ──&amp;gt; NATS JetStream ──┬──&amp;gt; ClickHouse (Batch / Analytical)
                                                  └──&amp;gt; Axum WS (Live Fanout / Clients)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because NATS supports pub/sub topics out of the box, our Axum WebSocket service acts as a pure fanout router:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a client connects to &lt;code&gt;wss://api.xroot.dev/ws/swaps?pair=SOL-USDC&lt;/code&gt;, Axum subscribes them to the corresponding NATS subject (e.g. &lt;code&gt;solana.swaps.raydium&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;When the Rust Ingester publishes a decoded swap to NATS, NATS &lt;strong&gt;broadcasts it simultaneously&lt;/strong&gt; to all active WebSocket clients and the ClickHouse batch consumer — no polling, no secondary query.&lt;/li&gt;
&lt;li&gt;Thousands of concurrent frontend WebSocket connections consume live data feeds with &lt;strong&gt;zero impact on database performance&lt;/strong&gt;, since ClickHouse is never touched by the fanout path.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;🔀 &lt;strong&gt;Why not query ClickHouse for live data?&lt;/strong&gt; ClickHouse excels at analytical aggregation — it is not designed for sub-millisecond single-row lookups. Routing live streaming clients through NATS keeps the database free for the heavy aggregation workloads it was built for: hourly volume, OHLCV candles, top-pair rankings.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion &amp;amp; System Summary
&lt;/h2&gt;

&lt;p&gt;We have successfully engineered an end-to-end, high-throughput Web3 data pipeline from scratch. Here is the complete picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filtered RPC Ingestion:&lt;/strong&gt; Optimised Helius WebSocket subscriptions cap costs at under 8M credits/day on a $999/month budget — no wasted credit burn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Copy Rust Engine:&lt;/strong&gt; &lt;code&gt;carbon&lt;/code&gt; + bounded &lt;code&gt;tokio&lt;/code&gt; channels on a cost-effective 4-CPU pod handle thousands of events per second with no GC pauses and no memory leaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guaranteed Delivery:&lt;/strong&gt; NATS JetStream persists the stream to disk, ensuring zero data loss during downstream outages and enabling seamless replay on reconnect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytical Storage &amp;amp; Fanout:&lt;/strong&gt; A partitioned ClickHouse MergeTree table delivers sub-second analytical queries over billions of rows. A decoupled Axum WebSocket service fans out live swap events to frontend clients with zero database pressure.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>web3</category>
      <category>systemdesign</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Ingesting the Solana Firehose: High-Throughput Decoding with Rust</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Wed, 12 Aug 2026 06:15:57 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/ingesting-the-solana-firehose-high-throughput-decoding-with-rust-4a5c</link>
      <guid>https://dev.to/sulimanmukhtar/ingesting-the-solana-firehose-high-throughput-decoding-with-rust-4a5c</guid>
      <description>&lt;p&gt;In &lt;strong&gt;Part 1: Economics &amp;amp; System Design&lt;/strong&gt;, we established the blueprint. We proved that by optimizing Helius RPC subscriptions and decoupling our ingestion layer from our database using NATS, we can track all Solana swap volume without burning our runway on infrastructure costs.&lt;/p&gt;

&lt;p&gt;Now, we have to actually &lt;strong&gt;build the ingestion engine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you are dealing with thousands of transactions per second across Raydium, Orca, and Jupiter, traditional REST polling is obsolete. We need a persistent, real-time WebSocket connection. But keeping a socket alive at this scale, decoding the payload without causing CPU spikes, and forwarding it to our broker requires strict memory management.&lt;/p&gt;

&lt;p&gt;Here is how we architect the Rust ingestion service to survive the firehose on a highly cost-effective &lt;strong&gt;4-CPU pod.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  01. Connection Resilience &amp;amp; The Reconnection Loop
&lt;/h2&gt;

&lt;p&gt;Public and private WebSocket connections drop. Network blips happen. Helius might briefly cycle a node. If your Rust service uses a naive TCP stream that panics on a closed socket, &lt;strong&gt;your pipeline is dead in the water&lt;/strong&gt; and your database starts missing swaps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔁 &lt;strong&gt;Principle:&lt;/strong&gt; Resilience is not a feature. It is the foundation. A production ingestion service must detect a dead socket and re-establish its connection automatically — with no human intervention and no data gap.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We wrap our WebSocket connection in a &lt;code&gt;tokio&lt;/code&gt; asynchronous reconnection loop equipped with &lt;strong&gt;exponential backoff&lt;/strong&gt; and heartbeat monitoring. Instead of crashing, the service detects a missed ping/pong heartbeat or a dropped stream, pauses, and safely reconnects.&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;stream_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Sender&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RawEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;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;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&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;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;connect_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&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="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Connected to Helius Enhanced WebSocket"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&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;// Reset on success&lt;/span&gt;

                &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;process_message&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&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="nf"&gt;Err&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="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Disconnected: {}. Retrying in {:?}..."&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="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="c1"&gt;// Exponential backoff, capped at 30 s&lt;/span&gt;
                &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;cmp&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;backoff&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="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backoff starts at 1 second and doubles on each consecutive failure, capped at 30 seconds. On a successful reconnect, it resets to 1 second. This pattern ensures the service does not hammer Helius during an outage, while recovering instantly under normal network conditions.&lt;/p&gt;

&lt;p&gt;By handling socket death gracefully, this loop ensures that our container never has to be manually restarted by Kubernetes or Docker during a network hiccup. The entire reconnection lifecycle is invisible to the downstream NATS publisher.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Zero-Copy Decoding with the Carbon Crate
The fastest way to max out a 4-CPU server is attempting to fully deserialize every single transaction payload on Solana into a massive JSON object. The vast majority of the firehose is noise — NFT mints, token transfers, and governance votes that our swap indexer doesn't care about. If we allocate memory for those, we lose.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Filtering by Program Discriminators&lt;br&gt;
To solve this, we leverage the Carbon framework. Carbon is an indexing framework built for Solana that allows us to pipeline our data: Datasource → Decoder → Processor.&lt;/p&gt;

&lt;p&gt;Instead of deserializing the entire block, we configure our Helius datasource to apply a RpcBlockSubscribeFilter. This drops non-swap transactions at the RPC layer in nanoseconds — before they ever enter our process memory. For the transactions that do come through, Carbon's generated decoders turn the raw byte arrays directly into typed Rust structures without unnecessary allocations.&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="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;carbon_core&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;
    &lt;span class="nn"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;carbon_rpc_block_subscribe_datasource&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;
    &lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RpcBlockSubscribe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Filter noise at the RPC layer — nanosecond cost&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;datasource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;RpcBlockSubscribe&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;helius_ws_url&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nn"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;RpcBlockSubscribeFilter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;MentionsAccountOrProgram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;RAYDIUM_PROGRAM_ID&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nb"&gt;None&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="c1"&gt;// 2. Carbon pipeline: Datasource → Decoder → Processor&lt;/span&gt;
&lt;span class="nn"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.datasource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datasource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.instruction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RaydiumDecoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;SwapProcessor&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nats_tx&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="o"&gt;?&lt;/span&gt;
    &lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚡ Key insight: By filtering by Program ID at the RPC layer, a 4-CPU pod can comfortably track all Raydium and Orca swap volume. Non-swap transactions are dropped in nanoseconds. Memory is only allocated for actual swap events that pass the discriminator check.&lt;/p&gt;

&lt;p&gt;By strictly filtering by Program Discriminators (like Raydium's program ID) and using Carbon to map byte arrays directly into structs, our CPU utilization stays incredibly low, easily fitting within our compute constraints.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Managing Backpressure &amp;amp; NATS Publishing
We are now successfully isolating and decoding swap events. But what happens when Solana processes a massive burst of volume in a single slot, and our internal network to NATS briefly lags for 10 milliseconds?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If our pipeline is perfectly synchronous, the WebSocket reader will block while waiting for the NATS publish to succeed. The incoming TCP buffer will instantly fill up, and Helius will aggressively disconnect our socket for being too slow.&lt;/p&gt;

&lt;p&gt;Decoupling I/O with Bounded Channels&lt;br&gt;
To prevent this, we completely decouple our I/O tasks using tokio::sync::mpsc (Multi-Producer, Single-Consumer) bounded channels.&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;// Bounded channel — acts as an in-memory ring buffer&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DecodedSwap&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&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;// Task 1: Producer — WebSocket reader / Carbon pipeline&lt;/span&gt;
&lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;run_carbon_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Task 2: Consumer — NATS publisher, drains independently&lt;/span&gt;
&lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;nc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;async_nats&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="s"&gt;"nats://localhost:4222"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rx&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;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;bincode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Err&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nc&lt;/span&gt;&lt;span class="nf"&gt;.publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"solana.swaps.raydium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NATS publish failed: {}"&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="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 bounded channel with a capacity of 10,000 acts as a shock absorber. The WebSocket reader (Producer) can instantly dump decoded swaps into the channel and immediately go back to reading the socket.&lt;/p&gt;

&lt;p&gt;🛡 The NATS publisher (Consumer) drains the channel at its own pace. If a downstream service lags, the bounded buffer absorbs the spike without ever blocking the incoming WebSocket stream. If the buffer fills completely, the producer applies backpressure naturally — preventing unbounded memory growth.&lt;/p&gt;

&lt;p&gt;This two-task architecture is the key to operating safely on a cost-effective pod. The WebSocket reader and the NATS publisher run as independent tokio tasks on the same thread pool, coordinating through the channel without any locks, mutexes, or shared mutable state.&lt;/p&gt;

&lt;p&gt;What's Next?&lt;br&gt;
Our Rust service is now resilient, highly efficient, and pumping structured data into our message broker without dropping packets. To summarize what we have built:&lt;br&gt;
Exponential backoff reconnection loop — the pipeline self-heals from network blips with no human intervention and no data gap.&lt;br&gt;
Carbon framework with program discriminator filtering — non-swap transactions are dropped at the RPC layer in nanoseconds, keeping CPU and memory usage minimal.&lt;br&gt;
Bounded mpsc channels — the WebSocket reader and NATS publisher run as fully decoupled async tasks, absorbing traffic spikes without blocking the incoming socket.&lt;br&gt;
Data in motion is useless without a place to query it. In Part 3: Zero-Data-Loss Analytics with ClickHouse, we will consume these NATS streams, design an optimal ClickHouse schema for billions of swap rows, and build the real-time WebSocket interface for the frontend.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>web3</category>
      <category>systemdesign</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Architecting a Solana Data Pipeline: Economics &amp; System Design</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Mon, 10 Aug 2026 11:12:23 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/architecting-a-solana-data-pipeline-economics-system-design-41g8</link>
      <guid>https://dev.to/sulimanmukhtar/architecting-a-solana-data-pipeline-economics-system-design-41g8</guid>
      <description>&lt;h1&gt;
  
  
  Architecting a Solana Data Pipeline: Economics &amp;amp; System Design
&lt;/h1&gt;

&lt;p&gt;If you have ever tried to track every single swap across Raydium, Orca, and Jupiter in real-time, you already know the painful truth: &lt;strong&gt;Solana is simply too fast for traditional Web2 architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Standard REST API polling will get you rate-limited instantly. If you try to blindly open a WebSocket and dump the raw payload into a standard database, your server will run out of memory, drop packets, and corrupt your dataset. Drinking from the Solana firehose requires a paradigm shift in how we handle data ingestion.&lt;/p&gt;

&lt;p&gt;This is Part 1 of a 3-part series where we will design a resilient, zero-data-loss swap indexer. We will architect the system, write a high-throughput ingestion service in Rust, and scale the analytics using ClickHouse.&lt;/p&gt;

&lt;p&gt;Before we write a single line of code, we have to solve the hardest problem in Web3 infrastructure: &lt;strong&gt;Cloud Economics.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  01. Cloud Economics &amp;amp; The RPC Math
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The fastest way to kill a Web3 startup is uncontrolled RPC usage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you subscribe to real-time blockchain events, every message, parsed transaction, and account fetch burns RPC credits. If you scale your ingestion blindly, your infrastructure bill will scale exponentially right alongside it.&lt;/p&gt;

&lt;p&gt;For high-throughput Solana pipelines, &lt;strong&gt;Helius is the industry standard&lt;/strong&gt;, but you have to engineer your system to fit their pricing model.&lt;/p&gt;

&lt;p&gt;Currently, the Helius Professional Plan offers &lt;strong&gt;200 million monthly credits for $999/month&lt;/strong&gt;. If you want to track all swap volume on Solana today, optimizing your WebSocket subscriptions and transaction fetching is mandatory. By filtering intelligently and only decoding the exact transaction signatures you need, tracking all swaps consumes roughly &lt;strong&gt;5 to 8 million credits a day&lt;/strong&gt;.&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;Value / Estimate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Helius Professional Plan&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;200M credits / month · $999&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average Daily Swap Volume&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~3M swaps / day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Credits per Enhanced Transaction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1–2 credits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Estimated Daily Credit Burn&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5–8M credits / day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Peak Monthly Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ 240M credits — within plan ✓&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This keeps your monthly consumption comfortably under the 200M credit ceiling, proving that high-throughput Web3 infrastructure doesn't have to bankrupt the startup — if engineered correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  02. The Core Architecture Flow
&lt;/h2&gt;

&lt;p&gt;To handle this volume without dropping data, we must &lt;strong&gt;decouple the ingestion speed from the database write speed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Helius RPC ──&amp;gt; Rust Ingester (4-CPU) ──&amp;gt; NATS Broker ──┬──&amp;gt; ClickHouse (Analytics)&lt;br&gt;
└──&amp;gt; Axum WS (Live Feeds)&lt;/p&gt;

&lt;h3&gt;
  
  
  The Ingestion Layer (Rust)
&lt;/h3&gt;

&lt;p&gt;We are processing thousands of messages a second. Traditional languages will choke on garbage collection pauses at this scale. By using Rust, we can safely manage memory and handle the intense workload of streaming and decoding raw transactions on a highly cost-effective &lt;strong&gt;4-CPU server pod.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Message Broker (NATS)
&lt;/h3&gt;

&lt;p&gt;This is the MVP of the entire architecture. If ClickHouse goes down for a split-second or experiences a CPU spike, the Rust ingester doesn't care. It simply publishes the decoded payload to NATS. &lt;strong&gt;NATS handles the backpressure and guarantees message replayability&lt;/strong&gt;, ensuring zero data loss.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage &amp;amp; Interface (ClickHouse &amp;amp; WebSockets)
&lt;/h3&gt;

&lt;p&gt;ClickHouse independently consumes the NATS stream to build tables for heavy historical analytics. Simultaneously, a separate lightweight WebSocket service can listen to the exact same NATS subjects to push live swap events directly to frontend clients.&lt;/p&gt;




&lt;h2&gt;
  
  
  03. Network Topology: The Zero-Latency Requirement
&lt;/h2&gt;

&lt;p&gt;Having the right hardware and software stack is useless if your network topology is flawed.&lt;/p&gt;

&lt;p&gt;When processing real-time financial data, &lt;strong&gt;external internet routing is your biggest enemy.&lt;/strong&gt; To prevent latency lag and bandwidth bottlenecks, your Rust Ingester, NATS broker, and ClickHouse database must be deployed within the same local network (Virtual Private Cloud).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;Rule:&lt;/strong&gt; Keep all internal service-to-service communication inside the VPC. Internal latency is measured in &lt;em&gt;microseconds&lt;/em&gt;. External internet routing adds 5–50ms per hop — which at thousands of messages per second means a queue that grows faster than it is consumed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By keeping all internal communication localized, you eliminate external SSL handshake overhead and internet routing hops. &lt;strong&gt;The only time data touches the public internet&lt;/strong&gt; is when it arrives from Helius, and when it is served to your end user.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion &amp;amp; Next Steps
&lt;/h2&gt;

&lt;p&gt;We now have the complete economic and architectural foundation. To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Helius Professional Plan&lt;/strong&gt; with filtered WebSocket subscriptions keeps costs predictable and within budget even at peak Solana swap volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; is the only language that sustains this memory footprint safely at the required throughput with no GC pauses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NATS&lt;/strong&gt; decouples ingestion speed from write speed, absorbing backpressure and guaranteeing zero data loss during downstream hiccups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC co-location&lt;/strong&gt; reduces inter-service latency from milliseconds to microseconds and eliminates the public attack surface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now that our blueprint is locked in, we need to handle the incoming firehose. In &lt;strong&gt;Part 2: Ingesting the Solana Firehose with Rust&lt;/strong&gt;, we will write the high-throughput service that decodes raw transactions using the &lt;code&gt;carbon&lt;/code&gt; crate, handles reconnection logic, and publishes structured events to NATS with zero data loss.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>systemdesign</category>
      <category>clickhouse</category>
      <category>blockchain</category>
    </item>
  </channel>
</rss>
