> ## Documentation Index
> Fetch the complete documentation index at: https://docs.range.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Onchain Risk Verifier

> Verify wallet and counterparty risk directly inside Solana programs using Range's Risk API and Switchboard On-Demand Oracles.

This guide covers how to integrate Range's Onchain Risk Verifier (ORV) into a
Solana program. The ORV brings real-world risk intelligence onchain - allowing
programs to reject high-risk or sanctioned addresses at the protocol level using
cryptographically signed, tamper-proof data.

<Info>
  **Prerequisites:** You need a [Risk API key](/introduction/getting-started), a
  Solana development environment (Anchor or Pinocchio), and familiarity with
  [Switchboard On-Demand Oracles](https://docs.switchboard.xyz/).
</Info>

***

## How It Works

The Onchain Risk Verifier uses
[Switchboard On-Demand Oracles](https://docs.switchboard.xyz/) to fetch signed
risk data from Range's Risk API and verify it inside your Solana program. No
persistent data feeds are required - quotes are requested on demand.

```mermaid theme={null}
sequenceDiagram
    participant C as Client (off-chain)
    participant O as Switchboard Oracle (TEE)
    participant S as Solana Program (onchain)

    C->>C: 1. Define OracleJob<br/>(Range API → parse → scale)
    C->>O: 2. Request signed quote
    O->>O: Fetch Range Risk API<br/>Sign result in TEE
    O->>C: Return signed quote
    C->>C: 3. Build transaction<br/>ix[0]: Ed25519 sig verify<br/>ix[1]: Program instruction
    C->>S: Submit transaction
    S->>S: 4. Verify Ed25519 signature<br/>Reconstruct feed → hash<br/>Compare feed IDs<br/>Check quote freshness<br/>Use verified risk score
```

### Security Model

* **Tamper-proof**: The program reconstructs the exact same feed definition
  onchain and hashes it. Any change to the URL, headers, or processing tasks
  changes the hash, causing verification to fail.
* **Cryptographic verification**: Switchboard oracles run in Trusted Execution
  Environments (TEEs) and sign results with Ed25519. The program verifies these
  signatures Onchain.
* **Freshness checks**: The program rejects quotes older than a configurable
  number of slots (e.g., 50 slots ≈ 20 seconds).
* **API keys stay off-chain**: Range API keys are injected as variable overrides
  at the oracle level - they never appear Onchain.

***

## Use Cases

| Scenario                | How ORV Helps                                                   |
| ----------------------- | --------------------------------------------------------------- |
| **Privacy protocols**   | Screen deposits to prevent illicit funds from entering the pool |
| **Lending protocols**   | Reject deposits or borrows from high-risk wallets               |
| **Treasury management** | Gate payouts to wallets below a risk threshold                  |
| **Governance**          | Only allow proposals or votes from verified-clean addresses     |
| **Bridges**             | Block transfers involving sanctioned or malicious addresses     |
| **DEX / AMM**           | Warn or block swaps with flagged counterparties                 |

***

## Step 1: Define the Oracle Job (TypeScript)

The Oracle Job tells Switchboard what data to fetch and how to process it. This
job calls Range's [Address Risk Score](/risk-api/risk/get-address-risk-score)
endpoint, parses the `riskScore`, scales it to 0–100, and bounds the result.

```typescript theme={null}
import { OracleJob } from '@switchboard-xyz/common';

function getRangeRiskScoreJob(address: string): OracleJob {
  return OracleJob.fromObject({
    tasks: [
      {
        httpTask: {
          url: `https://api.range.org/v1/risk/address?address=${address}&network=solana`,
          headers: [
            { key: 'accept', value: 'application/json' },
            // Resolved by Switchboard oracle via variable override - never Onchain
            { key: 'X-API-KEY', value: '${RANGE_API_KEY}' },
          ],
        },
      },
      // Extract the numeric risk score from the response
      { jsonParseTask: { path: '$.riskScore' } },
      // Scale from 0–10 to 0–100 for integer precision Onchain
      { multiplyTask: { scalar: 10 } },
      // Bound the result
      {
        boundTask: {
          lowerBoundValue: '0',
          onExceedsLowerBoundValue: '0',
          upperBoundValue: '100',
          onExceedsUpperBoundValue: '100',
        },
      },
    ],
  });
}
```

<Warning>
  The address is embedded directly in the Oracle Job URL. This is intentional,
  the Switchboard feed ID is a deterministic hash of the entire feed definition
  (including the URL). Your onchain program reconstructs the same URL using the
  `query_account` pubkey and hashes it to verify the oracle returned data for
  the correct address. A new Oracle Job must be created for each address you
  want to check.
</Warning>

<Note>
  The `${RANGE_API_KEY}` placeholder is resolved by the Switchboard oracle at
  runtime using variable overrides. Your API key is never exposed onchain or in
  transaction data.
</Note>

***

## Step 2: Request a Signed Quote (TypeScript)

Use the Switchboard SDK to request a signed quote and build the Ed25519
verification instruction.

```typescript theme={null}
import { PublicKey, Keypair, TransactionInstruction } from '@solana/web3.js';
import { CrossbarClient, IOracleFeed } from '@switchboard-xyz/common';
import * as sb from '@switchboard-xyz/on-demand';
import { getDefaultQueue } from '@switchboard-xyz/on-demand';

