Security

Integrate Range's Onchain Risk Verifier into your Solana Program

You can now make your protocol risk-aware with auditable, signed data - powered by Range's Risk API and Switchboard On-Demand Oracles

Jose S, Range

·

Oct 17, 2025

Integrate Range's Onchain Risk Verifier into your Solana Program
Integrate Range's Onchain Risk Verifier into your Solana Program

Real-time, onchain risk verification is now possible using Range's Risk API and Switchboard's On-Demand Oracles. This allows Solana programs to reject high-risk or sanctioned addresses at the protocol level, gate treasury actions or governance flows based on counterparty risk, or any number of other risk-based logic. Learn more here.

In this guide, we'll walk through exactly how to implement it using the Switchboard SDK and a Solana program, including how to fetch signed quotes off-chain and verify them onchain.

By the end, you'll be able to:

  • Request a signed risk quote for a wallet address using Switchboard

  • Verify the result in your Solana program

  • Act on that result to block, allow, or log based on risk thresholds

While our Onchain Risk Verifier is currently designed for Solana programs, the same logic can work for all EVM or CosmWasm smart contracts, allowing onchain risk analysis no matter which ecosystem you're in. Get in touch with us to discuss your integration.

Onchain Risk API via Switchboard

Because of the synchronous nature of the Solana Virtual Machine (SVM), bringing off-chain data on-chain isn't as straightforward as it might seem. On-chain programs execute deterministically, they can't just “call an API” during runtime. Any external dependency would break consensus.

Fortunately, that's exactly the kind of challenge oracles were built to solve. And thanks to Switchboard, the everything oracle, we now have a way to bridge real-world intelligence, like Range's Risk API, directly into Solana programs.

What are oracles?

In simple terms, oracles are like data bridges between the blockchain and the outside world. They fetch information from external sources, such as APIs and feed it securely to on-chain programs.

Traditional oracles work through data feeds: continuous updates that publish values like prices or weather data at regular intervals. Each feed is verified by multiple nodes (called guardians) to ensure accuracy, and those nodes sign the data cryptographically so that smart contracts can trust it.

Switchboard takes this a step further by combining its decentralized oracle network with Trusted Execution Environments (TEEs) - secure enclaves that guarantee each oracle runs verified code. This means every piece of data delivered, including a Range Risk Score, comes with hardware-backed proof of integrity.

Finally, Switchboard On-Demand Oracles, the first of their kind, allow data feeds to be created as required. This not only eliminates the cost of constant data streams but unlocks a hidden superpower, the ability to securely call any API on-chain, including the Range Risk API.

These on-demand oracles are the innovation that elevates the Risk API to the on-chain world, making real-time, verifiable risk assessment a native feature of decentralized protocols.

