> 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/project-yellowstone/yellowstone-account-sync.md).

# Yellowstone Account Sync

Faster account info queries with lower costs

## Yellowstone Account Sync and the Triton SDKs

This guide explains how to read Solana accounts with the Triton JavaScript/TypeScript SDK (`@triton-one/triton-sdk`) and Rust SDK (`triton-sdk`) through the account-sync service.

You do not need to understand gRPC, WebSocket, or the service internals before you start.

### TL;DR

If your application frequently reads the same Solana accounts, those reads can add RPC latency, request volume, and per-request costs as your application scales.

Account Sync streams account updates in the background and keeps the latest account state in memory. The SDKs expose familiar account-reading methods from `@solana/web3.js` and the asynchronous Solana Rust `RpcClient`.

Supported reads can use account state already held in memory. JSON-RPC still supplies initial values and fallback reads.

| Action                 | Standard RPC reads       | Account Sync                      |
| ---------------------- | ------------------------ | --------------------------------- |
| Account updates        | Fetched on request       | Streamed continuously             |
| RPC processing latency | On every request         | Avoided for cached account reads  |
| Request rate limits    | Apply                    | Not applicable to in-memory reads |
| Usage cost             | RPC requests + bandwidth | Primarily streamed bandwidth      |

### JavaScript benchmark

In our JavaScript `getMultipleAccountsInfo` benchmark, Account Sync reduced median read latency from **12.01 ms to 1.65 ms** and p90 read latency from **14.94 ms to 1.98 ms**.

| Method                | Median latency | p90 latency |
| --------------------- | :------------: | :---------: |
| `@solana/web3.js` RPC |    12.01 ms    |   14.94 ms  |
| Account Sync          |     1.65 ms    |   1.98 ms   |
| **Reduction**         |    **86.3%**   |  **86.8%**  |

