RustAG
Reference

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

ts
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:

ts
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:8899

Client 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 } — set lamports and/or raw SPL tokenBalance.
  • 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).
ts
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");
Note
Non-2xx responses are turned into a thrown 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 & pathBody / queryReturns
GET /api/health{ status: "ok" }
GET /api/stagenetid, 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 & pathBody / queryReturns
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)
airdrop via REST
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 }.

MethodNotes
getHealthReturns "ok".
getVersion{ "solana-core": "2.1.0", "feature-set": 0 }.
getGenesisHash / getIdentityFixed stagenet values.
getSlot / getBlockHeightMonotonic slot (advances per transaction).
getEpochInfoSlot-derived (slotsInEpoch = 432000).
getLatestBlockhash{ blockhash, lastValidBlockHeight } (slot + 150).
isBlockhashValidAlways 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).
getFeeForMessageFixed 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.

ts
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 an accountNotification whenever 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.js uses, so sendAndConfirmTransaction / confirmTransaction work out of the box.
  • slotSubscribe → accepted for compatibility, but no slot stream is emitted.
  • accountUnsubscribe / signatureUnsubscribe / slotUnsubscribe → cancel a subscription.
Realtime push is Phase 2
An optional 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.