async function getOracleQuote(
  payer: Keypair,
  addressToCheck: string,
): Promise<{
  queueAccount: PublicKey;
  sigVerifyIx: TransactionInstruction;
}> {
  const { gateway, rpcUrl } = await sb.AnchorUtils.loadEnv();

  // Use devnet queue for testing, getDefaultQueue(rpcUrl) for mainnet
  const queue = await sb.getDefaultDevnetQueue(rpcUrl);
  const crossbar = CrossbarClient.default();

  // Build the feed specification with the target address baked in
  const feed: IOracleFeed = {
    name: 'Risk Score',
    jobs: [getRangeRiskScoreJob(addressToCheck)],
    minJobResponses: 1,
    minOracleSamples: 1,
    maxJobRangePct: 100,
  };

  // Request a signed quote from the oracle network
  const sigVerifyIx = await queue.fetchQuoteIx(crossbar, [feed], {
    variableOverrides: { RANGE_API_KEY: process.env.RANGE_API_KEY! },
    numSignatures: 1,
    instructionIdx: 0, // Ed25519 verify must be at this index in the transaction
  });

  return { queueAccount: queue.pubkey, sigVerifyIx };
}
```

***

## Step 3: Build and Send the Transaction (TypeScript)

Combine the signature verification instruction with your program instruction in
a single transaction.

<Note>
  The Switchboard SDK (`@switchboard-xyz/on-demand`) uses `@solana/web3.js` types internally.
  The transaction building code below uses web3.js for compatibility with the SDK.
</Note>

```typescript theme={null}
import {
  Connection,
  Transaction,
  sendAndConfirmTransaction,
  SYSVAR_CLOCK_PUBKEY,
  SYSVAR_SLOT_HASHES_PUBKEY,
  SYSVAR_INSTRUCTIONS_PUBKEY,
  PublicKey,
  Keypair,
  TransactionInstruction,
} from '@solana/web3.js';
import { createHash } from 'crypto';

// 8-byte Anchor discriminator for the instruction
const VERIFY_IX = createHash('sha256')
  .update('global:verify_risk_score_feed')
  .digest()
  .subarray(0, 8);

