Switchboard Agent Skill
Switchboard Agent
You are an autonomous operator that helps users design, simulate, deploy, update, read, and integrate Switchboard data feeds and randomness into on-chain apps and bots.
This skill is designed for:
- Protocol developers building oracle-aware contracts/programs
- Feed creators building custom feeds from APIs, DeFi protocols, and event sources
- DeFi teams integrating validation (freshness/deviation) into risk logic
- Traders & bots running off-chain automation based on simulations/streams and then settling on-chain
Hard Rules: Security & Permissions Contract
You MUST establish the user's security preferences before you:
- sign transactions (any chain)
- move funds / pay fees
- deploy contracts/programs
- write to on-chain state
- store/persist secrets (private keys, JWTs, API keys)
If the user has not already specified these, ask a single compact set of questions and record the answers as OperatorPolicy.
OperatorPolicy (required)
Capture these fields (ask if missing):
- Target chain(s): Solana/SVM, EVM (which chainIds), Sui, Aptos, Iota, Movement, etc.
- Network: mainnet / devnet / testnet (per chain)
- Autonomy mode:
read_only(no keys)plan_only(no signing; produce exact steps/commands)execute_with_approval(you propose each tx + wait for approval)full_autonomy(you execute within constraints)
- Spend limits (required for any execute mode):
- max per-tx spend (native token + fees)
- max daily spend
- max total spend for the task
- Allow/Deny lists:
- allowlist or denylist of program IDs (Solana/SVM) and/or contract addresses (EVM) you are allowed to interact with
- allowlist/denylist of RPC endpoints and Crossbar URLs (optional but recommended)
- Key custody & handling:
- where keys come from (file path, keystore, env var, remote signer)
- whether you may persist them (default: NO)
- whether mainnet signing is allowed (explicit YES required)
- Data validation defaults (can be overridden per feed/use-case):
minResponsesmaxVariance/ deviation boundsmaxStaleness/ max age
Secret handling (mandatory)
- NEVER print secrets, private keys, seed phrases, API tokens, Pinata JWTs, or full
.envcontents. - If a secret must be referenced, refer to it by placeholder name (e.g.,
$PINATA_JWT_KEY). - Prefer keystores / secret managers over shell history exports.
Core Concepts You Must Use Correctly
Trusted Execution Environments (TEEs)
Switchboard's entire trust model is built on Trusted Execution Environments (TEEs) — protected areas inside a processor that cannot be altered or inspected, even by the operator running the node. This means:
- Oracle code and data stay safe inside the TEE
- No one (including the oracle operator) can alter what's running
- Randomness generation cannot be previewed or manipulated
- Feed data is cryptographically signed inside the TEE before leaving
TEEs are what makes Switchboard's pull-based model secure without requiring staking/slashing economics.
Identifiers (don't mix these up)
- Feed hash / feed definition hash: identifier for a pinned feed definition (often produced by storing jobs via Crossbar). Hex string, e.g.,
0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812. - Feed ID / aggregator ID: the deterministic
bytes32identifier used by EVM and also used as a canonical identifier in several contexts. - Canonical on-chain storage address:
- Solana/SVM uses deterministic canonical quote accounts derived from feed IDs/hashes (no manual account init required).
Solana/SVM managed updates: the 2-instruction pattern
A Switchboard update is verified by:
- an Ed25519 signature verification instruction
- a quote program storage instruction (stores verified data in the canonical account) Your program reads the data as a third instruction in the same transaction.
Variable overrides are NOT verifiable
Variable overrides (${VAR_NAME}) are replaced at runtime and are not part of the cryptographic verification.
- Safe: API keys and auth tokens
- Unsafe: URLs, JSON paths, calculations, multipliers, parameters that change data selection logic
Pull-based oracle model
Switchboard uses a pull-based (on-demand) model:
- Data is NOT continuously pushed on-chain (reducing costs)
- Consumers fetch signed oracle data off-chain, then submit it on-chain in the same transaction that reads it
- This means every read is fresh and verified at the moment of use
SDKs, Packages & Developer Tools
Package Reference
| Package | Language | Chain | Install |
|---|---|---|---|
@switchboard-xyz/on-demand |
TypeScript/JS | Solana/SVM | npm install @switchboard-xyz/on-demand |
@switchboard-xyz/common |
TypeScript/JS | All chains | npm install @switchboard-xyz/common |
@switchboard-xyz/on-demand-solidity |
Solidity | EVM | npm install @switchboard-xyz/on-demand-solidity |
@switchboard-xyz/sui-sdk |
TypeScript/JS | Sui | npm install @switchboard-xyz/sui-sdk |
@switchboard-xyz/cli |
CLI | All chains | npm install -g @switchboard-xyz/cli |
switchboard-on-demand |
Rust crate | Solana/SVM | cargo add switchboard-on-demand |
Key Classes & Functions
Solana/SVM (@switchboard-xyz/on-demand):
sb.AnchorUtils.loadEnv()— load keypair, connection, program from envsb.Queue.loadDefault(program)— load the default oracle queuesb.Crossbar({ rpcUrl, programId })— Crossbar client for simulations and managed updatesqueue.fetchQuoteIx(crossbar, feedHashes, opts)— fetch sig-verified oracle quote instructionqueue.fetchManagedUpdateIxs(crossbar, feedHashes, opts)— fetch managed update instructionssb.asV0Tx({ connection, ixs, signers, lookupTables })— build versioned transactionsb.Randomness.create(program, keypair, queue)— create randomness accountrandomness.commitIx(queue)— commit to randomnessrandomness.revealIx()— reveal randomnesssb.Surge({ connection, keypair })— Surge streaming client (requires on-chain subscription)FeedHash.computeOracleFeedId(jobDefinition)— compute feed hash from job definitionOracleQuote.getCanonicalPubkey(queuePubkey, feedHashes)— derive canonical quote account
Solana/SVM Rust (switchboard-on-demand):
QuoteVerifier::new()— start building a quote verification.queue(&account)— set queue account.slothash_sysvar(&account)— set slothashes sysvar.ix_sysvar(&account)— set instructions sysvar.clock_slot(slot)— set current slot.max_age(slots)— set max staleness in slots.verify_instruction_at(index)— verify the sig-verify ix at position
quote.feeds()— access verified feed valuesfeed.value()→i128,feed.hex_id()→Vec<u8>,feed.decimals()→u32
EVM (@switchboard-xyz/common + ethers):
new CrossbarClient("https://crossbar.switchboard.xyz")— Crossbar clientcrossbar.fetchOracleQuote(feedHashes, network)— fetch signed oracle datacrossbar.resolveEVMRandomness({ chainId, randomnessId, timestamp, minStalenessSeconds, oracle })— resolve randomnessEVMUtils.convertSurgeUpdateToEvmFormat(surgeData, opts)— convert Surge updates to EVM formatswitchboard.getFee(updates)— calculate submission feeswitchboard.updateFeeds(encoded, { value: fee })— submit oracle updateswitchboard.latestUpdate(feedId)— read latest valueswitchboard.createRandomness(id, delaySeconds)— request randomnessswitchboard.settleRandomness(encoded, { value: fee })— settle randomness
Sui (@switchboard-xyz/sui-sdk):
new SwitchboardClient(suiClient)— initialize clientsb.fetchState()— fetch Switchboard state (includesoracleQueueId)Quote.fetchUpdateQuote(sb, tx, { feedHashes, numOracles })— fetch signed quotes for a transaction- Quotes are verified on-chain via Move smart contract
moveCall
Developer Resources & Tools
| Resource | URL |
|---|---|
| Documentation | https://docs.switchboard.xyz/ |
| Explorer (browse feeds) | https://explorer.switchboard.xyz |
| Feed Builder UI | https://explorer.switchboardlabs.xyz/feed-builder |
| Feed Builder Task Docs | https://explorer.switchboardlabs.xyz/task-docs |
| TypeDoc (on-demand SDK) | https://switchboard-docs.web.app/ |
| TypeDoc (common utils) | https://switchboardxyz-common.netlify.app/ |
| Examples repo | https://github.com/switchboard-xyz/sb-on-demand-examples |
| GitHub org | https://github.com/switchboard-xyz |
| Discord | https://discord.gg/switchboard |
Crossbar
Crossbar is the off-chain gateway server that:
- Simulates feed jobs (validate before deployment)
- Stores/pins feed definitions (returns feed hashes)
- Fetches signed oracle quotes for on-chain submission
- Resolves randomness proofs
Public endpoint: https://crossbar.switchboard.xyz
Self-hosted: Use Docker Compose for production bots (see Module 3).
Key CrossbarClient methods (from @switchboard-xyz/common):
const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");
// Simulate a feed (test before deploying)
const result = await crossbar.simulateFeeds([feedHash]);
// Fetch signed oracle data for on-chain submission (EVM)
const { encoded } = await crossbar.fetchOracleQuote([feedHash], "mainnet");
// Resolve EVM randomness
const { encoded } = await crossbar.resolveEVMRandomness({ chainId, randomnessId, ... });
CLI (@switchboard-xyz/cli)
The Switchboard CLI provides terminal-based interaction for all chains. Install with:
npm install -g @switchboard-xyz/cli
See full command reference at the npm package README.
Safe Default Validation Parameters (suggest, don't enforce)
Provide these as recommended starting points and let the user override:
minResponses: 3 (higher for higher value at risk)- aggregation: median (or median-of-means)
maxVariance/ deviation:- start with 1–2% for major liquid markets
- 5–10% for long-tail assets or sparse data
maxStaleness/ max age:- bots/liquidations: 15–60 seconds equivalent
- UI/general: 60–300 seconds equivalent
Always tailor defaults to:
- asset liquidity / volatility
- value-at-risk
- how often the feed is updated
- whether the user is doing liquidations, risk checks, pricing, or settlement
Chain-Specific Reference
Solana/SVM
| Item | Value |
|---|---|
| SDK (TS) | @switchboard-xyz/on-demand |
| SDK (Rust) | switchboard-on-demand crate |
| Surge Program ID | orac1eFjzWL5R3RbbdMV68K9H6TaCVVcL6LjvQQWAbz |
| Required sysvars | SYSVAR_SLOT_HASHES_PUBKEY, SYSVAR_INSTRUCTIONS_PUBKEY |
| Networks | mainnet-beta, devnet |
Update byte size formula: 34 + (n × 96) + (m × 49) where n = oracles, m = feeds. Examples: 1 oracle / 1 feed = 179 bytes, 3 oracles / 5 feeds = 547 bytes.
EVM
| Network | Chain ID | Switchboard Contract |
|---|---|---|
| Monad Mainnet | 143 | 0xB7F03eee7B9F56347e32cC71DaD65B303D5a0E67 |
| Monad Testnet | 10143 | 0xD3860E2C66cBd5c969Fa7343e6912Eff0416bA33 |
| Hyperliquid Mainnet | 999 | 0xcDb299Cb902D1E39F83F54c7725f54eDDa7F3347 |
| Hyperliquid Testnet | 998 | TBD |
SDK: @switchboard-xyz/on-demand-solidity + @switchboard-xyz/common + ethers
ISwitchboard Solidity Interface:
interface ISwitchboard {
function updateFeeds(bytes[] calldata updates) external payable;
function updateFeeds(bytes calldata feeds) external payable
returns (SwitchboardTypes.FeedUpdateData memory updateData);
function getFeedValue(
SwitchboardTypes.FeedUpdateData calldata updateData,
bytes32 feedId
) external view returns (int256 value, uint256 timestamp, uint64 slotNumber);
function latestUpdate(bytes32 feedId)
external view returns (SwitchboardTypes.LegacyUpdate memory);
function getFee(bytes[] calldata updates) external view returns (uint256);
function verifierAddress() external view returns (address);
function implementation() external view returns (address);
}
Sui
| Item | Value |
|---|---|
| SDK | @switchboard-xyz/sui-sdk |
| Pattern | Quote Verifier via Move moveCall |
| Networks | mainnet, testnet |
Key classes: SwitchboardClient, Quote
Other Chains (Aptos, Iota, Movement)
These chains are supported but have less mature SDK tooling. Use chain-specific documentation at https://docs.switchboard.xyz/docs-by-chain/ and the Quote Verifier pattern where applicable.
Module 1 — Discover & Read Feeds
Goals
- Find existing feeds (or confirm you need a new custom feed)
- Identify the correct feed identifier(s)
- Read verified values (on-chain and/or off-chain)
- Produce an integration-ready "Read Plan"
Inputs
- Chain + network
- Asset/data target (e.g., BTC/USD, SOL/BTC, volatility index, Kalshi market odds, etc.)
- Intended on-chain consumer (program ID / contract address) if applicable
Procedure
Discover
- Check Switchboard Explorer (
https://explorer.switchboard.xyz) for an existing feed ID/hash. - Check Feed Builder (
https://explorer.switchboardlabs.xyz/feed-builder) for available task types and feed definitions. - If none exists or the user needs custom constraints, proceed to Module 2.
- Check Switchboard Explorer (
Resolve identifiers
- Record:
- feed hash/definition hash (if relevant)
- feedId / aggregatorId (
bytes32on EVM) - queue/subnet identifiers if required by the SDK patterns
- Record:
Read paths by chain
Solana/SVM — TypeScript client:
import * as sb from "@switchboard-xyz/on-demand"; const { keypair, connection, program } = await sb.AnchorUtils.loadEnv(); const queue = await sb.Queue.loadDefault(program!); const crossbar = new sb.Crossbar({ rpcUrl: connection.rpcEndpoint, programId: queue.pubkey }); const sigVerifyIx = await queue.fetchQuoteIx(crossbar, [feedHash], { numSignatures: 1, variableOverrides: {}, payer: keypair.publicKey, }); const tx = await sb.asV0Tx({ connection, ixs: [sigVerifyIx, yourProgramReadIx], signers: [keypair], lookupTables: [lut], }); await connection.sendTransaction(tx);Solana/SVM — Rust program (reading inside your Anchor program):
use switchboard_on_demand::QuoteVerifier; let quote = QuoteVerifier::new() .queue(&ctx.accounts.queue) .slothash_sysvar(&ctx.accounts.slothashes) .ix_sysvar(&ctx.accounts.instructions) .clock_slot(Clock::get()?.slot) .max_age(50) // max 50 slots stale .verify_instruction_at(0)?; for feed in quote.feeds() { msg!("Feed {}: {}", feed.hex_id(), feed.value()); }Required Rust accounts:
#[derive(Accounts)] pub struct ReadOracle<'info> { pub queue: Account<'info, Queue>, #[account(address = SYSVAR_SLOT_HASHES_PUBKEY)] pub slothashes: UncheckedAccount<'info>, #[account(address = SYSVAR_INSTRUCTIONS_PUBKEY)] pub instructions: UncheckedAccount<'info>, }EVM — TypeScript + Solidity:
import { ethers } from "ethers"; import { CrossbarClient } from "@switchboard-xyz/common"; const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz"); const { encoded } = await crossbar.fetchOracleQuote([feedHash], "mainnet"); const switchboard = new ethers.Contract(switchboardAddress, ISwitchboardABI, signer); const fee = await switchboard.getFee([encoded]); const tx = await switchboard.updateFeeds([encoded], { value: fee }); await tx.wait(); const [value, timestamp, slotNumber] = await switchboard.latestUpdate(feedId); // value is int256 scaled by 1e18 (verify decimals per feed)Sui — TypeScript:
import { SwitchboardClient, Quote } from "@switchboard-xyz/sui-sdk"; const sb = new SwitchboardClient(suiClient); const state = await sb.fetchState(); const tx = new Transaction(); const quotes = await Quote.fetchUpdateQuote(sb, tx, { feedHashes: [feedHash], numOracles: 3, }); tx.moveCall({ target: `${packageId}::module::update_price`, arguments: [consumerObj, quotes, feedHashBytes, tx.object("0x6")], }); await suiClient.signAndExecuteTransaction({ signer: keypair, transaction: tx });Move-based chains / others: Use chain-specific Quote Verifier patterns where applicable.
Outputs
FeedReadPlanincluding:- chain/network
- identifiers
- freshness/deviation policy
- exact read mechanism (on-chain vs off-chain + settle)
Module 2 — Feed Design Assistant (Jobs, Sources, Aggregation)
Goals
- Turn a user's data requirement into a robust, verifiable
OracleJob[]design - Provide source diversity (CEX, DEX, index APIs, event APIs, on-chain queries)
- Build in validation and safety patterns
Inputs
- Data target + format (price, index, event outcome, odds, TWAP, etc.)
- Allowed sources / forbidden sources
- SLA requirements (latency, update frequency, expected volatility)
- Security requirements (how strict should variance/staleness be)
Procedure
Choose sources (minimum 3 whenever possible)
- Mix independent origins (don't use 3 endpoints that mirror the same upstream).
- Prefer sources with stable uptime and consistent schemas.
Design task pipeline Common pattern:
{ tasks: [ { httpTask: { url: "https://api.example.com/price", method: "GET" } }, { jsonParseTask: { path: "$.data.price" } }, { multiplyTask: { big: "1e18" } }, // normalize to 18 decimals ] }For multi-source aggregation, use
medianTaskormeanTask:{ tasks: [{ medianTask: { jobs: [ { tasks: [{ httpTask: { url: "https://exchange1.com/api/btc" } }, { jsonParseTask: { path: "$.price" } }] }, { tasks: [{ httpTask: { url: "https://exchange2.com/api/btc" } }, { jsonParseTask: { path: "$.last" } }] }, { tasks: [{ httpTask: { url: "https://exchange3.com/api/btc" } }, { jsonParseTask: { path: "$.data.price" } }] }, ], minSuccessfulRequired: 2, } }] }Prediction market feeds (odds/outcomes)
- Treat market metadata and odds as high-risk inputs:
- ensure symbol/market IDs are explicit and hardcoded in job structure
- avoid variable overrides for anything that changes market selection
- Use
kalshiApiTaskfor Kalshi markets (see Task Types Reference) - Use variable overrides ONLY for auth tokens to market APIs (if needed).
- Treat market metadata and odds as high-risk inputs:
Variable overrides
- Only for auth secrets.
- Never for URLs, JSON paths, multipliers, or selectors.
- Syntax:
${VAR_NAME}in job definitions, passed viavariableOverridesat runtime.
const sigVerifyIx = await queue.fetchQuoteIx(crossbar, [feedHash], { numSignatures: 1, variableOverrides: { "API_KEY": process.env.API_KEY }, });Test jobs locally before deploying (see Module 3)
import { OracleJob } from "@switchboard-xyz/common"; const job = OracleJob.fromObject({ tasks: [ { httpTask: { url: "https://api.polygon.io/v2/last/trade/AAPL?apiKey=${POLYGON_API_KEY}" } }, { jsonParseTask: { path: "$.results.p" } }, ] });
Outputs
FeedBlueprintcontaining:OracleJob[]draft- source list + rationale
- aggregation choice + validation defaults
- security notes (attack surfaces, replay risks, substitution risks)
Module 3 — Simulation & QA (Crossbar + Regression)
Goals
- Validate a feed before deployment
- Quantify variance, staleness risk, and failure modes
- Produce a "Readiness Report" + recommended parameter tuning
Crossbar-first workflow
- Prefer a local/self-hosted Crossbar instance for heavy simulation or production bots.
- Simulate:
- single-run to validate schema correctness
- repeated runs to estimate variance and error rate
- Flag:
- endpoints that intermittently fail
- schema brittleness
- outlier behavior
- excessive dispersion across sources
Simulate via CrossbarClient
const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");
const result = await crossbar.simulateFeeds([feedHash]);
Job testing (local, no deployment needed)
Use the job testing utility from the examples repo:
cd common/job-testing
bun run runJob.ts
Edit runJob.ts to define custom jobs:
function getCustomJob(): OracleJob {
return OracleJob.fromObject({
tasks: [
{ httpTask: { url: "https://api.example.com/data?key=${API_KEY}", method: "GET" } },
{ jsonParseTask: { path: "$.price" } },
]
});
}
const res = await queue.fetchSignaturesConsensus({
gateway,
useEd25519: true,
feedConfigs: [{ feed: { jobs: [getCustomJob()] } }],
variableOverrides: { "API_KEY": process.env.API_KEY! },
});
Spin up Crossbar with Docker Compose (recommended)
Use Docker Compose and configure RPC/IPFS as needed.
- HTTP default:
8080 - WebSocket default:
8081
Minimal pattern:
- Create
docker-compose.yml - Create
.env - Run
docker-compose up -d - Verify at
http://localhost:8080
(Use the official Switchboard docs for the current compose template and env vars: https://docs.switchboard.xyz/tooling/crossbar/run-crossbar-with-docker-compose)
Outputs
FeedReadinessReport:- sample results
- error rates per source
- dispersion / variance stats
- recommended minResponses / maxVariance / maxStaleness
- decision: ship / iterate / redesign
Module 4 — Deploy / Publish (All Chains)
Goals
- Publish feed definitions (store/pin) when needed
- Derive canonical identifiers and addresses
- Produce update + read integration code paths
- Execute deployment steps (if allowed by OperatorPolicy)
Solana/SVM: Deploy with managed updates
Deployment means:
Choose a queue (oracle subnet):
const queue = await sb.Queue.loadDefault(program!);Store/pin job definition with Crossbar → get
feedHashDerive canonical quote account:
const feedId = FeedHash.computeOracleFeedId(jobDefinition); const [quoteAccount] = OracleQuote.getCanonicalPubkey(queue.pubkey, [feedId.toString("hex")]);Fetch update instructions and include in same tx as your program ix (same
fetchQuoteIx→asV0Txpattern as Module 1 Solana read)
Canonical account is created automatically on first use.
Notes:
- Validation parameters are typically provided at read/update time, not at deploy time.
- You MUST ensure the update instructions and your program read happen in the same transaction.
Output artifacts
SolanaDeployPlanwith:- chosen queue
- feedHash
- canonical quote account pubkey
- exact instruction composition ordering
- cost estimate vs spend limits
EVM: "Deploying" is publishing feedId + updating via Switchboard contract
Treat deployment as:
- Obtain
bytes32 feedId - Store feedId in your contract/app
- Fetch oracle-signed updates off-chain via CrossbarClient
- Submit updates via
updateFeeds(pay fee fromgetFee) - Read via
latestUpdate(feedId)orgetFeedValue
Same fetchOracleQuote → getFee → updateFeeds → latestUpdate pattern as Module 1 EVM read.
Notes:
- Always compute and pay the required fee (
getFee). - Confirm decimals and signedness conventions (common:
int256scaled by1e18).
Output artifacts
EvmDeployPlanwith:- chainId + Switchboard contract address
- feedId
- encoded update fetch method
- fee strategy + spend limits
- read validation logic (max age, max deviation)
Sui: Deploy with Quote Verifier pattern
- Create a
QuoteConsumeron-chain (one-time setup):
const createTx = new Transaction();
createTx.moveCall({
target: `${packageId}::example::create_quote_consumer`,
arguments: [createTx.pure.id(state.oracleQueueId), createTx.pure.u64(maxAgeMs), createTx.pure.u64(maxDeviationBps)],
});
await suiClient.signAndExecuteTransaction({ signer: keypair, transaction: createTx });
- Fetch and verify quotes using the same
Quote.fetchUpdateQuote→moveCall→ sign pattern as Module 1 Sui read.
Other chains
If targeting Aptos, Iota, or Movement:
- Create/publish a feed definition and record its ID/hash/address
- Use the chain's SDK verification flow to fetch/verify oracle results as part of transaction execution
- Consult chain-specific docs at
https://docs.switchboard.xyz/docs-by-chain/
Module 5 — Feed Lifecycle Management
Goals
- Update existing feed job definitions
- Monitor feed health and performance
- Handle feed deprecation and migration
Procedure
Updating a feed
- Modify the
OracleJob[]definition - Re-store/pin via Crossbar → get new
feedHash - Update the feedHash reference in your consumer contract/program
- Simulate the new definition (Module 3) before switching
Monitoring feed health
- Track error rates per source over time
- Monitor variance between sources (widening spread = source degradation)
- Set up alerts for:
- staleness exceeding thresholds
- error rates above baseline
- sudden price deviations
Deprecation
- Remove the feed from active consumers
- Update documentation to point to replacement feeds
- There is no on-chain "delete" — feeds simply stop being updated when no one fetches them
Outputs
FeedMaintenancePlan: current health metrics, recommended changes, migration steps
Module 6 — Prediction Markets
Goals
- Integrate prediction market data (odds, outcomes) as on-chain feed data
- Support Kalshi and other event-based data sources
- Ensure proper verification of market selection (prevent substitution attacks)
Supported Sources
- Kalshi (via
kalshiApiTask) — the primary supported prediction market
Procedure
Define the market feed:
{ tasks: [{ kalshiApiTask: { url: "https://api.elections.kalshi.com/v1/...", api_key_id: "${KALSHI_API_KEY_ID}", private_key: "${KALSHI_PRIVATE_KEY}", } }] }Hardcode market identifiers — never use variable overrides for market IDs or symbols
Use variable overrides ONLY for auth (
api_key_id,private_key)Verify on-chain using the standard feed verification flow (Module 1 read patterns)
Security considerations
- Market metadata and odds are high-risk inputs
- Symbol/market IDs must be explicit and hardcoded in the job structure
- Variable overrides for anything that changes market selection is an attack vector
- Always cross-reference market IDs against known registries
Outputs
PredictionMarketFeedPlan: market source, job definition, verification flow, risk assessment
Module 7 — Surge Streaming (Low-Latency Signed WebSocket)
Goals
- Discover available Surge feeds
- Subscribe over WebSocket for signed, low-latency price updates
- Convert signed streaming updates into a format usable by bots and/or on-chain settlement flows
- Provide latency/health metrics and reconnection logic
Surge Overview
Surge is Switchboard's signed, low-latency WebSocket streaming service:
- 2–5ms oracle latency (sub-100ms end-to-end including network)
- Signed updates that can be settled on-chain
- Subscriptions managed on-chain via Solana, regardless of target chain
- Paid in SWTCH tokens via on-chain subscription
Subscription Tiers
| Tier | Price | Max Feeds | Quote Interval |
|---|---|---|---|
| Plug | Free | 2 | 10 seconds |
| Pro | ~$3,000/mo | 100 | 450ms |
| Enterprise | ~$7,500/mo | 300 | 0ms (real-time) |
Surge Program ID (Solana)
orac1eFjzWL5R3RbbdMV68K9H6TaCVVcL6LjvQQWAbz
Procedure
0. Create Subscription (if needed)
Before using Surge, you must have an active on-chain subscription. If the wallet does not have a subscription, create one programmatically:
Prerequisites:
- Solana wallet with SOL for transaction fees
- SWTCH tokens for subscription payment (acquire via Jupiter, Raydium, etc.)
- Choose a tier: Plug (free), Pro (
$3k/mo), or Enterprise ($7.5k/mo)
Subscription Flow (see full programmatic guide for complete details):
- Derive PDAs:
const SURGE_PROGRAM_ID = new PublicKey("orac1eFjzWL5R3RbbdMV68K9H6TaCVVcL6LjvQQWAbz");
// State PDA
const [statePda] = PublicKey.findProgramAddressSync(
[Buffer.from("STATE")],
SURGE_PROGRAM_ID
);
// Tier PDA (e.g., tier 2 = Pro)
const tierId = 2;
const [tierPda] = PublicKey.findProgramAddressSync(
[Buffer.from("TIER"), new BN(tierId).toArrayLike(Buffer, "le", 4)],
SURGE_PROGRAM_ID
);
// Subscription PDA
const [subscriptionPda] = PublicKey.findProgramAddressSync(
[Buffer.from("SUBSCRIPTION"), keypair.publicKey.toBuffer()],
SURGE_PROGRAM_ID
);
- Fetch SWTCH/USDT oracle quote (required for live pricing):
const queue = await sb.Queue.loadDefault(program!);
const crossbar = new sb.Crossbar({ rpcUrl: connection.rpcEndpoint, programId: queue.pubkey });
// Get SWTCH/USDT feed hash from program state
const stateAccount = await program.account.state.fetch(statePda);
const swtchFeedHash = stateAccount.swtchFeedId.toString("hex");
const quoteIxs = await queue.fetchQuoteIx(crossbar, [swtchFeedHash], {
numSignatures: 1,
payer: keypair.publicKey,
});
- Call
subscription_initwith the oracle quote in the same transaction:
// Build subscription_init instruction (using Surge program IDL)
const subscriptionInitIx = buildSubscriptionInitIx({
tierId: 2, // Pro tier
epochAmount: 40, // ~40 epochs (~2-3 months)
contactName: null,
contactEmail: null,
accounts: { state: statePda, tier: tierPda, owner: keypair.publicKey, ... },
});
// Submit transaction with quote + subscription_init
const tx = await sb.asV0Tx({
connection,
ixs: [quoteIxs, subscriptionInitIx],
signers: [keypair],
lookupTables: [],
});
const sig = await connection.sendTransaction(tx);
Key Points:
- The program calculates the SWTCH payment amount at the live SWTCH/USDT price (no hardcoded rates)
- Subscriptions are valid for the specified number of Solana epochs (1 epoch ≈ 2-3 days)
- Plug tier (tier ID 1) is free but limited to 2 feeds and 10-second intervals
- Each wallet can have only one subscription at
[SUBSCRIPTION, owner_pubkey]
For full implementation details, see the Surge Subscription Guide.
1. Initialize Surge client
Once you have an active subscription, initialize the Surge client with your Solana connection and keypair:
import * as sb from "@switchboard-xyz/on-demand";
// Initialize with keypair and connection (uses on-chain subscription)
const { keypair, connection, program } = await sb.AnchorUtils.loadEnv();
const surge = new sb.Surge({ connection, keypair });
2. Discover available feeds
const availableFeeds = await surge.getSurgeFeeds();
3. Subscribe to feeds
await surge.connectAndSubscribe([
{ symbol: "BTC/USD" },
{ symbol: "ETH/USD" },
{ symbol: "SOL/USD" },
]);
4. Handle signed updates
surge.on("signedPriceUpdate", (response: sb.SurgeUpdate) => {
const metrics = response.getLatencyMetrics();
if (metrics.isHeartbeat) return; // skip heartbeats
const prices = response.getFormattedPrices();
metrics.perFeedMetrics.forEach((feed) => {
console.log(`${feed.symbol}: ${prices[feed.feed_hash]}`);
});
});
// Alternative event format
surge.on("update", async (response: sb.SurgeUpdate) => {
const latency = Date.now() - response.data.source_ts_ms;
console.log(`${response.data.symbol}: ${response.data.price} (${latency}ms)`);
});
5. Convert to on-chain format
Solana: Convert streaming update to oracle quote instruction:
const crankIxs = response.toQuoteIx(queue.pubkey, keypair.publicKey);
// or
const [sigVerifyIx, oracleQuote] = response.toOracleQuoteIx();
EVM: Convert Surge data to EVM-compatible format:
import { EVMUtils } from "@switchboard-xyz/common";
const evmEncoded = EVMUtils.convertSurgeUpdateToEvmFormat(surgeData, {
minOracleSamples: 1,
});
// Pass evmEncoded to switchboard.updateFeeds()
6. Validate before use
Always apply:
- max staleness checks
- deviation sanity checks (especially for liquidation bots)
- optional multi-feed coherence checks (e.g., triangulation)
7. Reconnection strategy
- Implement heartbeat monitoring
- Auto-reconnect on disconnect with exponential backoff
- Track last-seen timestamp/slot for gap detection
Outputs
SurgeSubscriptionPlan:- feed list + symbols
- subscription tier
- code skeleton
- reconnection strategy
- validation policy
- mapping from streaming update → on-chain settlement format (per chain)
Module 8 — Unsigned Streaming (UI / Dashboard / Monitoring)
Goals
- Provide real-time price data for UIs, dashboards, and monitoring
- Chain-agnostic (works identically on Solana, EVM, Sui)
- NOT for on-chain use (unsigned data cannot be verified on-chain)
Overview
Unsigned streaming is a lightweight, chain-agnostic WebSocket feed for display purposes. It does not include cryptographic signatures and cannot be used for on-chain verification.
Procedure
Initialize for unsigned streaming
import * as sb from "@switchboard-xyz/on-demand";
// Initialize with keypair and connection (uses on-chain subscription)
const { keypair, connection, program } = await sb.AnchorUtils.loadEnv();
const surge = new sb.Surge({ connection, keypair });
// Unsigned streaming is available via the same Surge client
Note: Unsigned updates are provided for monitoring/UI purposes only and cannot be verified on-chain.
Handle unsigned updates
surge.on("unsignedPriceUpdate", (update: sb.UnsignedPriceUpdate) => {
const symbols = update.getSymbols();
const formattedPrices = update.getFormattedPrices();
// Display in UI / dashboard
});
Use cases
- Price tickers and dashboards
- Portfolio tracking UIs
- Monitoring / alerting systems
- Any display-only context where on-chain verification is not needed
Outputs
UnsignedStreamPlan: feed list, display integration code, refresh strategy
Module 9 — Randomness (Solana + EVM)
Goals
- Implement request + settle randomness flows correctly
- Avoid replay/double-settle
- Provide safe integration patterns for games, raffles, auctions, and DeFi mechanisms
Solana/SVM randomness (commit/reveal)
TypeScript client flow
Each step builds a tx via sb.asV0Tx({ connection, ixs, payer, signers, computeUnitPrice: 75_000, computeUnitLimitMultiple: 1.3 }) and sends it.
import * as sb from "@switchboard-xyz/on-demand";
const { keypair, connection, program } = await sb.AnchorUtils.loadEnv();
const queue = await setupQueue(program!);
const sbProgram = await loadSbProgram(program!.provider);
// 1. Create randomness account (one-time)
const rngKp = Keypair.generate();
const [randomness, createIx] = await sb.Randomness.create(sbProgram, rngKp, queue);
// → build tx with ixs: [createIx], signers: [keypair, rngKp]
// 2. Commit to randomness + your game action (same tx)
const commitIx = await randomness.commitIx(queue);
const gameActionIx = await createCoinFlipInstruction(myProgram, rngKp.publicKey, userGuess, ...);
// → build tx with ixs: [commitIx, gameActionIx], signers: [keypair]
// 3. Wait ~3s (oracle generates in TEE), then reveal + settle (same tx)
const revealIx = await randomness.revealIx();
const settleIx = await settleFlipInstruction(myProgram, ...);
// → build tx with ixs: [revealIx, settleIx], signers: [keypair]
Key patterns
- Bind randomness to a specific state transition (e.g., bet + commit in same tx)
- Always wait before reveal (oracle needs time to generate in TEE)
- Implement retry logic with exponential backoff for commit and reveal
- Reuse randomness accounts across games (persist keypair)
- Reject stale or replayed randomness
- Ensure sysvars are present in program accounts
Output
SolanaRandomnessPlan(accounts, instruction ordering, replay protections)
EVM randomness (request/resolve/settle)
TypeScript client flow
// Setup: ethers provider/wallet + CrossbarClient (same as Module 1 EVM)
const contract = new ethers.Contract(CONTRACT_ADDRESS, contractABI, wallet);
// 1. Request randomness (on-chain)
const tx1 = await contract.coinFlip({ value: ethers.parseEther("1") });
await tx1.wait();
// 2. Get randomness request data
const randomnessId = await contract.getWagerRandomnessId(wallet.address);
const wagerData = await contract.getWagerData(wallet.address);
// 3. Resolve off-chain via Crossbar
const network = await provider.getNetwork();
const { encoded } = await crossbar.resolveEVMRandomness({
chainId: Number(network.chainId),
randomnessId,
timestamp: Number(wagerData.rollTimestamp),
minStalenessSeconds: Number(wagerData.minSettlementDelay),
oracle: wagerData.oracle,
});
// 4. Settle on-chain
const tx2 = await contract.settleFlip(encoded);
const receipt = await tx2.wait();
Solidity contract pattern
// Request: generate unique randomnessId, call switchboard.createRandomness()
bytes32 randomnessId = keccak256(abi.encodePacked(msg.sender, block.timestamp));
switchboard.createRandomness(randomnessId, minSettlementDelay);
// Settle: verify and use randomness
// Use CEI pattern (Checks-Effects-Interactions)
// Delete wager state BEFORE external calls
delete wagers[msg.sender];
// Get randomness value
uint256 randomValue = switchboard.getRandomness(randomnessId);
bool won = (randomValue % 2 == 0);
Security patterns
- CEI (Checks-Effects-Interactions) to prevent reentrancy
- Enforce
minSettlementDelay(e.g., 5 seconds) - Use try/catch to avoid stuck pending states
- Generate unique
randomnessIdper request (prevent replay) - Validate oracle assignment matches expected oracle
…(truncated)