See the [JavaScript benchmark](https://github.com/rpcpool/triton-js-sdk/blob/main/examples/account-sync/src/compare_get_multiple_accounts_info_latency.ts) and [Rust timing example](https://github.com/rpcpool/triton-rust-sdk/blob/main/triton-sdk/examples/compare_multiple_accounts_latency.rs). They measure account read duration, not stream delivery latency. A successful read alone does not prove that the result came from the stream.

### What the SDKs do

Solana stores programs and user state in **accounts**. Each account has an address, a balance, an owner, and some data bytes.

Each SDK adds a live account stream:

1. Your application tells the SDK which accounts it needs.
2. The account-sync service sends updates for those accounts.
3. The SDK keeps the newest observed value for each tracked account in memory.
4. Supported account read methods use that local value when possible.
5. The SDK also uses JSON-RPC to load initial state, resolve a missing local value, and keep reads working while the stream is down.

The in-memory copy is called the **buffer** in this guide.

The main benefit is that repeated reads can use data already held by your application instead of making a new JSON-RPC request each time.

### When to use Account Sync

Account Sync is designed for applications that frequently poll the same accounts and benefit from having the latest account state available with low read latency.

#### A good fit

Account Sync is a good fit if your application:

* Frequently reads the same accounts.
* Polls accounts to keep application state up to date.
* Needs fresh account data with low read latency.
* Makes enough account RPC requests that request volume or RPC costs are significant.

In these workloads, Account Sync replaces repeated RPC reads with streamed account updates and serves subsequent reads from memory. This can reduce both read latency and RPC request costs.

#### When standard RPC may be a better fit

Standard RPC calls may be more appropriate if your application:

* Reads account data infrequently.
* Does not require account state to stay continuously up to date.
* Only needs fresh account data in response to occasional user actions or events.

Account Sync continuously receives updates for subscribed accounts, even when your application is not reading them. If reads are infrequent, the bandwidth cost of maintaining the stream may outweigh the savings from avoiding individual RPC requests.

**Rule of thumb:** The more frequently you poll the same accounts, the more Account Sync can reduce repeated RPC work. For occasional reads, fetching the data on demand is typically a better fit.

## SDK and transport support

| SDK and environment                  | WebSocket | gRPC |
| ------------------------------------ | --------- | ---- |
| JavaScript / TypeScript in Node.js   | Yes       | Yes  |
| JavaScript / TypeScript in a browser | Yes       | No   |
| Rust with Tokio                      | No        | Yes  |

## Install

{% tabs %}
{% tab title="JavaScript / TypeScript" %}
With npm:

```bash
npm install @triton-one/triton-sdk
```

With pnpm:

```bash
pnpm add @triton-one/triton-sdk
```

With Yarn:

```bash
yarn add @triton-one/triton-sdk
```

{% endtab %}

{% tab title="Rust" %}
Install the SDK from crates.io and enable Tokio's runtime and macros:

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

The `triton-sdk` crate is a drop in relacement for the `solana-client` crate.
{% endtab %}
{% endtabs %}

### Examples

Runnable examples are available for [JavaScript / TypeScript](https://github.com/rpcpool/triton-js-sdk/tree/main/examples/account-sync) and [Rust](https://github.com/rpcpool/triton-rust-sdk/tree/main/triton-sdk/examples).

## Quickstart with gRPC

Use gRPC for backend applications. Set a JSON-RPC URL and an account-sync stream URL for the same cluster. They may be the same URL if your endpoint serves both APIs.

```bash
export RPC_URL='https://rpc.example.com/<RPC_TOKEN>'
export ACCOUNT_SYNC_URL='https://stream.example.com/<STREAM_TOKEN>'
```

{% tabs %}
{% tab title="JavaScript / TypeScript" %}
Run this example in Node.js:

```ts
import {
  AccountSyncTransports,
  Connection,
  PublicKey,
} from "@triton-one/triton-sdk";

const endpoint = process.env.RPC_URL;
const subscriptionEndpoint = process.env.ACCOUNT_SYNC_URL;

if (!endpoint || !subscriptionEndpoint) {
  throw new Error("Set RPC_URL and ACCOUNT_SYNC_URL");
}

const address = new PublicKey(
  "So11111111111111111111111111111111111111112",
);

const connection = new Connection(endpoint, {
  accountSync: {
    transport: AccountSyncTransports.GRPC,
    subscriptionEndpoint,
    commitment: "confirmed",
    initialAccounts: [address],
  },
});

try {
  const response = await connection.getAccountInfoAndContext(address);

  console.log("Observed at slot:", response.context.slot);
  console.log("Account:", response.value);
} finally {
  await connection.close();
}
```

{% endtab %}

{% tab title="Rust" %}
Save this example as `src/main.rs` and run it with `cargo run`:

```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 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(())
}
```

{% endtab %}
{% endtabs %}

### Browser with WebSocket (JavaScript / TypeScript)

The package automatically selects its browser entry when used by a browser build tool. The browser entry does not load the Node.js gRPC code. Requesting gRPC in a browser throws during `Connection` construction.

```ts
import {
  AccountSyncTransports,
  Connection,
  PublicKey,
} from "@triton-one/triton-sdk";

const address = new PublicKey(
  "So11111111111111111111111111111111111111112",
);

const connection = new Connection(
  "https://api.example.com/<BROWSER_TOKEN>",
  {
    accountSync: {
      transport: AccountSyncTransports.WS,
      initialAccounts: [address],
    },
  },
);

async function showAccount(): Promise<void> {
  const response = await connection.getAccountInfoAndContext(address);
  const output = document.querySelector("#account-output");

  if (output) {
    output.textContent = response.value
      ? `Balance: ${response.value.lamports} lamports at slot ${response.context.slot}`
      : `Account missing at slot ${response.context.slot}`;
  }
}

void showAccount().catch(console.error);

window.addEventListener("pagehide", () => {
  void connection.close();
});
```

## Choose RPC-only or account-sync mode

{% tabs %}
{% tab title="JavaScript / TypeScript" %}

### Native web3.js connections

Omit `accountSync` to get an actual `@solana/web3.js` Connection. Its prototype and methods come from web3.js. The SDK does not create an account-sync core, transport, hydration plugin, or parse cache.

```ts
import { Connection, PublicKey } from "@triton-one/triton-sdk";

const connection = new Connection(
  "https://example.com/token",
  "confirmed",
);

const account = await connection.getAccountInfo(PublicKey.default);
```

An ordinary web3.js config object also selects native mode. Its `fetch`, `fetchMiddleware`, `httpHeaders`, `wsEndpoint`, retry settings, and other options go directly to web3.js. An omitted commitment keeps the web3.js default behavior.

### Account-sync connections

Pass `accountSync: {}` to enable buffered account reads with the default settings. Supply options inside that object to configure the transport and buffer. Node supports WebSocket and gRPC; the browser build supports WebSocket.

```ts
import { Connection, PublicKey } from "@triton-one/triton-sdk";

const connection = new Connection("https://example.com/token", {
  accountSync: {},
});

try {
  const account = await connection.getAccountInfo(PublicKey.default);
} finally {
  await connection.close();
}
```

Account-sync reads default to `confirmed` when neither the constructor nor `accountSync` supplies a commitment. The enabled implementation retains its own hydration and parsing behavior. Its hydration requests do not inherit web3.js fetch or header settings.

### Choosing a mode

| Input                                                       | Connection returned |
| ----------------------------------------------------------- | ------------------- |
| No config, a commitment string, or ordinary web3.js options | Native web3.js      |
| `accountSync: undefined`                                    | Native web3.js      |
| `accountSync: {}` or an options object                      | Account-sync        |

Only account-sync instances provide `addAccounts`, `removeAccounts`, `setAccounts`, `close`, and `getLastTransportError`.
{% endtab %}

{% tab title="Rust" %}
`RpcClient::new` and the other constructors return `RpcClient<Plain>`. Its methods use the underlying asynchronous Solana RPC client. Call `with_account_sync` to get `RpcClient<Configured>` with buffered raw reads and account controls:

```rust
use triton_sdk::{AccountSyncConfig, CommitmentConfig, RpcClient};

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

The stream endpoint is required; it is not derived from the RPC URL. Set both to the same URL if your service supports that. The default account read commitment comes from the underlying Solana client. `RpcClient::new` uses its `finalized` default; this guide sets `confirmed` explicitly.

`RpcClient::from_inner` wraps an existing asynchronous Solana RPC client, including its RPC settings. `client.inner()` gives direct access to that client and bypasses the account-sync buffer. Other Solana client methods are also available through dereferencing.

Only configured clients provide account controls, `account_sync_config()`, and `close()`. Cloned clients share the RPC client and account-sync state. Closing one clone stops account sync for all clones.
{% endtab %}
{% endtabs %}

## How a read works

A supported read can return a buffered value from memory without a new JSON-RPC request. If no suitable value is ready, the SDK uses JSON-RPC and can create a temporary subscription. Both SDKs compare observation slots and stream write order when updating the buffer.

{% tabs %}
{% tab title="JavaScript / TypeScript" %}
**1. An account that is already in the buffer**

If the SDK has the value buffered, it returns that value from memory. It does not start a new JSON-RPC request for that read.

**2. An account that is not in the buffer**

The SDK starts a one-time `getMultipleAccounts` JSON-RPC request. By default, it also adds a stream subscription for the account. The SDK returns the first suitable answer from the buffer or JSON-RPC.

If JSON-RPC confirms that the account is absent, the result is `null`. The SDK stores the slot where that absence was observed.

If neither source can produce a suitable answer before `missTimeoutMs`, the read fails.

**3. Startup**

The SDK opens the stream and asks JSON-RPC for the current value of every tracked account specified in the `initialAccounts` field. This prevents the SDK from waiting for the next account change before it knows the starting state.

Stream updates may arrive while the JSON-RPC starting values are loading. The SDK compares their slots and write order and gives you the newest value.

**4. When the stream disconnects**

The SDK starts JSON-RPC polling for tracked accounts and tries to reconnect in the background. After reconnecting, it sends the complete account set again and gets a fresh JSON-RPC view before it stops polling.

Reads can still succeed during this time. They may be served from a buffered value or a JSON-RPC request.
{% endtab %}

{% tab title="Rust" %}
**Startup:** Configuration and account-set changes do not start a stream by themselves. The first supported read at a commitment starts that commitment's background tasks. After a stream session opens, the SDK fetches initial values for tracked accounts through RPC. Failed initial reads can be retried on `subscription_refresh` ticks.

**Cache miss:** The SDK waits for a complete buffered result while making the normal single-account or multiple-account RPC call. A ready buffered result takes priority. Otherwise, the RPC result can finish the read, including with an error. Successful RPC observations can populate the buffer for tracked accounts.

**Wait limit:** `cache_miss_wait` limits the wait for the buffer. If that wait expires, the read continues waiting for RPC. It is not a deadline for the whole read; configure the underlying RPC client's timeout separately.

**Disconnect or account-set change:** The SDK clears cached entries for the affected commitment and reconnects with the desired account set. Reads use RPC as needed. It does not periodically poll all tracked accounts during a stream outage. After a new session opens, it loads initial values again.
{% endtab %}
{% endtabs %}

## Read one account

{% tabs %}
{% tab title="JavaScript / TypeScript" %}
**Value only**

```ts
const account = await connection.getAccountInfo(address);

if (account) {
  console.log(account.lamports);
}
```

The result is `AccountInfo<Buffer> | null`.

**Account fields**

For an account that exists, the result is same as web3.js response which contains:

| Field        | Meaning                                             |
| ------------ | --------------------------------------------------- |
| `lamports`   | Account balance. One SOL is 1,000,000,000 lamports. |
| `owner`      | `PublicKey` of the program that owns the account.   |
| `executable` | Whether the account contains an executable program. |
| `rentEpoch`  | Epoch when the account will next owe rent.          |
| `data`       | Raw account bytes as a Node.js `Buffer`.            |
| `space`      | Full account data size in bytes at runtime.         |

The current web3.js `AccountInfo` TypeScript type does not declare `space`, but the Triton SDK includes it at runtime to match current RPC responses.
{% endtab %}

{% tab title="Rust" %}

```rust
let account = client.get_account(&address).await?;
println!("Balance: {}", account.lamports);
```

The result is `Account`. A missing account returns an `AccountNotFound` client error. Use the context method when absence is an expected result:

```rust
let response = client
    .get_account_with_commitment(&address, client.commitment())
    .await?;
println!("Observed at slot: {}", response.context.slot);
match response.value {
    Some(account) => println!("Balance: {}", account.lamports),
    None => println!("Account missing"),
}
```

The response contains `Option<Account>` and a context slot. `get_account_data(&address)` returns only the `Vec<u8>` and also fails if the account is missing.

| Field        | Meaning                                                          |
| ------------ | ---------------------------------------------------------------- |
| `lamports`   | Account balance as `u64`. One SOL is 1,000,000,000 lamports.     |
| `owner`      | `Pubkey` of the program that owns the account.                   |
| `executable` | Whether the account contains an executable program.              |
| `rent_epoch` | Rent epoch as `u64`.                                             |
| `data`       | Raw account bytes as `Vec<u8>`. Use `data.len()` for their size. |

Rust's `Account` has no separate `space` field.
{% endtab %}
{% endtabs %}

## Read several accounts

Results stay in input order. Duplicate addresses produce duplicate result positions. A missing account occupies its own position as `null` in JavaScript or `None` in Rust.

{% tabs %}
{% tab title="JavaScript / TypeScript" %}

```ts
const addresses = [addressA, addressB, addressA];

const accounts = await connection.getMultipleAccountsInfo(addresses);

for (const [index, account] of accounts.entries()) {
  console.log(addresses[index].toBase58(), account);
}
```

To receive a shared context:

```ts
const response = await connection.getMultipleAccountsInfoAndContext([
  addressA,
  addressB,
]);

console.log("Shared slot:", response.context.slot);
console.log("Accounts:", response.value);
```

The SDK sends unresolved accounts in one JSON-RPC request when possible. Large background hydration sets are split into groups of 100 accounts.
{% endtab %}

{% tab title="Rust" %}

```rust
let addresses = [address_a, address_b, address_a];
let accounts = client.get_multiple_accounts(&addresses).await?;

for (address, account) in addresses.iter().zip(&accounts) {
    println!("{address}: {account:?}");
}
```

To receive a shared context:

```rust
let response = client
    .get_multiple_accounts_with_commitment(&addresses, client.commitment())
    .await?;
println!("Shared slot: {}", response.context.slot);
println!("Accounts: {:?}", response.value);
```

The buffered path accepts 1–100 addresses per call. Empty input and batches larger than 100 go directly to the underlying RPC method, with its usual validation and errors. Rust does not split those calls into smaller batches. This per-call limit is separate from the number of accounts you can subscribe to.
{% endtab %}
{% endtabs %}

### What the context slot means

An RPC observation uses the bank context slot returned by JSON-RPC. A streamed observation uses the slot of the account update. An unchanged account can therefore have an older slot than the current bank. A buffered context slot is not a full bank-progress marker.

For a buffered multiple-account result, the shared context is the lowest observation slot in that result. It does not mean that all accounts came from the same bank snapshot.

## JavaScript account read options

The following options apply to the JavaScript account-sync methods. Rust's buffered methods return raw accounts and accept no data-slice or minimum-slot options. Rust calls such as `get_account_with_config` use the underlying Solana RPC client directly.

### Read parsed accounts

Raw account data is bytes. Some common Solana programs have known data layouts. A parsed account replaces those bytes with a JavaScript object that is easier to read.

Read one parsed account:

```ts
const response = await connection.getParsedAccountInfo(address, {
  commitment: "confirmed",
});

if (response.value) {
  console.log("Parsed account:", response.value.data.parsed);
}
```

Read several parsed accounts:

```ts
const response = await connection.getMultipleParsedAccounts([
  addressA,
  addressB,
]);

console.log(response.context.slot, response.value);
```

**Supporting accounts and parse failures**

The parser must support the account's owner program. Some parsers also need another account. For example, parsing an SPL token account needs its mint account to calculate display amounts.

The connection automatically fetches a required mint through its own `getAccountInfo` method. It keeps these supporting values in a small cache. The default cache holds up to 256 entries for five minutes. Concurrent parses that need the same uncached mint share one fetch.

In account-sync mode, parse failures and missing supporting data reject the read; they do not return raw fallback data. Use raw account methods when you need bytes. `dataSlice` is intended for raw reads; the parsed methods parse the full buffered account.

### Read part of account data

Use `dataSlice` when you need only a range of raw account bytes:

```ts
const account = await connection.getAccountInfo(address, {
  commitment: "confirmed",
  dataSlice: {
    offset: 8,
    length: 32,
  },
});
```

This means “start at byte 8 and return at most 32 bytes.”

Both `offset` and `length` must be non-negative safe integers. A zero length returns an empty `Buffer`. An offset past the end also returns an empty `Buffer`. A range that reaches past the end stops at the end.

The slice changes only the returned `data`. The runtime `space` field still contains the full size of the account data.

The same option works with single-account and multiple-account methods.

### Require a minimum slot

Use `minContextSlot` when you must not accept an observation older than a known slot:

```ts
const recentSlot = await connection.getSlot("finalized");

const response = await connection.getAccountInfoAndContext(address, {
  commitment: "confirmed",
  minContextSlot: recentSlot,
});

if (response.context.slot < recentSlot) {
  throw new Error("The SDK returned an older observation than requested");
}
```

`minContextSlot` must be a non-negative safe integer.

On a cache miss, the SDK sends this value to Solana JSON-RPC. If the RPC node has not reached the requested slot, it commonly returns error code `-32016`. The SDK also waits for a suitable buffered observation until `missTimeoutMs`.

For unchanged accounts, the last streamed update can be older than the requested slot even when the bank has advanced. See [what the context slot means](#what-the-context-slot-means).

## Choose a commitment

Both SDKs support `processed`, `confirmed`, and `finalized`. Each commitment uses separate account state and its own stream.

{% tabs %}
{% tab title="JavaScript / TypeScript" %}
Other web3.js commitment names, such as `max`, are rejected for account-sync reads.

Set the default for the connection:

```ts
const connection = new Connection(endpoint, {
  accountSync: {
    commitment: "confirmed",
  },
});
```

You can also set the commitment in the connection config while enabling account sync:

```ts
const connection = new Connection(endpoint, {
  commitment: "finalized",
  accountSync: {},
});
```

`accountSync.commitment` wins when both forms are present. With neither form, the default is `confirmed`.

Override it for one read:

```ts
await connection.getAccountInfo(address, "processed");

await connection.getAccountInfo(address, {
  commitment: "finalized",
});
```

A different commitment has separate account state. The SDK therefore opens a separate stream and buffer for it. An unused non-default commitment stream is closed after its pinned and temporary accounts are gone.
{% endtab %}

{% tab title="Rust" %}
Set the default on the RPC client:

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

Override it for a read with a context method:

```rust
let response = client
    .get_account_with_commitment(&address, CommitmentConfig::finalized())
    .await?;
```

`AccountSyncConfig` has no commitment field. All commitment streams share the configured pinned account set. Temporary subscriptions belong to the commitment where the read occurred. A stream with no desired accounts waits for new accounts; its background task remains until the client closes.
{% endtab %}
{% endtabs %}

## Choose which accounts stay subscribed

There are two kinds of tracked accounts.

### Pinned accounts

Pinned accounts stay subscribed until you remove them or close the connection.

Configure pinned accounts and add more later. Duplicate addresses are ignored.

{% tabs %}
{% tab title="JavaScript / TypeScript" %}

```ts
const connection = new Connection(endpoint, {
  accountSync: {
    initialAccounts: [addressA, addressB],
  },
});
```

Pin more accounts later:

```ts
await connection.addAccounts([addressC, addressD]);
```

{% endtab %}

{% tab title="Rust" %}

```rust
let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed())
    .with_account_sync(AccountSyncConfig {
        endpoint: stream_url,
        pinned_accounts: [address_a, address_b].into(),
        ..Default::default()
    })?;

client.add_accounts([address_c, address_d]).await?;
```

`client.account_sync_config().pinned_accounts` returns a snapshot of the current pinned set. Editing that returned snapshot does not change the running client. The first supported read starts stream work.
{% endtab %}
{% endtabs %}

### Temporary accounts

By default, reading an untracked account creates a temporary subscription. Further reads renew it. Both SDKs use a default idle lifetime of 60 seconds.

{% tabs %}
{% tab title="JavaScript / TypeScript" %}
Set the idle lifetime with `dynamicSubscriptionTtlMs`.

Disable this for default-commitment cache misses:

```ts
const connection = new Connection(endpoint, {
  accountSync: {
    autoSubscribeOnMiss: false,
  },
});
```

The read still makes a one-time JSON-RPC request. A read at a commitment other than the connection default still needs its own commitment stream and may track the account there.
{% endtab %}

{% tab title="Rust" %}
Set the idle lifetime with `dynamic_subscription_lifetime`, a `Duration`. Expired subscriptions are removed on `subscription_refresh` ticks. Disable automatic subscriptions at every commitment with:

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

Unpinned reads still use JSON-RPC. They do not retain account state in the buffer when automatic subscriptions are disabled.
{% endtab %}
{% endtabs %}

### Remove or replace accounts

{% tabs %}
{% tab title="JavaScript / TypeScript" %}

```ts
await connection.removeAccounts([addressA]);
```

This removes pinned or temporary ownership and clears the cached account state immediately. A read already in progress may finish through its one-time JSON-RPC request.

A later read can create a new temporary subscription. If the account should remain subscribed, call `addAccounts` before reading it again.

Removing an address that is not tracked does not fail. It also clears any supporting parsed-account cache entry for that address.

**Replace the full account set**

```ts
await connection.setAccounts([addressB, addressC]);
```

This makes the given list the complete pinned set for that commitment. It also clears all temporary accounts for that commitment.

**Use a specific commitment**

All three account control methods accept an optional commitment:

```ts
await connection.addAccounts([addressA], "finalized");

await connection.removeAccounts([addressA], "finalized");

await connection.setAccounts([addressB], "finalized");
```

Without this argument, they use the configured account-sync commitment you set during `Connection` creation.
{% endtab %}

{% tab title="Rust" %}

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

`remove_accounts` removes pins. `replace_accounts` replaces the complete pinned set. These methods have no commitment argument: changes apply to every existing commitment stream and to streams started later.

Neither method clears an existing temporary subscription. That subscription remains until its idle lifetime expires, and reads can renew it. Removing an unknown pin succeeds. To control subscriptions only through the pinned set, set `automatic_subscriptions: false` when creating the client.

A changed desired account set restarts the affected stream and clears its cached entries. A read already in progress can still finish through RPC. A rapid remove-and-re-add can accept an earlier RPC result if it passes the cache's slot checks.
{% endtab %}
{% endtabs %}

## Complete lifecycle example

These examples add, read, remove, re-add, and replace pinned accounts, then close the client. Automatic subscriptions are disabled to keep the example's account set controlled by explicit calls.

{% tabs %}
{% tab title="JavaScript / TypeScript" %}

```ts
import {
  AccountSyncTransports,
  Connection,
  PublicKey,
} from "@triton-one/triton-sdk";

const endpoint = process.env.RPC_URL;
const subscriptionEndpoint = process.env.ACCOUNT_SYNC_URL;

if (!endpoint || !subscriptionEndpoint) {
  throw new Error("Set RPC_URL and ACCOUNT_SYNC_URL");
}

const address = new PublicKey(
  "So11111111111111111111111111111111111111112",
);

const newAddress = new PublicKey(
  "SysvarC1ock11111111111111111111111111111111",
);

const connection = new Connection(endpoint, {
  accountSync: {
    transport: AccountSyncTransports.GRPC,
    subscriptionEndpoint,
    commitment: "confirmed",
    autoSubscribeOnMiss: false,
  },
});

try {
  await connection.addAccounts([address]);

  const first = await connection.getAccountInfoAndContext(address);
  console.log("First read:", first);

  await connection.removeAccounts([address]);

  // Re-add it before the next read so it remains pinned.
  await connection.addAccounts([address]);

  const second = await connection.getAccountInfoAndContext(address);
  console.log("Read after re-add:", second);

  await connection.setAccounts([newAddress]);

  const third = await connection.getAccountInfoAndContext(newAddress);
  console.log("New account:", third);
} finally {
  await connection.close();
}
```

{% endtab %}

{% tab title="Rust" %}

```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!("Read after re-add: {second:?}");

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

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

{% endtab %}
{% endtabs %}

Neither SDK sets a fixed subscription count limit. Your endpoint, memory, CPU, network, and the account-sync service can still have practical limits. Test the size you plan to use.

## Configuration reference

{% tabs %}
{% tab title="JavaScript / TypeScript" %}
All account-sync settings belong under `accountSync`:

```ts
const connection = new Connection(endpoint, {
  accountSync: {
    // Settings go here.
  },
});
```

**Common settings**

| Setting                    | Type                                        | Default                 | Meaning                                                                                    |
| -------------------------- | ------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------ |
| `transport`                | `"ws" \| "grpc"`                            | `"ws"`                  | Stream type. Browsers allow only `"ws"`.                                                   |
| `subscriptionEndpoint`     | `string`                                    | Derived from `endpoint` | Optional, overrides only the account-sync stream endpoint. JSON-RPC still uses `endpoint`. |
| `commitment`               | `"processed" \| "confirmed" \| "finalized"` | `"confirmed"`           | Default commitment for buffered reads.                                                     |
| `initialAccounts`          | Array of strings or `PublicKey` values      | `[]`                    | Accounts pinned from startup.                                                              |
| `autoSubscribeOnMiss`      | `boolean`                                   | `true`                  | Temporarily subscribe when a default-commitment read misses the buffer.                    |
| `missTimeoutMs`            | positive safe integer                       | `5000`                  | Longest wait for a suitable buffered or RPC observation.                                   |
| `rpcPollIntervalMs`        | positive safe integer                       | `1000`                  | Delay between RPC refreshes while the stream is unavailable.                               |
| `reconnectInitialDelayMs`  | positive safe integer                       | `100`                   | First wait before reconnecting a failed stream.                                            |
| `reconnectMaxDelayMs`      | positive safe integer                       | `5000`                  | Largest reconnect wait. Must be at least the initial delay.                                |
| `connectTimeoutMs`         | positive safe integer                       | `10000`                 | Longest wait for one WebSocket connection attempt. Not used by gRPC.                       |
| `closeTimeoutMs`           | positive safe integer                       | `5000`                  | Longest wait for account-sync shutdown.                                                    |
| `dynamicSubscriptionTtlMs` | positive safe integer                       | `60000`                 | Idle lifetime of an account subscription created by a read for an unpinned account.        |

A **safe integer** is a whole JavaScript number that can be represented exactly. These time values are milliseconds. Zero, negative values, fractions, `NaN`, and numbers outside the safe integer range are rejected.

### **gRPC settings**

These settings are valid only with the gRPC transport:

```ts
const connection = new Connection(endpoint, {
  accountSync: {
    transport: AccountSyncTransports.GRPC,
    grpc: {
      flowControlWindowBytes: 16 * 1024 * 1024,
      maxReceiveMessageLengthBytes: 16 * 1024 * 1024,
      keepAliveIntervalMs: 30_000,
      keepAliveTimeoutMs: 10_000,
      keepAlivePermitWithoutCalls: true,
    },
  },
});
```

| Setting                        | Default    | Meaning                                        |
| ------------------------------ | ---------- | ---------------------------------------------- |
| `flowControlWindowBytes`       | `16777216` | Fixed HTTP/2 receive window used by `grpc-js`. |
| `maxReceiveMessageLengthBytes` | `16777216` | Largest gRPC message the client accepts.       |
| `keepAliveIntervalMs`          | `30000`    | Time between gRPC keepalive pings.             |
| `keepAliveTimeoutMs`           | `10000`    | Time allowed for a keepalive response.         |
| `keepAlivePermitWithoutCalls`  | `true`     | Allow keepalive pings while no call is active. |

Byte and time values must be positive safe integers. The keepalive flag must be a boolean.

Do not add `grpc` settings while `transport` is WebSocket. The Node.js constructor rejects that combination. The browser build rejects all gRPC settings.

**Separate RPC and stream hosts**

Use `subscriptionEndpoint` if JSON-RPC and account sync have different hosts:

```ts
const connection = new Connection("https://rpc.example.com/<RPC_TOKEN>", {
  accountSync: {
    transport: AccountSyncTransports.GRPC,
    subscriptionEndpoint: "https://stream.example.com/<STREAM_TOKEN>",
  },
});
```

Initial account loading and fallback polling still use the RPC endpoint.
{% endtab %}

{% tab title="Rust" %}
Pass `AccountSyncConfig` to `with_account_sync`. Duration settings use `std::time::Duration`, not integer milliseconds:

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

let config = AccountSyncConfig {
    endpoint: stream_url,
    pinned_accounts: [address].into(),
    cache_miss_wait: Duration::from_secs(5),
    dynamic_subscription_lifetime: Duration::from_secs(60),
    ..Default::default()
};
let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed())
    .with_account_sync(config)?;
