TL;DR

  • Polling is simple to implement, but it ships redundant payloads, adds network latency on every call, and can hand back stale data between intervals
  • Streaming is the better choice for real-time workloads, but moving an existing codebase onto it is a serious engineering lift
  • Triton's new SDK supercharges your polling with streaming-grade freshness and bandwidth-only pricing
  • Under the hood, it consolidates your reads into one streaming subscription (gRPC or WebSocket) and resolves account queries from local RAM instead of the network
  • It covers single, multiple, and parsed-account reads, context variants, dataSlice and minContextSlot filters, and live subscription management
  • @triton-one/triton-sdk keeps full API compatibility with @solana/web3.js, requiring just one config change to integrate
  • And the triton-sdk crate does the same for Rust

Introduction

We love making devs' lives better. It's why we built Yellowstone gRPC for ultra-low-latency data access, and Fumarole for persistent, high-availability updates that survive reconnects.

One question we kept coming back to was: if streaming clearly wins on all fronts for real-time account data, but switching to it requires a significant code rewrite, could we bring the benefits to everyone without requiring the change?

Account Sync, a feature of our new Triton SDK, is our answer.

What is Account Sync

Account Sync is a Triton service that makes your polling faster and cheaper, with next-to-no migration effort. It automatically merges your account reads into a single streaming subscription, keeps updates cached locally, and resolves your calls from RAM.

The SDK and our backend handle all the plumbing, delivering:

Faster account reads

With traditional polling, there are two delays baked in:

  1. Slower read path. RPC can only answer after the node finishes replaying the block, even if the account was being updated continuously during replay.
  2. The interval. If you poll every 100ms, an account that changes 5ms after your last read stays stale in your app for the remaining 95ms, plus the round-trip.

With Account Sync, we close both gaps: the SDK opens a streaming subscription for each new account you read, updates your local state in real time during replay, and serves your queries from the freshest data available, with no network delay.

This also allows you to poll far more aggressively (every 10ms or less), without hitting rate limits or worrying about costs.

Lower per-request and bandwidth costs

When polling, you're billed for both the request and the bandwidth on every call, because the RPC node has to look up the account, serialise the entire payload, and send the complete JSON back, even if nothing has changed.

The upcoming Alpenglow upgrade will force apps to poll more often just to keep up with faster finality, further multiplying the costs. 

Because Account Sync removes repetitive work from the node, it lets you pay for just one RPC request plus the bandwidth cost of updates, and never for data that hasn't changed.

Near-zero migration effort

We didn't want the size of your codebase or a packed roadmap to stand between you and faster, cheaper reads, so the setup is minimal. 

The SDKs are fully API-compatible with @solana/web3.js and solana-client: swap one import, add one accountSync config block, and you're done. Your web3.js & solana-client imports, calls keep working.

It lets you keep the same signatures, parameters, and return types, requiring zero rewrite of your polling code. The only thing the SDK changes is where reads resolve, which it handles entirely on its own.

Who AccountSyncSDK is for

Account Sync is built for any app that reads account state in a loop. Since it streams over both gRPC and WebSocket, it fits Node backends, workers, and scripts just as well as browser frontends, benefiting:

  • Wallets and portfolio UIs, reading balances and token accounts continuously
  • DEXs and trading dashboards, tracking pool and market state across many accounts
  • Games and NFT marketplaces, polling on-chain state per frame and per view
  • Explorers and analytics frontends rendering decoded account data in real-time
  • Bots and services with a rotating wallet and token watchlists

However, if you're building latency-critical trading (HFT, MEV, liquidation engines), direct gRPC streaming is still the fastest path. We've laid out all three approaches below so you can choose what's best for your workload:


RPC polling

Account Sync

gRPC streaming

Payload size

Full account (JSON)

Protobuf

Protobuf

Migration effort

None

One import swap plus a config block

Complete architectural rewrite

Bandwidth cost

High

Low

Low

Provider cost

$10/M + $0.08/GB

$0.08/GB

$0.08/GB

Best for

Infrequent, one-off reads

Regularly polling the account state

Latency-sensitive backends

How it works

Account Sync has two parts: a client-side SDK you install, and a streaming fanout service running on Triton's infrastructure.

Drop-in SDK with local resolution

Once you swap your SDKs with the Triton SDKs, it builds a local buffer by fetching an account once over JSON-RPC on your first read, subscribing to its updates over gRPC or WebSocket, and applying them to your store.

These reads then resolve straight from your RAM:

  • getAccountInfo and getAccountInfoAndContext for single accounts
  • getMultipleAccountsInfo and getMultipleAccountsInfoAndContext for batches
  • getParsedAccountInfo and getMultipleParsedAccounts for decoded, human-readable account data

The SDKs automatically subscribes to updates for accounts that you request and caches them. You can also mention which accounts to cache when you create the Connection object.

You can check out the docs here: https://docs.triton.one/project-yellowstone/yellowstone-account-sync

Every other JSON-RPC method works the same as before, routing through a standard request to an RPC node.

The fanout service architecture

The fanout service sits between Dragon's Mouth and your SDK, routing each account update to the connections that asked for it. Three layers handle that:

  • Upstream handler maintains a gRPC subscription to the chain and adjusts it whenever the tracked-account set changes
  • Account filter engine indexes which connection is watching which account, routes each update to the matching connections, and passes new subscriptions to the upstream handler
  • Frontend layer accepts your SDK connection over gRPC or WebSocket, sending updates out to you and relaying your subscription changes back to the filter engine

Get started in < 2 minutes

If your app already uses @solana/web3.js, switching over is straightforward. For most teams, it comes down to swapping the import and adding the accountSync block. 

Before:

import { Connection, PublicKey } from "@solana/web3.js";
const accountId = "ping6gwBZx1ccMMFyLgkVSupUmujYrFidEXuNRPq989";
const connection = new Connection("https://api.rpcpool.com/YOUR_TOKEN");
const account = await connection.getAccountInfo(new PublicKey(accountId));

After:

import { Connection, PublicKey } from "@triton-one/triton-sdk";
const accountId = "ping6gwBZx1ccMMFyLgkVSupUmujYrFidEXuNRPq989";
const connection = new Connection("https://api.rpcpool.com/YOUR_TOKEN", {
  accountSync: {
    transport: "grpc",
    initialAccounts: [accountId], // accounts to buffer since start
  },
});
const account = await connection.getAccountInfo(new PublicKey(accountId));

Rust:

use std::{env, error::Error};
use triton_sdk::{AccountSyncConfig, CommitmentConfig, Pubkey, RpcClient};

async fn main() -> Result<(), Box<dyn Error>> {
    let address: Pubkey = "So11111111111111111111111111111111111111112".parse()?;
    let client =
        RpcClient::new_with_commitment(env::var("RPC_URL")?, CommitmentConfig::confirmed())
            .with_account_sync(AccountSyncConfig {
                endpoint: env::var("ACCOUNT_SYNC_URL")?,
                pinned_accounts: [address].into(),
                ..Default::default()
            })?;

    let result = client
        .get_account_with_commitment(&address, client.commitment())
        .await;
    let closed = client.close().await;
    let response = result?;
    closed?;

    println!("Observed at slot: {}", response.context.slot);
    println!("Account: {:?}", response.value);
    Ok(())
}

Happy smart polling 🌊