SDK & API
Three ways to drive a stagenet: the @rustag/sdk TypeScript client over REST, the raw REST contract the dashboard and SDK sit on, and the Solana-compatible JSON-RPC your existing tooling already speaks.
TypeScript SDK
The @rustag/sdk package exposes a single client class, RustagClient, that wraps a running stagenet's REST API (default base http://localhost:9000). It targets the REST surface, not the Solana JSON-RPC port — for transactions you point a @solana/web3.js Connection at the stagenet's RPC URL instead.
Construction
import { RustagClient } from "@rustag/sdk"; const client = new RustagClient({ baseUrl: "http://localhost:9000" });RustagClientOptions has two optional fields: baseUrl (default http://localhost:9000; trailing slashes are stripped) and fetch (a custom fetch implementation, e.g. for Node runtimes without a global fetch). The default global fetch is bound to globalThis to avoid the browser's “Illegal invocation” error; if no fetch is available, the constructor throws. getStagenet() returns the stagenet's rpcUrl, which you can hand straight to @solana/web3.js:
const stagenet = await client.getStagenet();await client.airdrop(wallet, 1000); import { Connection } from "@solana/web3.js";const connection = new Connection(stagenet.rpcUrl); // http://127.0.0.1:8899Client methods
Phase 1 — available in the local MVP:
health()→{ status }— liveness check.getStagenet()→StagenetInfo— id, name, network, slot, rpcUrl, wsUrl, mirrorEnabled, mainnetRpc, and account/transaction/dirty counts.listAccounts({ limit?, offset? })→AccountInfo[]— newest-touched first.getAccount(pubkey)→AccountInfo— lazily mirrored from mainnet if not local.listTransactions({ limit? })→TransactionInfo[].airdrop(pubkey, sol)→{ signature, lamports }— unlimited, instant, free.overrideAccount(params)→{ ok }— setlamportsand/or raw SPLtokenBalance.preload(programs)→{ loaded, unknown }.
Phase 2 Phase 2 · Preview — depends on the corresponding background workers being enabled on the server:
listSchedules(),createSchedule(params),deleteSchedule(id),toggleSchedule(id, enabled).getMetrics({ series?, limit? })→ analytics time-series, each point{ t, v }.simulate(transactions, { label?, encoding? })→ScenarioReport— replay signed transactions against an isolated fork (the base is never mutated).
await client.getStagenet();await client.listAccounts({ limit: 100 });await client.getAccount("<PUBKEY>");await client.airdrop("<PUBKEY>", 1000);await client.overrideAccount({ pubkey: "<PUBKEY>", lamports: 5_000_000_000 });await client.preload(["jupiter", "pyth", "raydium"]); const report = await client.simulate([signedTxBase64], { label: "swap-scenario" });console.log(report.succeeded, "/", report.total, "ok in", report.durationMs, "ms");Error of the form RustAG API <status> <statusText>: <body>. A transfer schedule's secret_key is redacted to ***redacted*** on read.REST API
The REST API is an axum router mounted under /api, served on http://127.0.0.1:9000 by default. It is the surface consumed by the dashboard and the SDK; CORS is permissive.
| Method & path | Body / query | Returns |
|---|---|---|
| GET /api/health | — | { status: "ok" } |
| GET /api/stagenet | — | id, name, network, slot, rpcUrl, wsUrl, mirrorEnabled, mainnetRpc, accounts, transactions, dirtyAccounts |
| GET /api/accounts | ?limit (1–1000, def 100) &offset | { accounts: [...] }, newest-touched first |
| GET /api/accounts/{pubkey} | — | one account (lazily mirrored); 404 if missing, 400 on invalid pubkey |
| GET /api/transactions | ?limit (1–500, def 50) | { transactions: [...] }, newest first |
| POST /api/airdrop | { pubkey, sol } | { signature, lamports } |
| POST /api/override | { pubkey, lamports?, tokenBalance? } | { ok: true } |
| POST /api/preload | { programs: [...] } | { loaded, unknown } |
Phase 2 Phase 2 · Preview endpoints — require the scheduler / metrics / simulation workers to be enabled:
| Method & path | Body / query | Returns |
|---|---|---|
| GET /api/schedules | — | { schedules: [...] } |
| POST /api/schedules | { name, schedule, action } | the created Schedule |
| DELETE /api/schedules/{id} | — | { ok: <removed> } |
| POST /api/schedules/{id}/toggle | { enabled } | { ok: true, enabled } |
| GET /api/metrics | ?series &limit (1–10000, def 500) | { metrics: { <series>: [{ t, v }] } } |
| POST /api/simulate | { transactions, label?, encoding? } | a ScenarioReport (≤5000 txs) |
curl -s http://127.0.0.1:9000/api/airdrop \ -H 'content-type: application/json' \ -d '{"pubkey":"<PUBKEY>","sol":1000}'# => { "signature": "...", "lamports": 1000000000000 }JSON-RPC compatibility
A stagenet speaks a Solana-compatible JSON-RPC dialect, so a wallet or a @solana/web3.js Connection can point at it and just work. The RPC server listens on http://127.0.0.1:8899 and accepts a JSON-RPC 2.0 body at POST /; both single requests and batch arrays are supported. The advertised version is { solana-core: 2.1.0, feature-set: 0 }.
| Method | Notes |
|---|---|
| getHealth | Returns "ok". |
| getVersion | { "solana-core": "2.1.0", "feature-set": 0 }. |
| getGenesisHash / getIdentity | Fixed stagenet values. |
| getSlot / getBlockHeight | Monotonic slot (advances per transaction). |
| getEpochInfo | Slot-derived (slotsInEpoch = 432000). |
| getLatestBlockhash | { blockhash, lastValidBlockHeight } (slot + 150). |
| isBlockhashValid | Always true — the stagenet blockhash never expires. |
| getMinimumBalanceForRentExemption | [dataLen] → lamports. |
| getBalance | [pubkey] → { context, value }; lazily mirrors. |
| getAccountInfo | [pubkey, {encoding}] → base64 account or null; lazily mirrors. |
| getMultipleAccounts | [[pubkey,…], {encoding}]. |
| getProgramAccounts | [programId, {filters}] → owned accounts; honors dataSize & memcmp (≤10000). |
| getTokenAccountBalance | [tokenAccount] → SPL amount. |
| requestAirdrop | [pubkey, lamports] → signature. |
| sendTransaction | [txBlob, {encoding}] → signature; base58 default, base64 fallback. |
| simulateTransaction | [txBlob, {encoding}] → { err, logs, unitsConsumed, returnData }. |
| getSignatureStatuses | [[sig,…]] → statuses (confirmationStatus "finalized"). |
| getTransaction | [signature] → indexed meta (fee, computeUnitsConsumed, logMessages). |
| getFeeForMessage | Fixed 5000-lamport fee. |
Any method outside this list returns JSON-RPC error -32601 (“method not found”). Encoded transactions are decoded as base58 first, falling back to base64, unless an explicit encoding is supplied.
import { Connection } from "@solana/web3.js"; const connection = new Connection("http://127.0.0.1:8899");const balance = await connection.getBalance(pubkey);await connection.requestAirdrop(pubkey, 2_000_000_000);WebSocket subscriptions
The WebSocket server listens on ws://127.0.0.1:8900. In Phase 1 subscriptions are poll-based (≈1s interval):
accountSubscribe→ returns a subscription id, then pushes anaccountNotificationwhenever the account's(lamports, data length)fingerprint changes.signatureSubscribe→ one-shot; fires once the transaction is found, then auto-cancels. This is what@solana/web3.jsuses, sosendAndConfirmTransaction/confirmTransactionwork out of the box.slotSubscribe→ accepted for compatibility, but no slot stream is emitted.accountUnsubscribe/signatureUnsubscribe/slotUnsubscribe→ cancel a subscription.
realtime feature adds a server-side push mirror: when enabled (and a realtime_ws upstream is configured) the server subscribes to the oracle registry over accountSubscribe upstream with a reconnect loop, replacing the poll. Build with --features realtime.