```

| Setting                         | Type              | Default             | Meaning                                                                                                                                |
| ------------------------------- | ----------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `endpoint`                      | `String`          | Empty; must be set  | Account-sync gRPC URL. RPC uses the URL on the underlying client.                                                                      |
| `pinned_accounts`               | `HashSet<Pubkey>` | Empty               | Accounts pinned across commitment streams.                                                                                             |
| `automatic_subscriptions`       | `bool`            | `true`              | Track unpinned accounts temporarily when read.                                                                                         |
| `cache_miss_wait`               | `Duration`        | 5 seconds           | Wait for a buffered result before relying on RPC. Not a whole-read deadline.                                                           |
| `subscription_refresh`          | `Duration`        | 1 second            | Check temporary subscription expiry and retry missing initial values while a stream session is active. Not an outage polling interval. |
| `reconnect_min_delay`           | `Duration`        | 100 milliseconds    | Initial reconnect delay.                                                                                                               |
| `reconnect_max_delay`           | `Duration`        | 5 seconds           | Maximum reconnect delay; must be at least the minimum.                                                                                 |
| `connect_timeout`               | `Duration`        | 10 seconds          | Timeout for establishing a gRPC transport connection.                                                                                  |
| `close_timeout`                 | `Duration`        | 5 seconds           | Maximum wait for account-sync background tasks to stop.                                                                                |
| `dynamic_subscription_lifetime` | `Duration`        | 60 seconds          | Idle lifetime for temporary subscriptions.                                                                                             |
| `http2_window_size`             | `u32`             | `16777216` (16 MiB) | Initial HTTP/2 connection and stream receive windows.                                                                                  |
| `max_decoded_message_size`      | `usize`           | `16777216` (16 MiB) | Largest decoded gRPC message accepted.                                                                                                 |
| `keepalive_interval`            | `Duration`        | 30 seconds          | Interval between HTTP/2 keepalive pings.                                                                                               |
| `keepalive_timeout`             | `Duration`        | 10 seconds          | Time allowed for a keepalive response.                                                                                                 |
| `keepalive_while_idle`          | `bool`            | `true`              | Allow keepalive pings without an active stream.                                                                                        |

All durations and byte limits must be nonzero. `with_account_sync` validates the configuration and returns `ConfigError` on invalid input. There is no transport selector or separate `grpc` settings object. The Rust client accepts gzip and Zstd stream compression.

RPC commitment and request timeout belong to the underlying client. For example, use `RpcClient::new_with_timeout_and_commitment` before `with_account_sync` to set both. `account_sync_config()` returns an owned snapshot of the configuration, including the current pins.
{% endtab %}
{% endtabs %}

## Errors and recovery

{% tabs %}
{% tab title="JavaScript / TypeScript" %}
**Missing account versus failed read**

These cases are different:

* `null`: JSON-RPC successfully observed that the account did not exist.
* rejected promise: the SDK could not complete the read, the input was invalid, or JSON-RPC returned an error.

### **Read timeout**

Context and multiple-account methods can reject with `AccountSyncReadTimeoutError`:

```ts
import {
  AccountSyncReadTimeoutError,
  Connection,
  PublicKey,
} from "@triton-one/triton-sdk";

