> For the complete documentation index, see [llms.txt](https://docs.triton.one/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.triton.one/solana/reading-account-state/account-sync/rust-sdk-reference.md).

# Account Sync Rust SDK reference

Yellowstone Account Sync in the triton-sdk Rust crate: RpcClient modes, AccountSyncConfig defaults, get\_account reads, pinned accounts, errors and API.

The `triton-sdk` crate (version 0.1.0) adds [Yellowstone Account Sync](/solana/reading-account-state/account-sync.md) to the asynchronous Solana `RpcClient`. Five raw account-read methods resolve from an in-memory buffer fed by a gRPC stream; every other method goes to JSON-RPC through the wrapped client. For what Account Sync is and a first program, start with the [overview](/solana/reading-account-state/account-sync.md).

```bash
cargo add triton-sdk
cargo add tokio --features rt,macros
```

* Requires **Rust 1.97.1 or newer**. Version 0.1.0 pins `solana-client` 4.3.0, which sets that minimum.
* Account Sync runs on Tokio. Reads must happen inside a Tokio runtime.
* Import Solana types (`Pubkey`, `Account`, `CommitmentConfig`) from `triton_sdk` so they match its Solana versions.

## Client modes

`RpcClient` has two modes, tracked in its type:

* **`RpcClient<Plain>`** (the default type) comes from `RpcClient::new`, `new_with_commitment`, `new_with_timeout`, `new_with_timeout_and_commitment` or `from_inner`. It uses Solana RPC directly.
* **`RpcClient<Configured>`** comes from `with_account_sync(config)`. It adds buffered reads and account controls.

```rust
let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed())
    .with_account_sync(AccountSyncConfig {
        endpoint: account_sync_url,
        ..Default::default()
    })?;
```

`with_account_sync` validates the configuration and returns `ConfigError` if it's invalid. It starts nothing: the first supported read at a commitment starts that commitment's stream.

* `RpcClient::from_inner(existing)` wraps a `solana_client::nonblocking::rpc_client::RpcClient` (version 4.3.0) and keeps its URL, timeout and commitment.
* `client.inner()` returns the wrapped Solana client. Its methods bypass the buffer.
* Every other Solana client method is available on `RpcClient` through `Deref` and goes to RPC.
* `triton_sdk::RpcClient` isn't Solana's `RpcClient` type.
* After `with_account_sync`, change explicit type annotations that used the plain `RpcClient` to `RpcClient<Configured>`.
* There is no blocking Account Sync client.
* Clones share the RPC client and Account Sync state. Closing one clone stops Account Sync for all of them.
* The default read commitment comes from the wrapped client: `RpcClient::new` uses `finalized`, so pass a commitment explicitly if you want `confirmed`.
* RPC request timeouts also belong to the wrapped client. Set them with `new_with_timeout_and_commitment` or `from_inner`.

## Endpoint and token

`AccountSyncConfig.endpoint` is the Account Sync gRPC URL. It is required and is never derived from the RPC URL; use the same URL for both if your endpoint serves both.

* The scheme must be `http` or `https`. Without a scheme, the SDK uses `https`, or `http` for `localhost`, `127.0.0.1`, `[::1]` and `0.0.0.0`.
* The path must be empty or exactly `/<token>`. The SDK sends the token as `x-token` metadata, not in the URL. Query strings and fragments are ignored.
* A `ws://` URL fails with `ConfigError::EndpointScheme`, and a longer path with `ConfigError::EndpointPath`.
* `https` uses TLS with the operating system's root certificates.

The RPC URL belongs to the wrapped Solana client, with your token in its path as usual.

## Configuration

Pass an `AccountSyncConfig` to `with_account_sync`. Durations are `std::time::Duration` values.

```rust
use std::time::Duration;

let config = AccountSyncConfig {
    endpoint: account_sync_url,
    pinned_accounts: [address].into(),
    cache_miss_wait: Duration::from_secs(5),
    dynamic_subscription_lifetime: Duration::from_secs(60),
    ..Default::default()
};
```

| Field                           | Type              | Default             | Meaning                                                                                       |
| ------------------------------- | ----------------- | ------------------- | --------------------------------------------------------------------------------------------- |
| `endpoint`                      | `String`          | Empty (must be set) | Account Sync gRPC URL.                                                                        |
| `pinned_accounts`               | `HashSet<Pubkey>` | Empty               | Accounts tracked at every commitment until removed.                                           |
| `automatic_subscriptions`       | `bool`            | `true`              | Track an unpinned account temporarily when it's read.                                         |
| `cache_miss_wait`               | `Duration`        | 5 s                 | How long a read waits for the buffer before relying only on RPC. Not a deadline for the read. |
| `subscription_refresh`          | `Duration`        | 1 s                 | Tick for expiring temporary subscriptions and retrying failed initial loads.                  |
| `reconnect_min_delay`           | `Duration`        | 100 ms              | First reconnect delay. Doubles after each failure.                                            |
| `reconnect_max_delay`           | `Duration`        | 5 s                 | Largest reconnect delay. Must be at least `reconnect_min_delay`.                              |
| `connect_timeout`               | `Duration`        | 10 s                | Limit for establishing the gRPC connection.                                                   |
| `close_timeout`                 | `Duration`        | 5 s                 | Limit for `close()` to wait for background tasks.                                             |
| `dynamic_subscription_lifetime` | `Duration`        | 60 s                | Lifetime of a temporary subscription after its most recent read.                              |
| `http2_window_size`             | `u32`             | 16 MiB              | Initial HTTP/2 connection and stream receive windows.                                         |
| `max_decoded_message_size`      | `usize`           | 16 MiB              | Largest decoded gRPC message accepted.                                                        |
| `keepalive_interval`            | `Duration`        | 30 s                | Interval between HTTP/2 keepalive pings.                                                      |
| `keepalive_timeout`             | `Duration`        | 10 s                | Time allowed for a keepalive response.                                                        |
| `keepalive_while_idle`          | `bool`            | `true`              | Send keepalive pings while no stream is active.                                               |

Every duration and both size limits must be nonzero. There's no commitment field, no transport selector and no WebSocket option. The client accepts gzip and Zstd stream compression.

`client.account_sync_config()` returns an owned snapshot of the configuration, including the current pinned set. Changing the snapshot doesn't affect the client.

## Reading accounts

These methods on `RpcClient<Configured>` use the buffer:

| Method                                                        | Success value                                                                                         |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `get_account(&pubkey)`                                        | `Account`. A missing account is a `ClientError` with the message `AccountNotFound: pubkey=<address>`. |
| `get_account_data(&pubkey)`                                   | `Vec<u8>`. A missing account is the same error as `get_account`.                                      |
| `get_account_with_commitment(&pubkey, commitment)`            | `Response<Option<Account>>`                                                                           |
| `get_multiple_accounts(&pubkeys)`                             | `Vec<Option<Account>>`                                                                                |
| `get_multiple_accounts_with_commitment(&pubkeys, commitment)` | `Response<Vec<Option<Account>>>`                                                                      |

All are `async` and return the Solana `ClientError` type on failure. `get_account`, `get_account_data` and `get_multiple_accounts` use the wrapped client's commitment.

### How a read resolves

1. If the buffer holds every requested account, the read returns from memory with no RPC request.
2. Otherwise the SDK makes the normal Solana RPC call and waits up to `cache_miss_wait` for the buffer at the same time. Whichever finishes first wins, including an RPC error. If the wait expires, the read waits for RPC under the wrapped client's timeout.
3. Successful RPC results are stored for accounts that are tracked.

`None`, or an error message starting with `AccountNotFound`, means RPC observed the account as missing. RPC failures stay errors.

Multiple-account reads use the buffer only for 1 to 100 addresses. An empty list or more than 100 addresses goes straight to the RPC method, which accepts at most 100 and applies its usual validation and errors. The SDK doesn't split a larger list into batches. Results keep input order and duplicates.

### Context slot

A value from RPC carries the RPC context slot. A value from the stream carries the slot of that account's last update, which can lag the cluster for an unchanged account. A buffered multiple-account response uses the lowest slot among its accounts.

### What isn't buffered

The buffered methods take no `dataSlice`, `min_context_slot` or encoding options, and return raw `Account` values (which have no `space` field; use `data.len()`). Methods such as `get_ui_account_with_config`, `get_multiple_ui_accounts_with_config` and parsed reads go to RPC.

## Pinned and temporary accounts

**Pinned** accounts stay tracked at every commitment until you remove them. Set them in `pinned_accounts` or change them at runtime:

```rust
client.add_accounts([address_c, address_d]).await?;
client.remove_accounts([address_a]).await?;
client.replace_accounts([address_b, address_c]).await?;
```

* The methods take any `IntoIterator<Item = Pubkey>` and return `Result<(), AccountSyncError>`.
* There is no commitment argument: changes apply to every running commitment and to commitments started later.
* Removing an address that isn't pinned succeeds. A change made before the first read only updates the configuration.

**Temporary** subscriptions are created when you read an unpinned account and `automatic_subscriptions` is `true`. One lasts `dynamic_subscription_lifetime` after the most recent read of that account and is removed on the next `subscription_refresh` tick. Temporary subscriptions belong to the commitment of the read.

`remove_accounts` and `replace_accounts` don't end a temporary subscription; it stays until it expires, and reads renew it. To make the pinned set the complete tracked set, set `automatic_subscriptions: false`. Unpinned reads then go to RPC and their results aren't kept.

The SDK sets no limit on the number of tracked accounts.

## Commitment levels

Each `CommitmentConfig` you read with gets its own stream and buffer, started by the first read at that commitment. All commitments share the pinned set. A commitment's stream with nothing to track waits idle, and its task runs until `close()`.

## Stream lifecycle

* When a stream session opens, the SDK loads every tracked account at that commitment over RPC, one `getAccountInfo` request per account. Stream updates that arrive first are kept when they're newer. Failed loads are retried on each `subscription_refresh` tick.
* Any change to a commitment's tracked set, including a temporary subscription starting or expiring, clears that commitment's buffer, reconnects its stream with the new set and reloads every tracked account.
* If the stream fails, the SDK clears that commitment's buffer and reconnects with backoff from `reconnect_min_delay` to `reconnect_max_delay`. It doesn't poll tracked accounts in the meantime.

{% hint style="warning" %}
**Known limitation (0.1.0):** while a stream is disconnected, an account value that a read fetched through RPC stays in the buffer, and later reads return it without refreshing until the stream reconnects. If the stream can never connect (wrong `endpoint`, token or access) and the tracked set stays the same, those values never refresh. Confirm the stream works before production, for example by checking that the context slot of a frequently changing account advances. For a read that must come from RPC, call `client.inner()`.
{% endhint %}

## Errors

| Operation                                                      | Error type         | Variants                                                                                                                              |
| -------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `with_account_sync`                                            | `ConfigError`      | `EndpointScheme`, `EndpointUrl`, `EndpointPath`, `EndpointToken`, `Endpoint`, `Zero(field)`, `ReconnectRange`                         |
| Buffered reads                                                 | `ClientError`      | Solana client errors. A missing account from `get_account` or `get_account_data` has the message `AccountNotFound: pubkey=<address>`. |
| `add_accounts`, `remove_accounts`, `replace_accounts`, `close` | `AccountSyncError` | `Closed` after `close()`; `CloseTimeout` or `TaskJoin` from `close()`; `ChannelClosed` if background work stopped unexpectedly        |

Stream failures trigger reconnection and are not returned to your code. There's no public accessor for the last stream error, and a successful read doesn't prove the stream is connected.

**Closing.** `close().await` cancels Account Sync work and waits up to `close_timeout` for it to stop, returning `AccountSyncError::CloseTimeout` if it doesn't. Later calls return `Ok(())`. After closing, account controls return `AccountSyncError::Closed`, and reads go straight to RPC. Dropping the last clone also cancels background work, without waiting.

## Complete lifecycle example

```rust
use std::{env, error::Error};

use triton_sdk::{AccountSyncConfig, CommitmentConfig, Pubkey, RpcClient};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
    let address: Pubkey = "So11111111111111111111111111111111111111112".parse()?;
    let new_address: Pubkey = "SysvarC1ock11111111111111111111111111111111".parse()?;

    let client =
        RpcClient::new_with_commitment(env::var("RPC_URL")?, CommitmentConfig::confirmed())
            .with_account_sync(AccountSyncConfig {
                endpoint: env::var("ACCOUNT_SYNC_URL")?,
                automatic_subscriptions: false,
                ..Default::default()
            })?;

    let result = async {
        client.add_accounts([address]).await?;
        let first = client.get_account_with_commitment(&address, client.commitment()).await?;
        println!("First read: {first:?}");

        client.remove_accounts([address]).await?;
        client.add_accounts([address]).await?;
        let second = client.get_account_with_commitment(&address, client.commitment()).await?;
        println!("After re-adding: {second:?}");

        client.replace_accounts([new_address]).await?;
        let third = client
            .get_account_with_commitment(&new_address, client.commitment())
            .await?;
        println!("Replacement: {third:?}");
        println!("Pinned: {:?}", client.account_sync_config().pinned_accounts);
        Ok::<_, Box<dyn Error>>(())
    }
    .await;

    let closed = client.close().await;
    result?;
    closed?;
    Ok(())
}
```

The example saves the result, always awaits `close()`, then returns the first error.

## Production guidance

* **Share one client and close it on shutdown.** Clone it into tasks; clones share state.
* **Size your RPC usage, not just the stream.** The SDK calls RPC for cache misses and to load every tracked account whenever a session opens or the tracked set changes. These requests count against your [rate limits](/using-your-endpoint/rate-and-connection-limits.md) and are billed under your [plan](/account-and-billing/plans-and-billing.md).
* **Keep the tracked set stable.** Every set change reconnects the stream and reloads each tracked account with its own request. For large sets, pin the accounts you need and set `automatic_subscriptions: false`.
* **Watch memory and bandwidth.** The full data of every tracked account sits in process memory, and every update streams in full. Test with realistic account sizes and update rates before you raise `http2_window_size` or `max_decoded_message_size`.
* **Keep up with the stream.** Triton closes a session whose server-side queue fills because the client can't consume updates fast enough. The SDK reconnects. Avoid short-lived function workers for a continuous stream.
* **Monitor freshness.** Track read errors and latency, and alert if context slots of active accounts stop advancing. See the known limitation above.

## Troubleshooting

### I configured pinned accounts, but no stream started

Configuration and pin changes don't start anything. Make a buffered read inside a Tokio runtime.

### Reads work, but the values don't change

The stream is probably not connected, and reads are returning values fetched through RPC. Check that `endpoint` is the Account Sync gRPC URL for the same cluster as the RPC URL, with at most one path segment (your token).

### My read takes longer than `cache_miss_wait`

That setting limits the wait for the buffer, not the whole read. Set the RPC timeout on the wrapped client.

### Removing a pin didn't unsubscribe the account

A temporary subscription is still active, and reads renew it. Set `automatic_subscriptions: false` if the pinned set must be the whole tracked set.

### Reads still work after `close()`

That's expected: after closing, reads use RPC. Account controls return `AccountSyncError::Closed`.

### My read always uses RPC

Only the five methods above use the buffer, and multiple-account reads only for 1 to 100 addresses. Calls through `inner()` and `_with_config` methods always use RPC.

## Public API

```rust
RpcClient::new(url)
RpcClient::new_with_commitment(url, commitment)
RpcClient::new_with_timeout(url, timeout)
RpcClient::new_with_timeout_and_commitment(url, timeout, commitment)
RpcClient::from_inner(solana_client)
plain_client.with_account_sync(config)          -> Result<RpcClient<Configured>, ConfigError>
client.inner()                                  -> &solana_client::nonblocking::rpc_client::RpcClient

configured.get_account(&pubkey).await
configured.get_account_data(&pubkey).await
configured.get_account_with_commitment(&pubkey, commitment).await
configured.get_multiple_accounts(&pubkeys).await
configured.get_multiple_accounts_with_commitment(&pubkeys, commitment).await

configured.account_sync_config()                -> AccountSyncConfig
configured.add_accounts(keys).await             -> Result<(), AccountSyncError>
configured.remove_accounts(keys).await          -> Result<(), AccountSyncError>
configured.replace_accounts(keys).await         -> Result<(), AccountSyncError>
configured.close().await                        -> Result<(), AccountSyncError>
```

**Crate exports:** `RpcClient` (also at `triton_sdk::nonblocking::rpc_client`), `Plain`, `Configured`, `AccountSyncConfig`, `ConfigError`, `AccountSyncError`, `Account`, `Pubkey`, `CommitmentConfig`, `CommitmentLevel`, `ClientError` and the Solana `response` module.

Runnable examples are in the [triton-rust-sdk repository](https://github.com/rpcpool/triton-rust-sdk/tree/main/triton-sdk/examples). The [read timings example](https://github.com/rpcpool/triton-rust-sdk/blob/main/triton-sdk/examples/compare_multiple_accounts_latency.rs) reads 100 accounts and measures read duration, not stream delivery latency.

***

<i class="fa-life-ring">:life-ring:</i> Contact support by clicking the chat icon in your [customer dashboard](https://customers.triton.one)\ <i class="fa-briefcase">:briefcase:</i> Sales questions? [Contact us](https://triton.one/contact)\ <i class="fa-sparkles">:sparkles:</i> AI agent? Read [llms.txt](https://docs.triton.one/llms.txt)\ <i class="fa-rss">:rss:</i> Follow updates: [Blog](https://blog.triton.one) · [X](https://x.com/triton_one) · [YouTube](https://www.youtube.com/@triton_one_ltd) · [Telegram](https://t.me/tritonone) · [GitHub](https://github.com/rpcpool)
