RustAG
Reference

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

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

ts
// 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
ts
// 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);
Note
Non-2xx responses are thrown as 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 & pathBody / queryReturns
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 via REST
# 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 & pathBody / queryReturns
GET /api/stagenetid, 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.

MethodNotes
getHealthReturns "ok".
getVersion{ "robinhood chain-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 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).
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"; // 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 limit

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; no slot stream is emitted.
  • accountUnsubscribe / signatureUnsubscribe / slotUnsubscribe → cancel a subscription.
Realtime push is Phase 2
An optional 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.