try {
  const response = await connection.getAccountInfoAndContext(address);
  console.log(response);
} catch (error) {
  if (error instanceof AccountSyncReadTimeoutError) {
    console.error("Account:", error.accountId);
    console.error("Commitment:", error.commitment);
    console.error("Waited milliseconds:", error.timeoutMs);
    console.error("Minimum slot:", error.minContextSlot ?? "not set");
  } else {
    throw error;
  }
}
```

`getAccountInfo` follows web3.js error wording and wraps failures in a new `Error` that includes the account address. Because of that wrapping, catch `AccountSyncReadTimeoutError` directly only when using a context or multiple-account method.

### **JSON-RPC errors**

Context and multiple-account methods keep web3.js `SolanaJSONRPCError` instances. For example, a node that has not reached `minContextSlot` commonly returns code `-32016`.

```ts
import { SolanaJSONRPCError } from "@triton-one/triton-sdk";

try {
  await connection.getAccountInfoAndContext(address, {
    minContextSlot: requiredSlot,
  });
} catch (error) {
  if (error instanceof SolanaJSONRPCError && error.code === -32016) {
    console.error("RPC has not reached the requested minimum slot");
  } else {
    throw error;
  }
}
```

Again, `getAccountInfo` wraps the original failure, so it does not return the original `SolanaJSONRPCError` instance to your catch block.

### **Background errors**

Stream, polling, hydration, and reconnect work happens in the background. Read the newest saved background error with:

```ts
const error = connection.getLastTransportError();

