You're debugging a Solana dashboard because the wallet balance looks valid in development, then a malformed API payload breaks the production worker. The fix usually isn't another clever type alias. It's a small, explicit TypeScript code example that separates RPC access, runtime validation, and transaction logic.
Solana applications sit between strongly typed application code and data that arrives dynamically from wallets, RPC nodes, WebSocket feeds, third-party APIs, and untrusted token projects. The patterns below keep that boundary narrow. Each example is intentionally concise, crypto-focused, and designed for Node.js services that need predictable behavior without turning every function into a generic-type puzzle.
Introduction and Setup
Start with a small Node.js project and keep infrastructure code separate from trading and analytics logic. Install TypeScript, a runner such as tsx, and the official Solana client:
npm install @solana/web3.js
npm install -D typescript tsx @types/node
Solana's official JavaScript and TypeScript SDK is @solana/web3.js. The Solana documentation describes it as the legacy TypeScript SDK and points developers toward related packages such as @solana/spl-token for Token and Token-2022 workflows.
Use strict compiler settings from the beginning:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"outDir": "dist"
},
"include": ["src"]
}
strict catches unsafe assumptions. noUncheckedIndexedAccess is particularly useful when reading token arrays, account lists, or event batches, because an array lookup can legitimately produce undefined.
Keep secrets and imports predictable
A practical layout might look like this:
src/
config.ts
rpc.ts
types.ts
validate.ts
swaps.ts
Keep the RPC endpoint in an environment variable rather than embedding it in source:
const rpcUrl = process.env.SOLANA_RPC_URL;
if (!rpcUrl) throw new Error("SOLANA_RPC_URL is required");
Use a funded signer only in a protected backend or local test environment. Public keys, token mints, and transaction signatures can be logged more freely, but private keys and API credentials shouldn't enter source control or client bundles.
Practical rule: TypeScript protects the code you compile. It doesn't prove that a wallet response, token account, or JSON payload is honest at runtime.
Overview of Solana Tracker APIs
A Solana application usually needs several kinds of access, and each one solves a different problem.
| API category | Best fit | Typical crypto task |
|---|---|---|
| RPC | Direct chain interaction | Read balances, submit transactions, inspect slots |
| WebSocket Datastream | Live updates | Track trades, wallet activity, prices, or launches |
| Data API | Indexed queries | Load token, wallet, market, and historical data |
| Raptor Swap | Programmatic execution | Request routed swap transactions |
| Risk analytics | Decision support | Inspect token and wallet risk signals before trading |
RPC is the primitive layer. Use it when your code needs Solana-native operations such as getBalance, getAccountInfo, transaction simulation, or transaction submission. It gives you chain access, but it doesn't automatically provide a convenient market-data model.
WebSocket feeds fit applications that must react continuously. A trading interface can subscribe to activity instead of polling. A wallet monitor can process account changes as they arrive. Data APIs are more suitable for indexed views, where the application needs token metadata, wallet history, or market information without reconstructing every record from raw chain data.
Raptor Swap belongs in the execution path. Your service can request a route, validate the returned transaction data, obtain the required approval, and submit the transaction through an RPC connection. Risk analytics should remain a separate decision layer. A quote tells you how a swap may execute. It doesn't tell you whether the token or wallet behavior is acceptable.