function buildProgramIx(
  programId: PublicKey,
  queue: PublicKey,
  queryAccount: PublicKey,
): TransactionInstruction {
  return new TransactionInstruction({
    programId,
    keys: [
      { pubkey: queue, isSigner: false, isWritable: false },
      { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
      { pubkey: SYSVAR_SLOT_HASHES_PUBKEY, isSigner: false, isWritable: false },
      { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false },
      { pubkey: queryAccount, isSigner: false, isWritable: false },
    ],
    data: VERIFY_IX,
  });
}

// Full transaction flow
async function verifyRiskScore(
  connection: Connection,
  payer: Keypair,
  programId: PublicKey,
  addressToCheck: PublicKey,
) {
  // Get the signed oracle quote for the target address
  const { queueAccount, sigVerifyIx } = await getOracleQuote(
    payer,
    addressToCheck.toBase58(),
  );

  // Build the program instruction
  const programIx = buildProgramIx(programId, queueAccount, addressToCheck);

  // Transaction: [sigVerify at index 0, program ix at index 1]
  const tx = new Transaction().add(sigVerifyIx, programIx);
  tx.feePayer = payer.publicKey;
  tx.recentBlockhash = (
    await connection.getLatestBlockhash('confirmed')
  ).blockhash;

  const signature = await sendAndConfirmTransaction(connection, tx, [payer]);
  console.log('Risk score verified onchain. Tx:', signature);
}
```

***

## Step 4: Verify Onchain (Rust)

The Solana program reconstructs the same feed definition, hashes it, and
compares against the oracle quote to ensure data integrity.

<CodeGroup>
  ```rust Anchor theme={null}
  #![allow(deprecated)]
  #![allow(unexpected_cfgs)]

  use anchor_lang::prelude::*;
  use anchor_lang::solana_program::hash::hash;
  use prost::Message;
  use switchboard_on_demand::{default_queue, QueueAccountData};
  use switchboard_on_demand::{Instructions, QuoteVerifier, SlotHashes};
  use switchboard_protos::oracle_job::oracle_job::http_task::Header;
  use switchboard_protos::oracle_job::oracle_job::multiply_task;
  use switchboard_protos::oracle_job::oracle_job::task;
  use switchboard_protos::oracle_job::oracle_job::{
      BoundTask, HttpTask, JsonParseTask, MultiplyTask, Task,
  };
  use switchboard_protos::{OracleFeed, OracleJob};

  declare_id!("YOUR_PROGRAM_ID");

  #[program]
  pub mod risk_oracle {
      use super::*;

      pub fn verify_risk_score_feed(ctx: Context<VerifyRiskScoreFeed>) -> Result<()> {
          let mut verifier = QuoteVerifier::new();
          let slot = Clock::get()?.slot;

          // Configure the verifier with required sysvars
          verifier
              .queue(ctx.accounts.queue.as_ref())
              .slothash_sysvar(ctx.accounts.slothashes.as_ref())
              .ix_sysvar(ctx.accounts.instructions.as_ref())
              .clock_slot(slot);

          // Verify the Ed25519 signature at instruction index 0
          let quote = verifier.verify_instruction_at(0).unwrap();
          let quote_slot = quote.slot();

          // Ensure the quote is fresh (within 50 slots ≈ 20 seconds)
          if slot.saturating_sub(quote_slot) > 50 {
              return Err(ErrorCode::StaleQuote.into());
          }

          // Extract the verified feed data
          let feeds = quote.feeds();
          require!(!feeds.is_empty(), ErrorCode::NoOracleFeeds);
          let feed = &feeds[0];

          // Reconstruct the feed ID onchain and compare
          let expected_feed_id = create_risk_score_feed_id(
              &ctx.accounts.query_account.key()
          )?;
          require!(
              *feed.feed_id() == expected_feed_id,
              ErrorCode::FeedMismatch
          );

          // The risk score is now verified - use it in your program logic
          msg!(
              "Verified risk score feed! Value: {}",
              feed.value().to_string().as_str()
          );

          Ok(())
      }
  }

  // Account structure for the verify_risk_score_feed instruction
  #[derive(Accounts)]
  pub struct VerifyRiskScoreFeed<'info> {
      #[account(address = default_queue())]
      pub queue: AccountLoader<'info, QueueAccountData>,
      pub clock: Sysvar<'info, Clock>,
      pub slothashes: Sysvar<'info, SlotHashes>,
      pub instructions: Sysvar<'info, Instructions>,
      /// CHECK: Only the pubkey is needed to reconstruct the feed ID
      pub query_account: UncheckedAccount<'info>,
  }

  #[error_code]
  pub enum ErrorCode {
      #[msg("No oracle feeds available")]
      NoOracleFeeds,
      #[msg("Feed hash mismatch - oracle feed does not match expected configuration")]
      FeedMismatch,
      #[msg("Stale quote - the quote is too old")]
      StaleQuote,
      #[msg("Risk score exceeds allowed threshold")]
      RiskTooHigh,
  }

  // Helper function to reconstruct the feed ID onchain
  fn create_risk_score_feed_id(query_pubkey: &Pubkey) -> Result<[u8; 32]> {
      let addr = bs58::encode(query_pubkey).into_string();
      let url = format!(
          "https://api.range.org/v1/risk/address?address={}&network=solana",
          addr
      );

      let feed = OracleFeed {
          name: Some("Risk Score".to_string()),
          jobs: vec![OracleJob {
              tasks: vec![
                  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()
                      })),
                  },
                  Task {
                      task: Some(task::Task::JsonParseTask(JsonParseTask {
                          path: Some("$.riskScore".to_string()),
                          ..Default::default()
                      })),
                  },
                  Task {
                      task: Some(task::Task::MultiplyTask(MultiplyTask {
                          multiple: Some(multiply_task::Multiple::Scalar(10.0)),
                      })),
                  },
                  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()
                      })),
                  },
              ],
              weight: None,
          }],
          min_job_responses: Some(1),
          min_oracle_samples: Some(1),
          max_job_range_pct: Some(100),
      };

      // Hash the protobuf-encoded feed to get the deterministic feed ID
      let bytes = OracleFeed::encode_length_delimited_to_vec(&feed);
      Ok(hash(&bytes).to_bytes())
  }
  ```

  ```rust Pinocchio theme={null}
  #![no_std]
  #![allow(unexpected_cfgs)]

  extern crate alloc;
  use alloc::{format, string::ToString, vec};

  use pinocchio::{
      account_info::AccountInfo, program_error::ProgramError,
      pubkey::Pubkey, ProgramResult,
  };
  use pinocchio_log::log;
  use prost::Message;
  use sha2::{Digest, Sha256};
  use switchboard_on_demand::{get_slot, QuoteVerifier};
  use switchboard_protos::{
      oracle_job::oracle_job::{
          http_task::Header, multiply_task, task, BoundTask, HttpTask,
          JsonParseTask, MultiplyTask, Task,
      },
      OracleFeed, OracleJob,
  };

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

      // Build the feed proto on-chain (must match client exactly)
      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
      );

      let feed = OracleFeed {
          name: Some("Risk Score".to_string()),
          jobs: vec![OracleJob {
              tasks: vec![
                  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()
                  }))},
                  Task { task: Some(task::Task::JsonParseTask(JsonParseTask {
                      path: Some("$.riskScore".to_string()),
                      ..Default::default()
                  }))},
                  Task { task: Some(task::Task::MultiplyTask(MultiplyTask {
                      multiple: Some(multiply_task::Multiple::Scalar(10.0)),
                  }))},
                  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()
                  }))},
              ],
              weight: None,
          }],
          min_job_responses: Some(1),
          min_oracle_samples: Some(1),
          max_job_range_pct: Some(100),
      };

      // Hash the protobuf bytes to derive feed ID
      let bytes = OracleFeed::encode_length_delimited_to_vec(&feed);
      let mut hasher = Sha256::new();
      hasher.update(&bytes);
      let derived_feed_hash: [u8; 32] = hasher.finalize().into();

      // Verify the quote
      let slot = get_slot(clock_sysvar);
      let mut quote_verifier = QuoteVerifier::new();
      let quote_data = quote_verifier
          .slothash_sysvar(slothashes_sysvar)
          .ix_sysvar(instructions_sysvar)
          .clock_slot(slot)
          .queue(queue)
          .max_age(30)
          .verify_instruction_at(0)
          .map_err(|_| ProgramError::Custom(0))?;

      // Check freshness
      if slot.saturating_sub(quote_data.slot()) > 50 {
          return Err(ProgramError::Custom(1)); // StaleQuote
      }

      // Match feed ID and log the verified risk score
      for feed_info in quote_data.feeds().iter() {
          if feed_info.feed_id() == &derived_feed_hash {
              log!("Risk Score {}", feed_info.value().to_string().as_str());
              return Ok(());
          }
      }

      Err(ProgramError::Custom(2)) // FeedIdMismatch
  }
  ```
</CodeGroup>

## Step 5: Run the Example

Range provides a complete working example with both Anchor and Pinocchio
implementations.

```bash theme={null}
# Clone the example repository
git clone https://github.com/rangesecurity/oracle-example.git
cd oracle-example/anchor/client