if (error) {
  console.warn("Account sync saw a background error:", error);
}
```

Despite its method name, this can contain more than a transport error. It can also contain an RPC polling, starting-state, or recovery error.

The SDK keeps the last error even after recovery. A non-null result does not by itself prove that the connection is currently broken. Use it for logs and health details, not as the only reason to stop serving reads.

### **Connection and shutdown timeouts**

A WebSocket attempt that exceeds `connectTimeoutMs` is reported as an error named `AccountSyncConnectTimeoutError`. Polling and reconnect work can continue.

If shutdown exceeds `closeTimeoutMs`, `close()` rejects with an error named `AccountSyncCloseTimeoutError`. The connection still remains closed.

After `close()`, reads and most account-set changes reject with `account-sync connection is closed`. Calling `close()` again is safe and shares the same shutdown result.
{% endtab %}

{% tab title="Rust" %}
**Missing account versus failed read**

`get_account_with_commitment` returns `Ok(Response { value: None, .. })` when the account is absent. Multiple-account results contain `None` at missing positions. `get_account` and `get_account_data` instead return an `AccountNotFound` client error. RPC failures remain errors, not absent accounts.

**Error types**

| Operation                    | Error type         | Meaning                                                                   |
| ---------------------------- | ------------------ | ------------------------------------------------------------------------- |
| `with_account_sync`          | `ConfigError`      | Invalid endpoint, duration, byte limit, or reconnect range.               |
| Supported account reads      | `ClientError`      | Solana RPC client error, including missing accounts for value-only reads. |
| Account controls and `close` | `AccountSyncError` | Account-sync lifecycle or background-task failure.                        |

Stream failures trigger reconnection and reads can use RPC. There is no public last-transport-error method. A successful account read does not prove that the stream is connected.

**Read wait and shutdown**

Rust has no `AccountSyncReadTimeoutError`. After `cache_miss_wait` expires, the read waits for the RPC result under the RPC client's timeout. An RPC error can also finish the read before the buffer wait expires.

`close().await` cancels account-sync work and waits for its background tasks. It returns `AccountSyncError::CloseTimeout` if that wait exceeds `close_timeout`. Subsequent `close()` calls return successfully. Account controls then return `AccountSyncError::Closed`, but account reads continue through the underlying RPC client. This applies to all clones of the configured client.
{% endtab %}
{% endtabs %}

## Production guidance

**Always close unused connections**

Each client can own streams, timers, retries, and pending reads. Use `try`/`finally` in JavaScript. In Rust, save the operation result, await `close()`, then propagate errors, as shown in the lifecycle example. In long-running applications, reuse a client instead of making one per read.

**Plan for both the stream and JSON-RPC**

JSON-RPC is not only an emergency fallback. It supplies starting state and resolves new account reads. Make sure the endpoint allows the expected RPC load, especially when adding many accounts at once.

**Watch memory and network use**

The full latest data for tracked accounts is kept in application memory. Large accounts and large account sets increase memory, RPC, and stream traffic. There is no client-side fixed count limit.

Measure with realistic account sizes and update rates. Do not raise gRPC window or message limits without checking memory use.

**Large gRPC streams**

For large streams, current project guidance is:

* use a host with at least 5 Gbps of internet capacity;
* keep round-trip time to the Triton endpoint at or below 50 ms;
* use the default fixed 16 MiB window settings as a starting point;
* avoid short-lived function workers for a continuous stream; and
* test the host and connection under expected load before production use.

The JavaScript `grpc-js` transport uses a fixed HTTP/2 flow-control window and does not support Zstd stream compression. The Rust SDK sets initial connection and stream windows through `http2_window_size` and accepts gzip and Zstd compression.

**Delivery limits**

The backend Triton service closes a client session if that client cannot consume updates and its server-side queue fills. Both SDKs reconnect and can serve reads through RPC. JavaScript also polls tracked accounts during the outage.

**Observe errors without causing false alarms**

Record read errors, successful read rates, response times, and application health signals. JavaScript also exposes `getLastTransportError()` for background errors. Rust has no equivalent public accessor. Successful reads can come from RPC, so they do not establish stream health.

## Troubleshooting

{% tabs %}
{% tab title="JavaScript / TypeScript" %}
**My result is `null`**

JSON-RPC confirmed that the account was missing at the returned context slot. Check the public key and selected commitment. A timeout or network failure is not returned as `null`.

**My read timed out**

The stream and JSON-RPC did not provide a suitable observation within `missTimeoutMs`. Check the endpoint, token, account-sync access, RPC health, and `minContextSlot`. Increase the timeout only when the expected operation can reasonably need more time.

**The browser says gRPC is unsupported**

Use `AccountSyncTransports.WS`. The JavaScript SDK supports gRPC only in Node.js.

**The WebSocket does not connect**

Check that:

* the endpoint starts with `http`, `https`, `ws`, or `wss` as appropriate;
* the token is present when required;
* the provider exposes the account-sync WebSocket service;
* a proxy allows WebSocket upgrades; and
* browser origin rules allow the request.

Call `getLastTransportError()` for the latest saved background error. The SDK may still be serving reads through JSON-RPC polling.

**`getLastTransportError()` still returns an error after recovery**

This is expected. The SDK keeps the latest background error and does not clear it after recovery.

**My Node.js process does not exit**

Call and await `connection.close()`. A connection owns background resources that can keep work active.

**My minimum slot has not been reached**

The RPC node may return code `-32016`, or the SDK may time out while waiting. Use a reachable slot, a suitable commitment, and enough `missTimeoutMs` for the cluster to progress. Remember that cached stream state uses the account update slot.

**Removing an account did not keep it removed**

A later read can create a new temporary subscription. If you want the account to remain absent from the tracked set, do not read it through this connection, or set `autoSubscribeOnMiss: false` for default-commitment reads.
{% endtab %}

{% tab title="Rust" %}
**I configured pinned accounts, but no stream started**

Configuration and pin changes do not start background tasks. Make a supported account read inside a Tokio runtime. That starts the stream work for its commitment.

**Reads work even though the stream is unavailable**

The client can use RPC. Check that `AccountSyncConfig.endpoint` selects the account-sync service on the same cluster as the RPC URL. Use an HTTP or HTTPS URL with at most one token path segment. There is no public last-transport-error accessor.

**My read takes longer than `cache_miss_wait`**

This setting limits waiting for the buffer, not the whole read. Set the underlying RPC timeout with `new_with_timeout_and_commitment` or an existing client passed to `from_inner`.

**Removing a pin did not unsubscribe the account**

An existing temporary subscription remains active until it expires, and reads renew it. `replace_accounts` also preserves temporary subscriptions. Disable automatic subscriptions at construction if you need the pinned set to be the complete subscription set.

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

Closing stops account sync for all client clones. Account reads then use RPC. Account controls fail with `AccountSyncError::Closed`.

**My configured read still uses RPC**

Only the five methods in the Rust public API summary below use the buffer. Other methods, including `get_account_with_config`, go to the underlying Solana client. Multiple-account calls with more than 100 keys also bypass the buffer.
{% endtab %}
{% endtabs %}

## Advanced account encoding (JavaScript / TypeScript)

Most users can use `getAccountInfo` and `getParsedAccountInfo` and skip this section. These helpers are useful when you already have raw account values and need Solana RPC-style encoding.

Encoding runs in a bundled WebAssembly (WASM) module that works in Node.js and browsers. The module loads when an encoding helper first needs it. All callers share the same loading operation.

The encoding helpers do not open network connections or fetch supporting accounts.

### Supported encodings

| Encoding      | Output                                                                       |
| ------------- | ---------------------------------------------------------------------------- |
| `binary`      | A base58 string. This is the legacy encoding name.                           |
| `base58`      | A `[data, "base58"]` tuple.                                                  |
| `base64`      | A `[data, "base64"]` tuple.                                                  |
| `base64+zstd` | A tuple containing base64-encoded, zstd-compressed data and `"base64+zstd"`. |
| `jsonParsed`  | A `ParsedAccountData` object, or an error if parsing fails.                  |

### Convert raw account data

```ts
import { convertAccountData } from "@triton-one/triton-sdk";