When a program requests a quote, for example, a risk score for a wallet address, the following happens:

  1. The client builds a small “feed schema” describing what data it wants (e.g., an HTTP call to Range's Risk API).

  2. Switchboard's oracles execute that job inside secure enclaves, guaranteeing the response is authentic and tamper-proof.

  3. The result is signed by a quorum of trusted oracles.

  4. Those signatures are verified on-chain by your Solana program using Switchboard's Quote Verifier.

This approach offers significant advantages: as there is no need for a constant data stream, each response is signed and verified on-chain and it can call any public or authenticated API, including Range’s Risk API.

Benefits and trade-offs

Every data point is cryptographically signed by Switchboard oracles and verified on-chain, ensuring full transparency and trust. With the combined forces of Range and Switchboard, verifiable on-chain risk intelligence is no longer a concept, it’s a reality.

Bringing the Range Risk API on-chain via Switchboard On-Demand unlocks powerful new possibilities for decentralized applications. However, this just-in-time oracle model introduces a few practical trade-offs that developers should be aware of.

Each on-chain verification depends on an off-chain “quote request” first. This means that the client, backend, or relayer must fetch a signed quote before submitting the on-chain transaction.

While this adds a few seconds of latency (typically 2–3 seconds) and slightly increases transaction size, it’s a reasonable cost for achieving fully verifiable, tamper-proof data flow between off-chain intelligence and on-chain logic.

Building an Onchain Risk Verifier

Now that we understand how Switchboard On-Demand connects the Range Risk API to the Solana blockchain, let’s see how it works in practice.

In this section we will be making a real-time risk score off-chain request and verifying it on-chain. By the end, you’ll know how to:

  1. Build and request a signed data quote for the Range API using the Switchoard SDK.

  2. Verify that quote inside a Pinocchio program, ensuring integrity and freshness.

The full source code and an anchor example are both availabe at the Risk API Oracle Example.


Off-Chain SetUp

At the core of every oracle request we have the OracleJobs. These jobs are structured task schemas that describe what the oracles need to run. I In our case, the job must make an HTTP request to the Range Risk API and parse the response.

Why do we need to parse the response?

The Risk API’s response includes multiple fields that provide context around the score:

{
  "riskScore": 10,
  "riskLevel": "CRITICAL RISK (Directly malicious)",
  "numHops": 0,
  "maliciousAddressesFound": [
    {
      "address": "AuZrspySopxfZUiXY6YxDyfS211KvXLe197kj3M2cLpq",
      "distance": 0,
      "name_tag": "Layering, Swapping",
      "entity": null,
      "category": "hack_funds"
    }
  ],
  "reasoning": "Address is directly flagged for malicious activity."
}

However, the oracle’s response format is optimized for compact numerical output, typically a single value. For on-chain protocols, the key piece of information is simply the riskScore. We use a JSON Parse Task to extract that value, ensuring that only the relevant number is returned.

In our example, we also use a Multiply Task to demonstrate transformation, scaling the score by 10 to represent a 0–100 percentage.

Finally, a Bound Task ensures the value always remains between 0 and 100 (for safety).

Let’s build a function that defines this Risk API OracleJob in TypeScript:

export function getRangeRiskScoreJob(): OracleJob {
  const job = OracleJob.fromObject({
    tasks: [
      {
        httpTask: {
          url: "<https://api.range.org/v1/risk/address?address=5PAhQiYdLBd6SVdjzBQDxUAEFyDdF5ExNPQfcscnPRj5&network=solana>",
          headers: [
            { key: "accept", value: "application/json" },
            { key: "X-API-KEY", value: "${RANGE_API_KEY}" },
          ],
        },
      },
      // Only accept numeric riskScore >= 0; null => no match => failure so no Risk
      { jsonParseTask: { path: "$.riskScore" } },
      { multiplyTask: { scalar: 10 } }, // 0–10 => 0–100
      {
        boundTask: {
          lowerBoundValue: "0",
          onExceedsLowerBoundValue: "0",
          upperBoundValue: "100",
          onExceedsUpperBoundValue: "100",
        },
      },
    ],
  });
  return job;
}

Notice the ${RANGE_API_KEY} placeholder in the HTTP task headers.

Switchboard’s variable substitution system ensures API keys and secrets remain off-chain, passed securely as variable overrides into the oracles’ Trusted Execution Environments (TEEs), never exposed publicly.

The off-chain request step can run anywhere, a backend, relayer, or dApp client, as long as it builds the OracleFeed and sends the quote request to Switchboard. The Solana program then verifies all signatures and freshness before trusting the data.

Ed25519 signature verification

In this step we perform the “heavy lifting” of the off-chain request and return a sig-verified quote in four steps:

  1. Choose the queue (devnet in this example)

  2. Initialize a CrossbarClient

  3. Build your IOracleFeed

  4. Request a quote with ****queue.fetchQuoteIx, passing [feed] and variableOverrides so oracles can resolve ${RANGE_API_KEY}

Step 1 - Get the queue

Fetch the queue for your network and keep its on-chain account — you’ll pass it to your program later.

export async function getOracleJobSignature(payer: Keypair): Promise<{ queue_account: PublicKey; sigVerifyIx: TransactionInstruction }> {
  const { gateway, rpcUrl } = await sb.AnchorUtils.loadEnv();

  // Get the queue for the network you're deploying on
  //
  // Devnet queue (use `getDefaultQueue(rpcUrl)` for mainnet)
  let queue = await sb.getDefaultDevnetQueue(rpcUrl);
  let queue_account = queue.pubkey;

Step 2 — CrossbarClient

CrossbarClient is the Switchboard gateway the SDK uses for routing/simulation.

  let crossbar_client = CrossbarClient.default();

Step 3 — Build IOracleFeed

Configure sampling/aggregation:

  • the number of oracles to sample before returning a result

  • minimum number of jobs required to succeed in order to produce a result

  • the maximum allowed percentage deviation between job responses

For risk scores (address-bound, not highly volatile), 1/1/100 is fine for demos — tune for production.

 const feed: IOracleFeed = {
    name: "Risk Score",
    jobs: [getRangeRiskScoreJob()],
    minJobResponses: 1,
    minOracleSamples: 1,
    maxJobRangePct: 100,
  };

Step 4 - fetchQuoteIx

Create the Ed25519 signature verification instruction for your feed. This embeds receipts that your on-chain QuoteVerifier will parse.

 const sigVerifyIx = await queue.fetchQuoteIx(
    crossbar_client,
    [feed], // pass the feed directly (no pre-storage needed)
    {
      variableOverrides: { RANGE_API_KEY: process.env.RANGE_API_KEY! },
      numSignatures: 1,
      instructionIdx: 0, // we'll put this ix at index 0 in the tx
    }
  );
  return { queue_account, sigVerifyIx };
}

Notes:

  • variableOverrides are passed to oracles so ${RANGE_API_KEY} can be injected into our HTTP task at runtime (without exposing secrets on-chain).

  • instructionIdx tells the Ed25519 program where to put the sigVerifyIx in the tx

  • numSignatures controls consensus level; keep >1 for production critical paths.

IX Building

There is one last step left in our example, to build the instruction that will call our on-chain program.

Our program expects 5 accounts:

  • queue account (to verify the quote)

  • sysvars (clock, slot hashes, instructions).

  • query_account (the address you want to fetch the risk score for)

No data or descriminator needed for this example.

  export function buildGetRiskScoreIx(queue: PublicKey, query_account: PublicKey): TransactionInstruction {

    return new TransactionInstruction({
      programId: PROGRAM_ID,
      keys: [
        // payer_info
        { pubkey: queue, isSigner: false, isWritable: false }, // queue
        { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, // clock_sysvar_info
        { pubkey: SYSVAR_SLOT_HASHES_PUBKEY, isSigner: false, isWritable: false }, // slothashes_sysvar_info
        { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }, // instructions_sysvar_info
        { pubkey: query_account, isSigner: false, isWritable: false }, // query_account_info

      ],
      data: Buffer.alloc(0), // no data to send
    });
  }

Putting it together (test flow)

Load env + keypair, build the instructions, and send the transaction.

// The deployed Pinocchio program ID (devnet).
export const PROGRAM_ID = new PublicKey("CR8mpiY9eEbNkU8w4VJkGB4gzEnozp739jwvTiXRmACc");

// Load a Keypair from a JSON file
function loadKeypairFromFile(p: string): Keypair {
  const raw = fs.readFileSync(p, "utf8");
  const secret = Uint8Array.from(JSON.parse(raw));
  return Keypair.fromSecretKey(secret);
}

const RPC_URL = process.env.RPC_URL ?? "<http://127.0.0.1:8899>";
const DEV_WALLET_KEYPAIR_PATH = process.env.DEV_WALLET_KEYPAIR_PATH ?? "./../keypair.json";

describe("Initialize Oracle Example", function () {
  this.timeout(10_000); // Responses can take from 2 to 5 seconds

  const connection = new Connection(RPC_URL, "confirmed");

  const DEV_WALLET = loadKeypairFromFile(
    path.resolve(DEV_WALLET_KEYPAIR_PATH)
  );

  it("initializes the Oracle and call the Oracle Program", async () => {

    let { queue_account, sigVerifyIx } = await getOracleJobSignature(DEV_WALLET);

    // Choose the account you want to check (becomes the address query param
    // in the on-chain HTTP task definition). Your program will reconstruct
    // the same feed proto using this pubkey to ensure the hash matches.
    const query_account = new PublicKey("5PAhQiYdLBd6SVdjzBQDxUAEFyDdF5ExNPQfcscnPRj5");

    // Build the target program instruction
    const ix = buildGetRiskScoreIx(queue_account, query_account);

    // Create the transaction and add both instructions
    const tx = new Transaction().add(sigVerifyIx, ix);

    // Fetch the latest blockhash
    const latest = await connection.getLatestBlockhash({ commitment: "confirmed" });

    // Set fee payer + recent blockhash 
    tx.feePayer = DEV_WALLET.publicKey;
    tx.recentBlockhash = latest.blockhash;

    // Send the transaction
    const transactionSignature = await sendAndConfirmTransaction(
      connection,
      tx,
      [DEV_WALLET] // signer
    );
    console.log("Fetched RiskScore Via Oracle. Tx:", transactionSignature);
    // Some basic assertion to ensure it went through can be added here
  });
});

Now before we test it we need a program don’t we?


Onchain Setup

To use the Risk API Oracle on-chain we will:

  • Destructure the accounts

  • Recreate the feed proto on-chain (with the exact same tasks/order/options used off-chain)

  • Verify quote signatures and freshness

  • Confirm the verified feed matches our derived feed id and then extract its value

Each of these steps ensures your program can independently verify that the off-chain data truly came from the intended feed, at the expected time, and with valid oracle signatures, without ever needing to trust a centralized source.

Step 1 - Account Destructuring

We start by destructuring the accounts passed into the program. The order of these accounts must match exactly the keys array defined in the client.

fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    // Destructure accounts
    let [queue, clock_sysvar, slothashes_sysvar, instructions_sysvar, query_account]: &[AccountInfo;
         5] = accounts
        .try_into()
        .map_err(|_| ProgramError::NotEnoughAccountKeys)?;

Tip: Keep the order consistent with your client keys array, or this will fail at runtime.

Step 2 - Derive Feed Hash

We recreate the exact same OracleFeed proto ****as defined off-chain, embedding the target address into the HTTP task. This guarantees that the feed definition and thus its SHA-256 hash matches byte-for-byte.

let addr_b58 = bs58::encode(query_account.key()).into_string();
let url = format!(
    "<https://api.range.org/v1/risk/address?address={}&network=solana>",
    addr_b58
);

We then rebuild each Oracle task exactly as in the client.

// Build the HTTP task: GET the Range endpoint with headers.
let http_task = Task {
	task: Some(task::Task::HttpTask(HttpTask {
	    url: Some(url),
	    headers: [
		Header {
		    key: Some("accept".to_string()),
		    value: Some("application/json".to_string()),
		},
		Header {
		    key: Some("X-API-KEY".to_string()),
		    value: Some("${RANGE_API_KEY}".to_string()),
		},
	    ]
	    .into(),
	    ..Default::default()
	})),
};

// Parse the JSON response at the path `$.riskScore`.
let json_parse_task = Task {
	task: Some(task::Task::JsonParseTask(JsonParseTask {
	    path: Some("$.riskScore".to_string()),
	    // aggregation_method: Some(1), // optional; not needed for single value
	    ..Default::default()
	})),
};

// Multiply the risk score (0–10) by 10 to get a 0–100 range.
//
let multiply_task = Task {
	task: Some(task::Task::MultiplyTask(MultiplyTask {
	    multiple: Some(multiply_task::Multiple::Scalar(10.0)), // 0–10 => 0–100
	})),
};

// Bound the result to [0,100]. If out of bounds, set to nearest bound.
let bound_task = Task {
	task: Some(task::Task::BoundTask(BoundTask {
	    lower_bound_value: Some("0".into()),
	    upper_bound_value: Some("100".into()),
	    on_exceeds_lower_bound_value: Some("0".into()),
	    on_exceeds_upper_bound_value: Some("100".into()),
	    ..Default::default()
	})),
};

// Create the OracleJob with tasks in order.
let oracle_job = oracle::OracleJob {
	tasks: vec![http_task, json_parse_task, multiply_task, bound_task],
	weight: None, // keep None to match client canonicalization; using Some(1) changes hash
};

Gotcha: Optional fields (like weight) must match the client. Changing defaults → different bytes → different hash

Once the tasks are created we make the OracleFeed and derive the hash via Sha256.

    // Create the OracleFeed with one job.
    //
    let feed = OracleFeed {
        name: Some("Risk Score".to_string()),
        jobs: vec![oracle_job],
        min_job_responses: Some(1),
        min_oracle_samples: Some(1),
        max_job_range_pct: Some(100),
    };

    // Encode to length-delimited protobuf bytes
    let bytes = OracleFeed::encode_length_delimited_to_vec(&feed);

    // Hash to 32-byte feed id (Switchboard uses SHA-256 of the length-delimited bytes)
    let mut hasher = Sha256::new();
    hasher.update(&bytes);
    let derived_feed_hash: [u8; 32] = hasher.finalize().into();

Step 3- Verify The Quote

Next, we use QuoteVerifier to confirm that the Ed25519 instruction (at index 0) is valid and recent. This step ensures that the signed quote wasn’t reused from an old block and that the oracles indeed signed the current feed.

// - 'get_slot' reads current slot from Clock sysvar (Pinocchio-friendly).
    let slot = get_slot(clock_sysvar);

    // - `QuoteVerifier` verifies the Ed25519 signature ix and decodes the quote.
    let mut quote_verifier = QuoteVerifier::new();
    let quote_data = quote_verifier
        .slothash_sysvar(slothashes_sysvar) // Sets the slot hash sysvar account for verification.
        .ix_sysvar(instructions_sysvar) // Sets the instructions sysvar account for verification.
        .clock_slot(slot) // Sets the current slot for freshness verification.
        .queue(queue) // Sets the oracle queue account.
        .max_age(30) // Sets the maximum age of the quote in seconds.
        .verify_instruction_at(0) // Verifies the quote is at instruction index 0.
        .map_err(|_| OracleError::InstructionQuoteMissing)?;

    let quote_slot = quote_data.slot();

    // Ensure the quote is recent enough (within 50 slots).
    //
    if slot.saturating_sub(quote_slot) > 50 {
        // Extra check: ensure the quote is fresh enough (within 30 slots).
        log!(
            "Quote too old. Current slot: {}, quote slot: {}",
            slot,
            quote_slot
        );
        return Err(OracleError::StaleQuote.into());
    }

Why this matters: These freshness checks prevent replay attacks — ensuring your program only uses data signed within the last few slots.

Step 4 - Feed Match & Result

Finally, we confirm that one of the verified feeds matches our derived feed hash.

If it does, we can safely read the risk score value and act on it.

   let mut matched = false;
   for feed_info in quote_data.feeds().iter() {
       if feed_info.feed_id() == &derived_feed_hash {
           matched = true;
           log!("Risk Score {}", feed_info.value().to_string().as_str());
       }
   }

   if !matched {
       return Err(OracleError::FeedIdMismatch.into());
   }

    Ok(())
}

Once verified, the feed’s value is cryptographically guaranteed to originate from the intended Range API endpoint, processed and signed by trusted Switchboard oracles.

Our program is now ready for deployment and testing. You can find the complete code (including imports and custom error definitions) in the Risk API Oracle Example.

Don’t forget to update the client’s PROGRAM_ID to match your deployed program id.

When you run the test with the sample address, you should see a a transaction log like:

Program log: Risk Score 100

Recap

Let’s recap what we learned.

We started with a simple question: “How can we bring real-world risk intelligence onchain, without compromising decentralization?”.

By the end we answer it. By combining Range's Risk API with Switchboard On-Demand Oracle.

We first explored Switchboard and learned how oracles act as the secure bridge between off-chain APIs and on-chain programs. Then we introduced the Switchboard On-Demand feature that creates data only when requested, eliminating constant streaming and allowing users to make feeds on the go.

After grasping the concepts, we walked through the full flow, by concept and implementation:

  1. We build an off-chain (backend, relayer, or dApp) feed schema describing what data to fetch (e.g., the Range Risk Score).

  2. Switchboard oracles execute the job inside secure enclaves, sign the result, and return a quote.

  3. Our Solana program verifies those signatures on-chain using QuoteVerifier, guaranteeing authenticity and freshness.

We also learned that this process adds a short pre-transaction latency (~2–3 seconds) for fetching the quote. A small cost for achieving real-time, tamper-proof risk checks directly in the program logic.

In summary, we’ve taken an off-chain API and turned it into verifiable on-chain intelligence, something that was impossible until now.


Build with Range

The proof is here: verifiable, real-time risk intelligence can now live directly on-chain.

If you're building a protocol, wallet, or compliance layer that needs auditable trust signals, this integration is for you.

Range's Risk API delivers the intelligence. Switchboard makes it verifiable. Together, they make crypto safer.

Here's how to get started:

About Range

Range is the leading blockchain security and intelligence platform, operating across ecosystems. We work with teams like the Solana Foundation, Circle, dYdX, and Osmosis to deliver secure, cross-chain infrastructure. Our products include the industry’s first Cross-Chain Explorer – tracking activity across 105+ chains, bridges and interoperability protocols – as well as real-time monitoring, alerting, and forensic tools used by developers, security teams, and protocols alike.

From the USDC Explorer powering Circle’s CCTP to the Solana Transaction Security Standard adopted by Squads Protocol, Range’s tools secure over $30B in onchain assets. We also provide IBC Rate Limit contracts on Cosmos and Range Trail, our cross-chain forensics engine, to support investigations and incident response across networks.

The stablecoin risk and intelligence platform

Helping the best teams build and use DeFi protocols, blockchains, rollups, and cross-chain bridges with peace of mind.

Book an intro call

Skip the form. Choose a day and time that suits you to book an exploratory call or demo with our team.

Get in touch

Areas of interest*

The stablecoin risk and intelligence platform

Helping the best teams build and use DeFi protocols, blockchains, rollups, and cross-chain bridges with peace of mind.

Book an intro call

Skip the form. Choose a day and time that suits you to book an exploratory call or demo with our team.

Get in touch

Areas of interest*

The stablecoin risk and intelligence platform

Helping the best teams build and use DeFi protocols, blockchains, rollups, and cross-chain bridges with peace of mind.

Book an intro call

Skip the form. Choose a day and time that suits you to book an exploratory call or demo with our team.

Get in touch

Areas of interest*

The blockchain security and intelligence platform. Featuring a comprehensive security and risk management suite powered by machine learning and security expertise.

Resources

The blockchain security and intelligence platform. Featuring a comprehensive security and risk management suite powered by machine learning and security expertise.

Resources

The blockchain security and intelligence platform. Featuring a comprehensive security and risk management suite powered by machine learning and security expertise.

Resources