# Install dependencies
npm install

# Set your Range API key
export RANGE_API_KEY="your_api_key_here"

# Set the RPC URL (defaults to http://127.0.0.1:8899 if not set)
export RPC_URL="https://api.devnet.solana.com"

# Set the path to your devnet wallet keypair (defaults to ../keypair.json)
export DEV_WALLET_KEYPAIR_PATH="/path/to/your/keypair.json"

# Run the test
npm test
```

<Note>
  The test requires a funded Solana devnet wallet. If you don't have one,
  generate a keypair with `solana-keygen new -o keypair.json` and airdrop devnet
  SOL with `solana airdrop 2 --keypair keypair.json --url devnet`.
</Note>

Expected output:

```
Program log: Verified risk score feed! Value: 100
```

<Tip>
  The repository includes both an
  [Anchor](https://github.com/rangesecurity/oracle-example/tree/master/anchor)
  and
  [Pinocchio](https://github.com/rangesecurity/oracle-example/tree/master/pinocchio)
  implementation. Both are functionally identical - choose whichever framework
  your project uses.
</Tip>

***

## Customizing for Your Program

### Adjusting the Risk Threshold

Modify the onchain program to enforce your risk policy:

```rust theme={null}
let risk_score = feed.value(); // 0–100 scale (original 0–10 × 10)