const base58Data = await convertAccountData(
  base64Data,
  "base64",
  "base58",
);
```

`convertAccountData` converts between raw encodings. It does not accept `jsonParsed` as either the source or destination encoding.

### Encode a web3.js account

```ts
import {
  accountInfoToEncodingInput,
  encodeAccount,
} from "@triton-one/triton-sdk";

const accountInfo = await connection.getAccountInfo(address);

if (accountInfo) {
  const encoded = await encodeAccount(
    accountInfoToEncodingInput(address, accountInfo),
    "base64",
  );

  console.log(encoded.data);
}
```

`encodeAccount` returns a JSON-compatible `UiAccount`. Its fields are `lamports`, `data`, `owner`, `executable`, `rentEpoch`, and optional `space`.

An `AccountEncodingInput` accepts:

* `pubkey`: a string or `PublicKey`.
* `owner`: a string or `PublicKey`.
* `lamports`: an unsigned 64-bit value as a number, bigint, or decimal string.
* `executable`: a boolean.
* `rentEpoch`: an unsigned 64-bit value as a number, bigint, or decimal string.
* `data`: a `Uint8Array`, `Buffer`, or base64 string.

Unsafe numeric inputs, negative values, values outside the unsigned 64-bit range, invalid addresses, and invalid base64 strings are rejected.

For raw encodings, the optional `dataSlice` selects a byte range before encoding. The `space` field still reports the full account data size. Slicing does not apply to `jsonParsed`.

### Supply parse context

Some account parsers need supporting data. For example, parsing an SPL token account requires its mint account data.

Load that data before calling the helper and supply it through `parseContext`:

```ts
import { parseJsonParsed } from "@triton-one/triton-sdk";