Solana Tracker combines these categories through a trading and developer platform, while @solana/web3.js remains the direct SDK choice for native Solana calls. Pick the narrowest interface that matches the job. Don't use a WebSocket stream to answer a historical query, and don't treat an indexed Data API response as a signed transaction.
Connecting to Solana RPC
The smallest useful RPC wrapper should create one connection and expose domain-specific functions. That keeps endpoint configuration out of every call site.
import { Connection, PublicKey, type Commitment } from "@solana/web3.js";
const endpoint = process.env.SOLANA_RPC_URL;
if (!endpoint) throw new Error("SOLANA_RPC_URL is required");
const commitment: Commitment = "confirmed";
export const connection = new Connection(endpoint, commitment);
export async function readWalletBalance(address: string) {
const owner = new PublicKey(address);
const lamports = await connection.getBalance(owner);
return { address, lamports };
}
export async function readBlockTime(slot: number) {
const blockTime = await connection.getBlockTime(slot);
return { slot, blockTime };
}
getBalance returns lamports as a number, while getBlockTime can return null when the node has no timestamp for the slot. Preserve that possibility instead of forcing a value with a cast.
Handle failures at the boundary:
export async function safeBalance(address: string) {
try {
return await readWalletBalance(address);
} catch (error) {
const message = error instanceof Error ? error.message : "RPC request failed";
throw new Error(`Unable to read ${address}: ${message}`);
}
}
This wrapper adds context without pretending that every error is recoverable. Retry transport failures in a higher-level worker, but don't blindly retry invalid public keys or rejected transactions.
Separate declarations from implementation
A .d.ts file should describe a contract, not contain executable behavior:
export interface WalletBalance {
address: string;
lamports: number;
}
export declare function readWalletBalance(
address: string
): Promise<WalletBalance>;
TypeScript's declaration-file guidance states that .d.ts files contain only type information, emit no JavaScript, and exist for type-checking. For a package, point consumers to bundled declarations with the types field in package.json.
For a direct Node service, a normal .ts implementation is usually clearer than an ambient declaration. Use .d.ts when describing an external module, a global injected by a runtime, or a published package contract. The Solana RPC documentation is the right place to compare endpoint capabilities before selecting a provider.
Subscribing to WebSocket Streams
A subscription is an event source, not a database. The handler should validate or normalize incoming data, update application state, and remain safe to call repeatedly.
The official Solana WebSocket interface includes accountSubscribe, programSubscribe, logsSubscribe, blockSubscribe, rootSubscribe, and signatureSubscribe. These cover account changes, program-owned accounts, transaction logs, block notifications, root slots, and transaction confirmation respectively, as documented in the Solana WebSocket RPC reference.
Account and program subscriptions
import { PublicKey } from "@solana/web3.js";
import { connection } from "./rpc.js";
const owner = new PublicKey(process.env.WALLET_ADDRESS!);
const accountSubscription = connection.onAccountChange(
owner,
({ lamports }) => {
console.log({ wallet: owner.toBase58(), lamports });
},
"confirmed"
);
const programSubscription = connection.onProgramAccountChange(
new PublicKey(process.env.PROGRAM_ID!),
({ accountId, accountInfo }) => {
console.log({
account: accountId.toBase58(),
lamports: accountInfo.lamports
});
},
"confirmed"
);
The callback types come from @solana/web3.js, so you don't need to recreate the SDK's account structures. You should still decode program-specific account data separately. Raw bytes aren't a business object until the correct layout and discriminator have been checked.
Logs and signatures
const logsSubscription = connection.onLogs(
"all",
({ signature, logs, err }) => {
if (err) return console.error("Failed transaction", signature);
console.log({ signature, logs });
},
"confirmed"
);
const signature = "TRANSACTION_SIGNATURE";
const confirmationSubscription = connection.onSignature(
signature,
({ err }) => console.log({ signature, confirmed: !err }),
"confirmed"
);
Use a program filter when your provider supports it, rather than processing every transaction in a backend worker. For an account monitor, deduplicate by signature or slot before writing to storage. For a long-running process, keep the subscription IDs and remove them during shutdown:
async function closeStreams() {
await connection.removeAccountChangeListener(accountSubscription);
await connection.removeProgramAccountChangeListener(programSubscription);
await connection.removeOnLogsListener(logsSubscription);
await connection.removeSignatureListener(confirmationSubscription);
}
process.once("SIGTERM", () => void closeStreams());
WebSocket connections can drop. Reconnect with bounded backoff, resubscribe after reconnecting, and treat the stream as a live view that may need reconciliation through RPC. Don't assume an event was received merely because the socket was previously open.
Performing Token Swaps
Swap execution has three distinct stages: obtain a route, verify the transaction request, and sign only after policy checks pass. The exact Raptor Swap SDK method names depend on the installed client version, so keep the provider adapter isolated instead of spreading provider-specific calls through your application.
A typed application-level request can stay small:
type SwapRequest = {
inputMint: string;
outputMint: string;
amount: string;
slippageBps: number;
userPublicKey: string;
};
type SwapQuote = {
transaction: string;
inputMint: string;
outputMint: string;
amount: string;
minOutputAmount: string;
};
The adapter can expose a stable interface:
interface SwapRouter {
quote(request: SwapRequest): Promise<SwapQuote>;
}
Your route validation should reject mismatched mints, empty transaction payloads, invalid public keys, and unacceptable slippage policy before signing:
import { PublicKey } from "@solana/web3.js";
function validateSwap(request: SwapRequest) {
new PublicKey(request.inputMint);
new PublicKey(request.outputMint);
new PublicKey(request.userPublicKey);
if (!/^\d+$/.test(request.amount)) {
throw new Error("Amount must be an integer string");
}
if (!Number.isInteger(request.slippageBps) || request.slippageBps < 0) {
throw new Error("Invalid slippage policy");
}
}
A backend can then compose the flow:
async function prepareSwap(
router: SwapRouter,
request: SwapRequest
): Promise<SwapQuote> {
validateSwap(request);
const quote = await router.quote(request);
if (!quote.transaction) {
throw new Error("Router returned no transaction");
}
if (quote.inputMint !== request.inputMint) {
throw new Error("Router input mint mismatch");
}
return quote;
}
Treat approval as a policy boundary
Never interpret a successful quote as permission to spend. The signer or wallet must approve the transaction, and the application should display the input mint, output mint, amount, expected minimum output, and recipient before signing.
Signing rule: A quote is data. A serialized transaction is an instruction. Keep user approval between the two.
After signing, submit through sendRawTransaction, then confirm using the returned signature and the same commitment policy used elsewhere in the service. Handle rejected approvals, expired blockhashes, simulation failures, and slippage errors as separate outcomes so the UI can give a useful response.
For production routing, an aggregator such as Raptor Swap can sit behind the SwapRouter interface. That makes it possible to test policy and transaction handling without coupling every domain function to one provider.
Fetching Token and Wallet Data
Indexed APIs suit dashboards because they return prepared views instead of requiring the application to rebuild history from raw transactions. The endpoint name matters less than the contract: define the fields the UI consumes, validate the response at runtime, and retain unknown fields rather than assigning them an incorrect meaning.
type TokenOverview = {
mint: string;
symbol?: string;
name?: string;
priceUsd?: number;
};
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(url, init);
if (!response.ok) {
throw new Error(`HTTP ${response.status} from token API`);
}
return response.json() as Promise<T>;
}
The generic return type improves editor support and downstream code, but it does not validate JSON. Apply a runtime parser before trusting the result. Keep the API function narrow so callers receive a checked token object:
export async function getTokenOverview(
baseUrl: string,
mint: string,
apiKey: string
) {
const data = await fetchJson<unknown>(
`${baseUrl}/tokens/${encodeURIComponent(mint)}`,
{ headers: { Authorization: `Bearer ${apiKey}` } }
);
return parseTokenOverview(data);
}
Normalize wallet balances before rendering. Token accounts may use different decimal scales, so raw integer units and display values must remain separate:
type Balance = {
mint: string;
rawAmount: string;
decimals: number;
};
function displayAmount(balance: Balance): string {
const raw = BigInt(balance.rawAmount);
const scale = 10n ** BigInt(balance.decimals);
const whole = raw / scale;
const fraction = (raw % scale).toString().padStart(balance.decimals, "0");
return `${whole}.${fraction}`.replace(/\.?0+$/, "");
}
Store raw quantities as strings or bigint. JavaScript numbers can lose precision for large balances, and formatting code must never change the amount later submitted in a transaction. This separation keeps wallet views readable while preserving exact values for crypto workflows.
Ensuring Type Safety with Runtime Checks
A TypeScript annotation doesn't transform an API response. This code compiles, but it can still fail:
const token = await fetchJson<TokenOverview>(url);
console.log(token.mint);
The server could return an object without mint, a number encoded as a string, or an entirely different error payload with a successful HTTP status. The gap between static types and runtime data is especially important for API responses, event payloads, and forms, an issue highlighted in practical TypeScript generics guidance.
A small type guard works when the shape is simple:
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function parseTokenOverview(value: unknown): TokenOverview {
if (!isRecord(value) || typeof value.mint !== "string") {
throw new Error("Invalid token overview");
}
return {
mint: value.mint,
symbol: typeof value.symbol === "string" ? value.symbol : undefined,
name: typeof value.name === "string" ? value.name : undefined,
priceUsd: typeof value.priceUsd === "number" ? value.priceUsd : undefined
};
}
This parser deliberately treats optional fields as optional. It doesn't invent a symbol or coerce an arbitrary value into a price.
Use generic constraints where repetition is real
A generic helper makes sense when several endpoints share an envelope:
type ApiEnvelope<T> = {
data: T;
nextPage?: string;
};
function unwrap<T>(payload: ApiEnvelope<T>): T {
return payload.data;
}
The generic expresses a stable relationship. It doesn't claim that unknown JSON already matches ApiEnvelope<T>. Parse the envelope first, then pass the validated value to unwrap.
Avoid any for convenience. TypeScript's official style guidance recommends primitive lowercase types such as string and number, discourages broad any except during legacy migration, and favors the simplest type that accurately expresses the code. Prefer a union when a parameter accepts a small set of modes, and don't create overloads when one union-typed function communicates the behavior clearly.
Runtime checks should also cover transaction policy. Validate mint identity, amount format, signer ownership, and expected destination before a wallet prompt. Type safety is valuable here because it makes the checks visible, but the actual protection comes from executing those checks against runtime values.
Quick Reference Tables
Use this table as a compact lookup while writing a Solana service. The return descriptions reflect the application-level meaning you should preserve, including nullable values and asynchronous behavior.
| Action | Method | Parameters | Returns |
|---|---|---|---|
| Read wallet balance | connection.getBalance |
PublicKey, optional commitment |
Promise<number> lamports |
| Read block time | connection.getBlockTime |
slot number | Promise<number | null> |
| Read account data | connection.getAccountInfo |
PublicKey, optional commitment |
account info or null |
| Send a signed transaction | connection.sendRawTransaction |
serialized transaction bytes | Promise<string> signature |
| Confirm a transaction | connection.confirmTransaction |
signature or confirmation strategy | confirmation response |
| Watch one account | connection.onAccountChange |
public key, callback, commitment | numeric listener ID |
| Watch program accounts | connection.onProgramAccountChange |
program public key, callback, commitment | numeric listener ID |
| Watch transaction logs | connection.onLogs |
filter, callback, commitment | numeric listener ID |
| Watch a signature | connection.onSignature |
signature, callback, commitment | numeric listener ID |
| Remove account stream | connection.removeAccountChangeListener |
listener ID | Promise<void> |
| Fetch token data | Data API client or fetch |
mint and request options | validated token model |
| Fetch wallet data | Data API client or fetch |
wallet address and pagination | validated wallet model |
| Request a swap route | Raptor Swap adapter | input mint, output mint, amount, slippage, owner | typed quote or transaction request |
| Format token amount | local domain function | raw amount and decimals | display string |
Choose the narrowest return type
Don't expose a raw RPC response to every part of your application. Convert it at the edge:
type WalletSnapshot = {
address: string;
lamports: number;
observedAt: Date;
};
async function walletSnapshot(address: string): Promise<WalletSnapshot> {
const result = await readWalletBalance(address);
return {
address: result.address,
lamports: result.lamports,
observedAt: new Date()
};
}
The domain model is easier to test and less likely to leak provider-specific details into UI code. For paginated APIs, return both the records and the next cursor rather than hiding pagination inside an unbounded loop.
type Page<T> = {
items: T[];
nextCursor?: string;
};
That shape keeps backpressure under the caller's control. A worker can persist one page, retry it, and continue from the cursor without loading an entire wallet history into memory.
Cross-Reference to Advanced Patterns
A wallet monitor, token dashboard, or controlled swap service can start with compact snippets. Production Solana applications need stricter boundaries for decoding, signing, risk checks, and event recovery.
For program accounts, decode raw bytes only after verifying ownership and the expected layout. Anchor projects can use generated clients and account types. Lower-level clients should keep decoding behind a typed adapter, returning a domain model instead of exposing provider-specific data across the application. Short, focused patterns are available in the Solana web3.js examples repository.
Model every required signer explicitly:
type Approval = {
publicKey: string;
role: "payer" | "owner" | "delegate";
approved: boolean;
};
Approval should reflect the transaction message, signer role, and user-facing policy. A connected wallet alone does not prove that the required authorization is complete.
Run risk checks before execution. Solana Tracker provides a unified data API, real-time WebSocket feeds, RPC access, Raptor Swap routing, and tools including Rugcheck and wallet tracking. Keep these capabilities in separate adapters, so a risk response cannot be treated as a swap quote or RPC confirmation.
Use a durable event pipeline for high-throughput ingestion. Persist the signature, slot, source, and processing status, then reconcile missed events with an indexed query or RPC read. WebSocket callbacks provide delivery signals, not durable state. This separation lets workers retry safely and keeps dynamic blockchain data within explicit TypeScript types.
Glossary of Key Terms
- RPC: A request interface for reading Solana state and submitting transactions.
- WebSocket: A persistent connection used to receive live blockchain notifications.
- Subscription: A registered WebSocket listener for account, program, log, block, root, or signature events.
- Public key: The address that identifies a wallet, token mint, account, or program.
- Mint: The Solana account that defines a token and its supply rules.
- Lamport: The smallest unit used to represent SOL in RPC responses.
- Slippage: The allowed difference between expected and executed swap pricing.
- Runtime validation: Checks performed on actual values while the program runs.
- Generic constraint: A rule limiting which types a generic function can accept.
- Ambient declaration: Type information declared externally in a
.d.tsfile. - Paginated response: Data returned in batches, usually with a cursor for the next batch.
- Type guard: A function that proves a value has a specific runtime shape to TypeScript.
- Transaction signature: The identifier returned for a submitted Solana transaction.
- Commitment: The confirmation level requested from an RPC provider.
Solana Tracker offers RPC infrastructure, indexed token and wallet data, live streams, Raptor Swap routing, and risk-analysis tools that fit the typed boundaries in these examples. Use Solana Tracker to explore the APIs and build your next Solana Node.js workflow with explicit validation from request to transaction confirmation.