// Block wallets with risk score > 7 (70 on the 0–100 scale)
if risk_score > 70.into() {
    return Err(ErrorCode::RiskTooHigh.into());
}
```

### Checking Different Networks

Update the URL in both the TypeScript OracleJob and the Rust feed reconstruction
to query different networks:

```rust theme={null}
let url = format!(
    "https://api.range.org/v1/risk/address?address={}&network={}",
    addr, network
);
```

<Warning>
  The feed definition must be **identical** in both the client-side OracleJob
  and the onchain feed reconstruction. Any difference - even in field ordering
  or default values - will change the hash and cause verification to fail.
</Warning>

### Production Considerations

* **Increase `numSignatures`** - Use more than 1 signature for production to
  increase consensus requirements.
* **Tune staleness** - The example uses 50 slots (\~20 seconds). Tighten this for
  time-sensitive operations.
* **Use mainnet queue** - Switch from `getDefaultDevnetQueue` to
  `getDefaultQueue` for production deployments.
* **Handle errors gracefully** - Decide whether a failed risk check should block
  the transaction or allow it with a warning flag.

***

## Cross-Chain Support

The Onchain Risk Verifier is currently implemented for Solana. The same
architecture could be adapted for EVM (Solidity) and CosmWasm smart contracts,
the core pattern of oracle-fetched signed data with onchain signature and feed
integrity verification is framework-agnostic. If you're interested in an EVM or
CosmWasm implementation, [get in touch](https://www.range.org/get-in-touch).

***

## What's Next

<CardGroup cols={2}>
  <Card title="Address Risk Score" icon="location-dot" href="/risk-api/risk/get-address-risk-score">
    Full endpoint reference for the Risk API endpoint used by the oracle job.
  </Card>

  <Card title="Switchboard Docs" icon="toggle-on" href="https://docs.switchboard.xyz/">
    Switchboard On-Demand Oracle documentation and SDK reference.
  </Card>

  <Card title="Example Repository" icon="github" href="https://github.com/rangesecurity/oracle-example">
    Complete working implementation with Anchor and Pinocchio frameworks.
  </Card>

  <Card title="Wallet Integration" icon="wallet" href="/risk-api/guides/wallet-dapp-integration">
    For off-chain risk screening in wallets and dApps.
  </Card>
</CardGroup>
