Solana accountSubscribe accepts an optional encoding parameter with three documented values: base64, base64+zstd, and jsonParsed. The notification envelope (context and value) is identical across encodings; only value.data changes shape. base64 returns a two-element [data, encoding] tuple containing the account's raw bytes, while jsonParsed returns an object with program and parsed fields produced by the owning program's parser. jsonParsed is not universal: accounts owned by programs the runtime has no parser for return parsed: null, so clients that only implement the jsonParsed branch silently drop those updates. Production indexers typically request base64 for stability and use jsonParsed for inspection, because parser output can change with node versions. This article shows runnable Node.js examples for both encodings, a results table to verify against your own endpoint, and troubleshooting for shape mismatches, compressed payloads, and subscription ID correlation.
The accountSubscribe Request and Its Encoding Parameter
accountSubscribe is a Solana WebSocket method that registers a subscription for changes to a single account identified by its base58 public key. The request is a standard JSON-RPC 2.0 call over a WebSocket connection, carrying an id, the method name, and a params array. The Solana documentation for accountSubscribe specifies the params as the account pubkey, an optional commitment level, and an optional encoding value. The documented encoding values are base64, base64+zstd, and jsonParsed.
The encoding parameter controls only how the account's data field is represented in notifications. It does not change which account is watched, when notifications fire, or the surrounding notification envelope. If you omit encoding, the node applies its default, which is base64 in the documented behavior. Because the choice is per-subscription, you can open two subscriptions to the same account with different encodings and compare them side by side, which is the fastest way to internalize the difference.
The subscription is confirmed by a response containing the subscription id, and subsequent notifications arrive as JSON-RPC notifications with method accountNotification. That id is what you use to correlate notifications with the subscription that produced them, a topic covered in JSON-RPC notification ID correlation and batch ordering.
- Params: account pubkey (base58 string), optional commitment, optional encoding.
- Documented encodings: base64, base64+zstd, jsonParsed.
- Notification method: accountNotification, with a subscription id for correlation.
What base64 Returns: Raw Bytes in a Tuple
With encoding base64, the notification's value.data is a two-element array: the first element is the account's raw bytes encoded as a base64 string, and the second element is the literal string "base64". This tuple form is documented in the Solana RPC JSON structures page, which describes account data representations. The bytes are lossless: whatever the program wrote to the account is exactly what you receive, with no interpretation applied by the node.
Because the node does no parsing, base64 is the stable choice for production indexers. Your client owns the deserialization step, which means you must keep a Borsh layout in sync with the on-chain program. That is real work, but it is work you control. A node upgrade cannot change the meaning of your bytes, because the node never assigned meaning to them in the first place.
The tuple shape is easy to mishandle. A common bug is treating value.data as a string and calling Buffer.from(data, 'base64') directly, which fails because data is an array. The correct access is data[0] for the base64 string and data[1] for the encoding label. Branching on data[1] rather than assuming base64 is a cheap defensive habit if your code path might also receive jsonParsed.
- value.data is [base64String, "base64"].
- Lossless: no node-side interpretation.
- Client must deserialize with a layout kept in sync with the program.
What base64+zstd Returns and When Compression Helps
base64+zstd applies zstd compression to the same raw bytes before base64-encoding them, so the payload is smaller on the wire. The documented encoding label in the tuple is "base64+zstd". This is most useful for large accounts where bandwidth or message size is a constraint, such as accounts holding sizable state or arrays.
The tradeoff is that your client must decompress before it can deserialize. Node.js does not ship a zstd decompressor in the standard library, so you will need a dependency or a native binding. That adds a moving part to your pipeline. For small accounts, the compression overhead may not be worth the added dependency, and the reader should measure rather than assume.
Compression changes the wire representation, not the semantics. Once decompressed, the bytes are identical to what base64 would have delivered. If you are debugging a shape mismatch, confirm which encoding label you actually received before writing decompression logic, because a base64 subscription will never produce a zstd payload.
- Same bytes as base64, compressed with zstd, then base64-encoded.
- Tuple label is "base64+zstd".
- Requires a zstd decompressor in the client; measure benefit per account size.
What jsonParsed Returns: Parser Output and Its Limits
With encoding jsonParsed, the node runs the account through the parser associated with its owning program and returns value.data as an object with program and parsed fields. The parsed field contains a program-specific structure, such as token account fields for the Token program. This is the most convenient representation when a parser exists, because it removes the need for a client-side Borsh layout.
The critical limitation is coverage. The runtime only parses accounts owned by programs it knows, such as the System, Token, Token-2022, and Stake programs and similar built-ins. For an account owned by a custom program, the node has no parser, and the documented behavior is that parsed is null. The raw bytes remain the only truthful representation in that case.
This is the failure mode that catches teams off guard. A client that implements only the jsonParsed branch will receive notifications for custom-program accounts, see parsed: null, and either throw or silently skip the update. The subscription is working; the client is discarding data. The fix is to branch on the shape and fall back to raw bytes, which is why many teams request base64 for anything they intend to index.
- value.data is an object with program and parsed.
- parsed is null when the runtime has no parser for the owning program.
- Clients must branch on shape rather than assume parsed is present.
The AccountNotification Envelope Is Identical Across Encodings
The notification result shape does not change when you switch encodings. Per the Solana documentation, an accountNotification carries a result with context: { slot } and value: { lamports, owner, data, executable, rentEpoch }. The only field whose shape varies is data. Under base64 and base64+zstd it is a tuple; under jsonParsed it is an object.
This matters for client design. You can write one handler for the envelope and isolate the encoding-specific logic in a single function that normalizes data into bytes or a parsed object. That keeps the branching contained and makes it obvious where a shape assumption could break.
It also means you cannot infer the encoding from the envelope. The encoding label lives inside data for the tuple forms, and the object form is self-identifying. A robust handler inspects data rather than trusting a configuration flag, because a misconfigured subscription is otherwise indistinguishable from a parser gap.
- Envelope: context.slot plus value with lamports, owner, data, executable, rentEpoch.
- Only value.data changes shape between encodings.
- Normalize data in one place; keep the rest of the handler encoding-agnostic.
Runnable Node.js Example: Subscribing with base64 and jsonParsed
The example below opens two subscriptions to the same account, one with encoding base64 and one with jsonParsed, and prints the data shape each produces. It uses the ws package and the global fetch for the initial account read. Replace the endpoint and the account pubkey with your own. The goal is to observe the shapes on your endpoint, not to trust a screenshot.
Note the normalization function: it detects the tuple form, decodes base64, and reports byte length; it detects the object form and reports whether parsed is null. That single function is the pattern to carry into production, because it makes the parser gap visible instead of silent.
// npm install ws
import WebSocket from 'ws';
const WS_URL = process.env.SOLANA_WS_URL || 'wss://your-endpoint.example';
const ACCOUNT = process.env.ACCOUNT_PUBKEY || 'YourAccountPubkeyHere';
function normalizeData(data) {
if (Array.isArray(data)) {
const [payload, encoding] = data;
const bytes = Buffer.from(payload, 'base64');
return { kind: 'raw', encoding, byteLength: bytes.length };
}
if (data && typeof data === 'object') {
return {
kind: 'parsed',
program: data.program,
parsedIsNull: data.parsed === null,
parsed: data.parsed,
};
}
return { kind: 'unknown', data };
}
function subscribe(encoding) {
const ws = new WebSocket(WS_URL);
ws.on('open', () => {
ws.send(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'accountSubscribe',
params: [ACCOUNT, { encoding, commitment: 'confirmed' }],
}));
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.method === 'accountNotification') {
const { slot } = msg.params.result.context;
const { owner, data } = msg.params.result.value;
console.log(encoding, 'slot', slot, 'owner', owner, normalizeData(data));
} else {
console.log(encoding, 'control message', msg);
}
});
ws.on('error', (err) => console.error(encoding, 'ws error', err.message));
return ws;
}
const a = subscribe('base64');
const b = subscribe('jsonParsed');
// Close after 60s for a bounded experiment.
setTimeout(() => { a.close(); b.close(); }, 60000);Results Table: Measuring Encoding Behavior on Your Endpoint
The table below is a template, not a set of measured values. Fill it in against your own endpoint and account, because wire size, client work, and parser behavior depend on the account and the node version. Run the example above, capture one notification per encoding, and record the observed values. Do not copy numbers from any article, including this one.
For wire size, log the raw message length before JSON.parse. For client work, note whether you needed a Borsh layout or a zstd dependency. For parser sensitivity, compare parsed output across two node versions if you can, or across two providers, and note any field differences. The point is to make the tradeoff concrete for your workload.
- Encoding | Wire size (bytes) | Client work | Parser-version sensitivity | parsed null observed?
- base64 | measure | Borsh layout required | none (bytes are stable) | n/a
- base64+zstd | measure | Borsh layout plus zstd decompressor | none | n/a
- jsonParsed | measure | none for supported programs | yes, parser output can change | yes for custom-program accounts
Deserialization Cost and Parser Version Drift
The core tradeoff is where deserialization happens. base64 moves the work to your client, which must maintain a Borsh layout matching the on-chain program. That layout is a maintenance burden, but it is versioned by you, so a node upgrade cannot silently change your data's meaning. This is why production indexers usually request base64 for stability.
jsonParsed moves the work to the node. That is convenient and removes the layout burden, but it introduces a dependency on the node's parser. Parser output is program-specific and can change when the node's parser changes, which means the same account can produce different parsed structures across node versions or providers. For inspection and debugging, that is fine. For a long-running indexer, it is a stability risk.
A practical pattern is to use jsonParsed for human inspection and one-off scripts, and base64 for anything that feeds a database or downstream consumer. If you need both, subscribe twice and reconcile, or subscribe with base64 and parse locally with a layout you control. The Solana WebSocket API page is a useful reference for the method surface when you are wiring this up.
- base64: client owns deserialization; stable across node upgrades.
- jsonParsed: node owns deserialization; output can drift with parser changes.
- Common pattern: jsonParsed for inspection, base64 for indexing.
Troubleshooting: parsed null, Shape Mismatches, and Subscription IDs
When parsed is null, the account is owned by a program the runtime has no parser for. This is documented behavior, not an error. The fix is to fall back to raw bytes, which means you need a Borsh layout for that program. If you cannot maintain one, consider whether you need the account at all, or whether a different method such as getProgramAccounts with dataSlice and filters better fits the access pattern.
When you see an unexpected tuple or object shape, check the encoding label inside data. A tuple with "base64" means you subscribed with base64; an object means jsonParsed. If your handler assumed one and got the other, the subscription configuration and the handler are out of sync. Normalize in one place and log the shape on first notification during development.
For compressed payloads, confirm the label is "base64+zstd" before decompressing. Attempting to base64-decode a zstd payload without decompressing produces garbage bytes that may still deserialize into nonsense. Validate a known field, such as lamports, against a getAccountInfo read before trusting the decoded structure.
For subscription id correlation, store the id returned by the accountSubscribe response and match it against the subscription field in each notification. If you open multiple subscriptions, including to the same account with different encodings, the id is the only reliable way to know which is which. The logsSubscribe notification parsing article covers the same correlation discipline for log subscriptions.
If notifications stop arriving, check the connection lifecycle rather than the encoding. WebSocket connections can drop, and a dropped connection means no notifications regardless of encoding. Reconnect logic and resubscription are covered in the Solana WebSocket guide.
- parsed null: no parser for the owning program; fall back to raw bytes.
- Shape mismatch: inspect the encoding label inside data.
- Compressed payload: decompress before base64-decoding; validate a known field.
- Correlate notifications by subscription id, not by arrival order.
Limitations: Parser Coverage, Version Drift, and Account Scope
jsonParsed coverage is limited to programs the runtime knows. Custom programs are not parsed, and the set of parsed programs is not something you control. Treat jsonParsed as a convenience for supported programs, not a universal representation. Any client that assumes universal parsing will drop updates for custom-program accounts.
Parser output can drift across node versions and providers. The documented behavior is that parsed structures are program-specific; the practical consequence is that you should not treat parsed output as a stable schema for long-term storage. If you need stability, base64 is the safer contract because the bytes are the program's own serialization.
accountSubscribe watches a single account. It is not a program-wide subscription, and it does not give you the transaction that caused the change. If you need the cause, pair it with logsSubscribe or fetch the transaction by signature. If you need many accounts, consider getProgramAccounts or a program subscription pattern, and be aware of the cost and rate characteristics of your provider, which vary by provider and plan. For endpoint options, see Solana networks and RPC pricing.
- jsonParsed covers only programs the runtime knows; custom programs return parsed null.
- Parsed output can drift across node versions and providers.
- accountSubscribe is per-account and does not include the causing transaction.
Next Steps: Choosing an Encoding and Wiring It Into Your Stack
Start by running the example above against your endpoint and filling in the results table. That gives you a concrete basis for the decision rather than a rule of thumb. If your account is owned by a supported program and you only need inspection, jsonParsed is convenient. If you are indexing, base64 is the stable default, and base64+zstd is worth measuring if bandwidth is a constraint.
Then harden the handler: normalize data in one function, branch on shape, log the encoding label on first notification, and correlate by subscription id. Add reconnect logic and resubscription so a dropped connection does not silently stop your updates. If you are building on OnFinality, the Solana WebSocket API reference and the API service page describe the endpoint surface, and the OnFinality Learn hub collects related guides including reading Solana account info, rent, and token accounts.
Finally, decide where parsing lives in your architecture. If you parse locally, version your Borsh layouts alongside the program and test them against known accounts. If you rely on jsonParsed, pin your expectations to a node version and monitor for drift. Either way, make the encoding choice explicit in configuration and visible in logs, so the next person debugging a shape mismatch can see it immediately.
- Measure first: fill the results table on your endpoint.
- Harden the handler: normalize, branch, log, correlate.
- Decide where parsing lives and version it deliberately.