const mintAccount = await connection.getAccountInfo(mintAddress);

if (!mintAccount) {
  throw new Error("Mint account does not exist");
}

const parsed = await parseJsonParsed(tokenAccountInput, {
  parseContext: {
    splTokenMint: {
      pubkey: mintAddress,
      data: mintAccount.data,
    },
    unixTimestamp: Math.floor(Date.now() / 1000),
  },
});
```

`parseJsonParsed` parses once with the supplied context. It returns `ParsedAccountData` or rejects with an error. It does not fetch missing data, retry parsing, or return raw data after a failure.

`encodeAccount(input, "jsonParsed", options)` accepts the same parse context and follows the same rules. It returns a full `UiAccount` containing the parsed data.

`unixTimestamp` supplies the time for parsers whose output depends on it. A top-level `options.unixTimestamp` overrides `parseContext.unixTimestamp`.

Account-sync connection methods handle supporting reads separately. They can load a missing mint and retry parsing once. A parse or context fetch failure rejects the read. For `getMultipleParsedAccounts`, one failure rejects the entire call; missing accounts return `null`.

Without `accountSync`, `Connection` uses the upstream web3.js behavior.

### Encoding error classes

| Error                      | Meaning                                                                                                                 |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `UnsupportedEncodingError` | The requested encoding or conversion is unsupported.                                                                    |
| `InvalidAccountDataError`  | An account field, encoded value, or option is invalid.                                                                  |
| `MissingParseContextError` | Parsing requires supporting data that was not supplied.                                                                 |
| `WasmParserError`          | The account parser failed, including for unsupported programs or malformed account data, or WASM returned invalid JSON. |
| `ContextFetchError`        | An account-sync connection could not load supporting parse data.                                                        |

These errors can include the account address, owner, encoding, missing account, context kind, or original cause when that information is available.

### Lower-level helpers

* `loadAccountEncodingWasm()` loads and returns the bundled encoder.
* `bufferedAccountToEncodingInput()` converts an internal buffered state. Most application code will not have this internal type.
* `accountInfoToEncodingInput()` converts a raw web3.js account result.
* `toWeb3JsParsedAccountInfo()` converts a JSON-compatible parsed `UiAccount` into the web3.js account shape. It rejects raw encoded data.

## Compatibility and migration

{% tabs %}
{% tab title="JavaScript / TypeScript" %}
Without `accountSync`, the SDK returns a native web3.js `Connection`. With account sync enabled, it returns an implementation that extends the web3.js `Connection` class. The package also re-exports web3.js values and types.

These methods are replaced with account-sync-backed versions:

* `getAccountInfo`
* `getAccountInfoAndContext`
* `getParsedAccountInfo`
* `getMultipleAccountsInfo`
* `getMultipleAccountsInfoAndContext`
* `getMultipleParsedAccounts`

Other inherited methods, such as `getSlot`, continue to use JSON-RPC normally.

This often makes migration small:

```ts
// Before

