SDK & API
Three ways to drive RustAG: the @rustag/sdk TypeScript client over REST, the raw REST contract the dashboard sits on, and the Robinhood Chain-compatible JSON-RPC used during closure resolution.
TypeScript SDK
The @rustag/sdk package exposes a single client class, RustagClient, that wraps the running REST API (default base http://localhost:9000). The primary surface is rehearse() and verify().
Construction
import { RustagClient } from "@rustag/sdk"; const client = new RustagClient({ baseUrl: "http://localhost:9000" });// or against the hosted service:const client = new RustagClient({ baseUrl: "https://api.rustag.dev", apiKey: process.env.RUSTAG_API_KEY,});RustagClientOptions: baseUrl (default http://localhost:9000; trailing slashes stripped), apiKey (optional Bearer rk_… for the hosted service), and fetch (a custom fetch implementation for Node runtimes without global fetch).
Rehearse & verify
These are the two primary methods — everything else is dashboard/stagenet tooling.
// Rehearse a multisig proposalconst bundle = await client.rehearse({ proposal: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", rpc: "https://mainnet.alchemy.com/?api-key=YOUR_KEY", failOn: "high", // throws if any HIGH/CRITICAL alarm fires}); console.log(bundle.grade); // "A"console.log(bundle.alarms); // [{ rule, severity, message }]console.log(bundle.semanticDiff); // [{ type, ...fields }]console.log(bundle.preStateRoot); // hex SHA-256 Merkle rootconsole.log(bundle.postStateRoot);console.log(bundle.signerPubkey); // attester Ed25519 pubkey // Rehearse a raw transactionconst bundle2 = await client.rehearse({ payload: "<BASE64_TX>", rpc: process.env.MAINNET_RPC,}); // Built-in demo (no RPC needed)const demo = await client.rehearse({ demo: true }); // Verify a bundle offline (no network)const report = await client.verify({ bundle: bundle, // EvidenceBundle object or JSON string closure: closureJson, // portable closure JSON string});console.log(report.valid, report.grade); // true, "A"Other methods
Stagenet & dashboard methods — these target the persistent stagenet dev server (rustag serve or rustag start):
health()→{ status, version }getStagenet()→StagenetInfo— id, name, network, slot, rpcUrl, wsUrl, accounts, transactions.listAccounts({ limit?, offset? })→AccountInfo[]getAccount(pubkey)→AccountInfo— lazily mirrored from mainnet if not local.listTransactions({ limit? })→TransactionInfo[]airdrop(pubkey, sol)→{ signature, lamports }overrideAccount(params)→{ ok }preload(programs)→{ loaded, unknown }
Phase 2 Phase 2 · Preview — scheduler / analytics / simulation:
listSchedules(),createSchedule(params),deleteSchedule(id),toggleSchedule(id, enabled)getMetrics({ series?, limit? })→ analytics time-series, each point{ t, v }simulate(transactions, { label?, encoding? })→ScenarioReport
// Full GroundTruth CI workflow in TypeScriptimport { RustagClient } from "@rustag/sdk"; const client = new RustagClient({ baseUrl: process.env.RUSTAG_API_URL }); // Rehearse and gate on severityconst bundle = await client.rehearse({ proposal: process.env.PROPOSAL_PUBKEY, rpc: process.env.MAINNET_RPC, failOn: "high",}); // Write the bundle + closure for archivingawait fs.writeFile("bundle.json", JSON.stringify(bundle, null, 2)); // Offline verify (zero network)const report = await client.verify({ bundle, closure: closureJson });if (!report.valid) process.exit(1);Error of the form RustAG API <status> <statusText>: <body>. The failOn option causes rehearse() to throw before returning if any alarm meets or exceeds the given severity.REST API
Core GroundTruth endpoints
These endpoints are served on http://127.0.0.1:9000 by default (or $PORT on Render). CORS is permissive.
| Method & path | Body / query | Returns |
|---|---|---|
| GET /api/health | — | { status: "ok", version } |
| POST /api/rehearse | { proposal?, payload?, rpc?, demo?, failOn? } | EvidenceBundle JSON — signed, with pre/post state roots, semantic diff, alarms, grade |
| POST /api/verify | { bundle, closure } | { valid: bool, grade, alarms, signer } — offline verification, no RPC needed |
| POST /api/forensics | { signature, rpc, patch?, patchProgram? } | { verdict: 'BLOCKED' | 'REPRODUCED', diff, logs } |
# Rehearse a multisig proposalcurl -s http://127.0.0.1:9000/api/rehearse \ -H 'content-type: application/json' \ -d '{ "proposal": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", "rpc": "https://mainnet.alchemy.com/?api-key=YOUR_KEY" }' | jq '{grade, alarms: .alarms | length}' # Verify a bundle offlinecurl -s http://127.0.0.1:9000/api/verify \ -H 'content-type: application/json' \ -d '{"bundle": <BUNDLE_JSON>, "closure": <CLOSURE_JSON>}'Stagenet dev endpoints
Used by the dashboard and the stagenet development surface. Require a running stagenet (rustag serve or rustag start):
| Method & path | Body / query | Returns |
|---|---|---|
| GET /api/stagenet | — | id, name, network, slot, rpcUrl, wsUrl, mirrorEnabled, accounts, transactions, dirtyAccounts |
| GET /api/accounts | ?limit (1–1000, def 100) &offset | { accounts: [...] }, newest-touched first |
| GET /api/accounts/{pubkey} | — | one account (lazily mirrored from mainnet); 404 if missing |
| 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 } |
| 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) |
JSON-RPC (closure resolution)
The stagenet speaks a Robinhood Chain-compatible JSON-RPC dialect on http://127.0.0.1:8899. This is primarily used by the ingest layer during closure resolution (getMultipleAccounts) — but you can also point any @solana/web3.js Connection at it for integration testing. Both single requests and batch arrays are supported.
| Method | Notes |
|---|---|
| getHealth | Returns "ok". |
| getVersion | { "robinhood chain-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 from mainnet. |
| getAccountInfo | [pubkey, {encoding}] → base64 account or null; lazily mirrors. |
| getMultipleAccounts | [[pubkey,…], {encoding}] — used by the ingest layer during closure resolution. |
| 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"; // Point at the stagenet RPC for integration testingconst connection = new Connection("http://127.0.0.1:8899");const balance = await connection.getBalance(pubkey);await connection.requestAirdrop(pubkey, 2_000_000_000); // 2 ETH, no faucet limitWebSocket 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; no slot stream is emitted.accountUnsubscribe/signatureUnsubscribe/slotUnsubscribe→ cancel a subscription.
realtime Cargo 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.