import { Connection, PublicKey } from "@solana/web3.js";

// After

import { Connection, PublicKey } from "@triton-one/triton-sdk";
```

Then add `accountSync` configuration and call `close()` when you are finished.

The six method signatures and normal account result shapes match web3.js. The behaviour differs in important ways described in this guide: local buffering, temporary subscriptions, fallback polling, and commitment-specific streams.
{% endtab %}

{% tab title="Rust" %}
The SDK wraps `solana_client::nonblocking::rpc_client::RpcClient`. Use `triton_sdk::RpcClient` and enable account sync with `with_account_sync`. Use the SDK's re-exported `Pubkey`, `Account`, and `CommitmentConfig` to match its Solana dependency versions.

To retain an existing Solana client's RPC configuration, wrap it with `RpcClient::from_inner(existing_client)` before enabling account sync. This requires a compatible Solana client version.

The five raw account methods listed below use the buffer when possible. Other Solana methods remain available through dereferencing and use RPC, including configured and parsed RPC reads. Calling `client.inner()` explicitly bypasses buffered reads as well.

`with_account_sync` returns `RpcClient<Configured>`, so update explicit type annotations that previously used plain `RpcClient`. The wrapper is not the same concrete type as Solana's client. It does not provide a blocking account-sync client or the JavaScript encoding helpers.
{% endtab %}
{% endtabs %}

## Public API summary

{% tabs %}
{% tab title="JavaScript / TypeScript" %}
**Main values and classes**

* `Connection`: web3.js-compatible connection with buffered account reads.
* `AccountSyncTransports`: contains `WS` and `GRPC` transport choices.
* `PublicKey` and other web3.js exports: re-exported from `@solana/web3.js`.
* `AccountSyncReadTimeoutError`: typed timeout from context and multiple-account reads.

**Connection methods added or changed**

```
new Connection(endpoint, commitmentOrConfig?)

connection.getAccountInfo(publicKey, commitmentOrConfig?)

connection.getAccountInfoAndContext(publicKey, commitmentOrConfig?)

connection.getParsedAccountInfo(publicKey, commitmentOrConfig?)

connection.getMultipleAccountsInfo(publicKeys, commitmentOrConfig?)

connection.getMultipleAccountsInfoAndContext(publicKeys, commitmentOrConfig?)

connection.getMultipleParsedAccounts(publicKeys, config?)

connection.addAccounts(accountIds, commitment?)

connection.removeAccounts(accountIds, commitment?)

connection.setAccounts(accountIds, commitment?)

connection.getLastTransportError()

connection.close()
```

**Encoding values and classes**

* `AccountParseContextCache`
* `ContextFetchError`
* `InvalidAccountDataError`
* `MissingParseContextError`
* `UnsupportedEncodingError`
* `WasmParserError`
* `accountInfoToEncodingInput`
* `bufferedAccountToEncodingInput`
* `convertAccountData`
* `encodeAccount`
* `isBase64ZstdEncodingSupported`
* `loadAccountEncodingWasm`
* `parseJsonParsed`
* `toWeb3JsParsedAccountInfo`

**Exported account-sync types**

Both builds export:

* `AccountSyncCommitment`
* `AccountSyncOptions`
* `AccountSyncReadTimeoutErrorOptions`

The Node.js build also exports:

* `GrpcTransportOptions`
* `NodeAccountSyncConnectionConfig`
* `NodeSubscriptionTransport`

The browser build instead exports:

* `BrowserAccountSyncConnectionConfig`
* `BrowserSubscriptionTransport`

**Exported encoding types**

* `AccountDataEncoding`
* `AccountEncodingInput`
* `AccountEncodingOptions`
* `AccountParseContext`
* `AccountParseContextAccount`
* `AccountParseContextCacheOptions`
* `AccountParseContextFetcher`
* `AccountParseContextMint`
* `ConvertAccountDataOptions`
* `EncodedAccountData`
* `UiAccount`
  {% endtab %}

{% tab title="Rust" %}
**Main exports**

* `RpcClient`, also available at `triton_sdk::nonblocking::rpc_client::RpcClient`.
* `Plain` and `Configured`: client mode types.
* `AccountSyncConfig`, `ConfigError`, and `AccountSyncError`.
* `Account`, `Pubkey`, `CommitmentConfig`, `CommitmentLevel`, `ClientError`, and the Solana `response` module.

**Buffered methods on `RpcClient<Configured>`**

| Method                                                        | Successful result                 |
| ------------------------------------------------------------- | --------------------------------- |
| `get_account(&pubkey)`                                        | `Account`; absence is an error.   |
| `get_account_data(&pubkey)`                                   | `Vec<u8>`; absence is an error.   |
| `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 five are async and return a `Result` with the Solana `ClientError` type. Multiple-account methods use the buffer only for 1–100 input addresses.

**Constructors and account-sync controls**

```
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(existing_client)
plain_client.with_account_sync(config)
client.inner()
configured_client.account_sync_config()
configured_client.add_accounts(keys).await
configured_client.remove_accounts(keys).await
configured_client.replace_accounts(keys).await
configured_client.close().await
```

Account controls accept an iterator of `Pubkey` values and return `Result<(), AccountSyncError>`. They have no commitment argument. Configuration inspection is synchronous and returns a snapshot.
{% endtab %}
{% endtabs %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.triton.one/project-yellowstone/yellowstone-account-sync.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
