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

# SigVerify Onchain Attestation

> Verify wallet risk directly inside Solana programs using backend-signed Ed25519 attestations, no oracles required.

This guide covers how to integrate Range's SigVerify pattern into a Solana
program. SigVerify is a lightweight alternative to the
[Onchain Risk Verifier](/risk-api/guides/onchain-risk-verifier), instead of
relying on oracle networks, your backend calls the Range API, signs an
attestation, and your onchain program verifies the signature directly using
Ed25519.

<Info>
  **Prerequisites:** You need a [Risk API key](/introduction/getting-started), a
  Solana development environment (Anchor), and a backend service that holds an
  Ed25519 keypair.
</Info>

***

## How It Works

Your backend screens the user's wallet via the Range API, then signs a
time-stamped attestation message. The user submits this attestation in a Solana
transaction, and the onchain program verifies the signature, signer identity,
and freshness, all without any oracle dependency.

```mermaid theme={null}
sequenceDiagram
    participant U as User
    participant B as Backend
    participant R as Range API
    participant S as Solana Program

    U->>B: 1. Request access
    B->>R: 2. Screen wallet<br/>GET /v1/risk/address
    R-->>B: Risk score + level
    B->>B: 3. Check risk threshold<br/>Generate riskCallId
    B->>B: 4. Sign attestation<br/>"{timestamp}_{pubkey}_{riskCallId}"
    B-->>U: Return signature + message
    U->>S: 5. Submit transaction<br/>with signature + message
    S->>S: 6. Verify Ed25519 signature<br/>Match signer to message pubkey<br/>Check timestamp freshness<br/>Grant access
```

### How It Compares to the Oracle Approach

|                  | SigVerify                     | [Onchain Risk Verifier](/risk-api/guides/onchain-risk-verifier) |
| ---------------- | ----------------------------- | --------------------------------------------------------------- |
| **Trust model**  | Trust your backend signer     | Trust Switchboard TEE oracles                                   |
| **Dependencies** | None (Ed25519 only)           | Switchboard SDK + oracle network                                |
| **Latency**      | Single API call + sign        | Oracle request + quote fetch                                    |
| **Cost**         | \~40k CUs                     | \~20k Cus                                                       |
| **Best for**     | Apps with an existing backend | Fully decentralized protocols                                   |

### Security Model

* **Ed25519 signature verification**: The onchain program verifies the backend's
  signature using Ed25519. This example uses the
  [`brine-ed25519`](https://crates.io/crates/brine-ed25519) crate for in-program
  verification. Alternatively, you can use Solana's native
  [Ed25519 SigVerify precompile](https://solana.com/docs/core/programs#ed25519-program)
  , this requires an additional instruction and onchain introspection to
  validate the signature (similar to what Switchboard does under the hood), but
  avoids depending on a third-party crate. Either way, forged attestations are
  rejected.
* **Signer binding**: The message embeds the user's pubkey. The program verifies
  it matches the transaction signer, preventing replay attacks.
* **Freshness check**: Attestations expire after 60 seconds (configurable). The
  program compares the message timestamp against the onchain clock.
* **Audit trail**: Each attestation includes a unique `riskCallId` that links
  the onchain verification to the original API call.
* **API keys stay off-chain**: The Range API key is only used by your backend,
  it never appears in transaction data.

***

## Use Cases

| Scenario              | How SigVerify Helps                                             |
| --------------------- | --------------------------------------------------------------- |
| **Privacy protocols** | Screen deposits to prevent illicit funds from entering the pool |
| **Token launches**    | Gate participation to wallets below a risk threshold            |
| **Airdrops**          | Screen recipients before distributing tokens                    |
| **DAO treasuries**    | Require risk attestation before withdrawals                     |
| **NFT mints**         | Block mints from sanctioned or malicious wallets                |
| **DeFi protocols**    | Require fresh risk screening before deposits                    |
| **Payment rails**     | Verify sender/recipient risk before processing transfers        |

***

## Step 1: Screen the Wallet (TypeScript)

Your backend calls the Range API to get the wallet's risk score and generates a
unique `riskCallId` for audit tracking.

```typescript theme={null}
import { randomBytes } from 'crypto';

interface RiskScreenResult {
  score: number;
  level: string;
  reasoning: string;
  riskCallId: string;
}

async function screenRecipient(
  userAddress: string,
  apiKey: string,
  network: string = 'solana',
): Promise<RiskScreenResult> {
  const params = new URLSearchParams({ address: userAddress, network });
  const response = await fetch(
    `https://api.range.org/v1/risk/address?${params}`,
    { headers: { Authorization: `Bearer ${apiKey}` } },
  );

  if (!response.ok) {
    throw new Error(
      `Range API error: ${response.status} ${response.statusText}`,
    );
  }

  const data = await response.json();

  // Unique identifier that ties the onchain attestation
  // back to this specific API call for compliance tracking
  const riskCallId = randomBytes(16).toString('hex');

  return {
    score: data.riskScore,
    level: data.riskLevel,
    reasoning: data.reasoning,
    riskCallId,
  };
}
```

<Note>
  The `riskCallId` is generated client-side as a random 16-byte hex string. This
  ties the onchain attestation to a specific screening event for auditability.
</Note>

***

## Step 2: Sign the Attestation (TypeScript)

After screening, your backend signs a message that binds the risk result to the
user's wallet and a specific point in time.

```typescript theme={null}
import nacl from 'tweetnacl';

function createSignedMessage(
  timestamp: number,
  userAddress: string,
  riskCallId: string,
  backendSecretKey: Uint8Array,
): { signature: Uint8Array; message: Uint8Array } {
  // Message format: "{timestamp}_{pubkey}_{riskCallId}"
  const messageStr = `${timestamp}_${userAddress}_${riskCallId}`;
  const message = new TextEncoder().encode(messageStr);
  const signature = nacl.sign.detached(message, backendSecretKey);

  return {
    signature: new Uint8Array(signature),
    message: new Uint8Array(message),
  };
}
```

The message format is `{timestamp}_{pubkey}_{riskCallId}` where:

* **timestamp**: Unix timestamp in seconds (used for freshness check onchain)
* **pubkey**: The user's Solana wallet address (base58)
* **riskCallId**: Unique identifier linking to the API screening result

<Warning>
  The backend secret key must be kept secure. If compromised, an attacker could
  forge attestations. Rotate the backend keypair and update the onchain
  program's `BACKEND_PUBKEY` constant if a key is ever leaked.
</Warning>

***

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

The user submits the backend's attestation in a Solana transaction. The SDK's
`buildVerifyRiskInstruction` constructs the instruction with the Anchor
discriminator, signature, and message, see the
[full implementation in the example repo](https://github.com/rangesecurity/sigverify-example/blob/master/sigverify/app/sdk.ts).

```typescript theme={null}
async function verifyRiskOnchain(
  rpc: ReturnType<typeof createSolanaRpc>,
  rpcSubscriptions: ReturnType<typeof createSolanaRpcSubscriptions>,
  userSigner: TransactionSigner,
  backendSecretKey: Uint8Array,
  apiKey: string,
) {
  // 1. Screen the wallet
  const riskResult = await screenRecipient(userSigner.address, apiKey);
  console.log(`Risk score: ${riskResult.score}, Level: ${riskResult.level}`);

  if (riskResult.score >= 10) {
    throw new Error('Risk score too high, blocking access.');
  }

  // 2. Backend signs the attestation
  const timestamp = Math.floor(Date.now() / 1000);
  const { signature, message } = createSignedMessage(
    timestamp,
    userSigner.address,
    riskResult.riskCallId,
    backendSecretKey,
  );

  // 3. Build the instruction
  const instruction = buildVerifyRiskInstruction(
    userSigner,
    signature,
    message,
  );

  // 4. Build, sign, and send the transaction
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

  const transactionMessage = pipe(
    createTransactionMessage({ version: 0 }),
    m => setTransactionMessageFeePayerSigner(userSigner, m),
    m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
    m => appendTransactionMessageInstruction(instruction, m),
  );

  const signedTransaction =
    await signTransactionMessageWithSigners(transactionMessage);
  assertIsTransactionWithBlockhashLifetime(signedTransaction);

  const sendAndConfirm = sendAndConfirmTransactionFactory({
    rpc,
    rpcSubscriptions,
  });
  await sendAndConfirm(signedTransaction, { commitment: 'confirmed' });

  const txSignature = getSignatureFromTransaction(signedTransaction);
  console.log('Risk attestation verified onchain. Tx:', txSignature);
}
```

***

## Step 4: Verify Onchain (Rust)

The Anchor program verifies the Ed25519 signature, checks that the message
signer matches the transaction signer, and enforces a freshness window.

### Program Entry Point

```rust theme={null}
use anchor_lang::prelude::*;

declare_id!("YOUR_PROGRAM_ID");

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

    pub fn verify_risk_signature(
        ctx: Context<VeriFyRiskAccounts>,
        args: VerifyRangeArgs,
    ) -> Result<()> {
        ctx.accounts.verify(args)
    }
}

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct VerifyRangeArgs {
    pub signature: Vec<u8>, // 64-byte Ed25519 detached signature
    pub message: Vec<u8>,   // UTF-8: "{timestamp}_{pubkey}_{riskCallId}"
}

#[derive(Accounts)]
pub struct VeriFyRiskAccounts<'info> {
    #[account(mut)]
    pub signer: Signer<'info>,
}
```

### Verification Logic

```rust theme={null}
impl<'info> VeriFyRiskAccounts<'info> {
    pub fn verify(&self, args: VerifyRangeArgs) -> Result<()> {
        let user_key = self.signer.key();
        let signature = args.signature;
        let message = args.message;

        // 1. Verify the Ed25519 signature against the backend's public key
        let backend_pubkey = Pubkey::from_str_const(BACKEND_PUBKEY);
        ScoreVerifier::sig_verify(
            &backend_pubkey.to_bytes(),
            &signature,
            &message,
        )?;

        // 2. Parse the message into its components
        let ExtractedMessage {
            timestamp,
            riskscore_pubkey,
            risk_call_id,
        } = ScoreVerifier::extract_message(&message)?;

        // 3. Verify the message pubkey matches the transaction signer
        require_keys_eq!(user_key, riskscore_pubkey, CustomErrorCode::SignersMismatch);

        // 4. Verify the attestation is fresh (within MAX_SCORE_LIFETIME seconds)
        let clock = Clock::get()?;
        let current_timestamp = clock.unix_timestamp as u64;
        let time_diff = current_timestamp.saturating_sub(timestamp);
        require!(
            time_diff <= MAX_SCORE_LIFETIME,
            CustomErrorCode::ScoreLifetimeExpired
        );

        msg!("Signature verified successfully for risk call ID: {}", risk_call_id);
        msg!("User can now be granted access to the protocol.");
        Ok(())
    }
}
```

### Signature Verification and Message Parsing

```rust theme={null}
use brine_ed25519::sig_verify;

pub struct ExtractedMessage {
    pub timestamp: u64,
    pub riskscore_pubkey: Pubkey,
    pub risk_call_id: String,
}

pub struct ScoreVerifier;

impl ScoreVerifier {
    /// Verify an Ed25519 signature against the backend's public key
    pub fn sig_verify(pubkey: &[u8], sig: &[u8], message: &[u8]) -> Result<()> {
        sig_verify(pubkey, sig, message)
            .map_err(|_| CustomErrorCode::CouldntVerifySignature)?;
        Ok(())
    }

    /// Parse the attestation message: "{timestamp}_{pubkey}_{riskCallId}"
    pub fn extract_message(message_bytes: &[u8]) -> Result<ExtractedMessage> {
        let message_string = String::from_utf8_lossy(message_bytes);
        let parts: Vec<&str> = message_string.split('_').collect();
        require_eq!(parts.len(), 3, CustomErrorCode::WrongMessageSplitLength);

        let timestamp = parts[0]
            .parse::<u64>()
            .map_err(|_| CustomErrorCode::TimestampParsingFailed)?;
        let riskscore_pubkey = Pubkey::from_str(parts[1])
            .map_err(|_| CustomErrorCode::PubkeyParsingFailed)?;
        let risk_call_id = parts[2].to_string();

        Ok(ExtractedMessage {
            timestamp,
            riskscore_pubkey,
            risk_call_id,
        })
    }
}
```

***

## Step 5: Run the Example

Clone the
[example repository](https://github.com/rangesecurity/sigverify-example) and
set up your own keys.

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

# Install dependencies
yarn install

# Generate your own backend and user keypairs
solana-keygen new --outfile app/backend_keypair.json --no-bip39-passphrase
solana-keygen new --outfile app/user_keypair.json --no-bip39-passphrase
```

Update `BACKEND_PUBKEY` in `programs/sigverify/src/constants.rs` with your
backend's public key:

```bash theme={null}
solana-keygen pubkey app/backend_keypair.json
```

```rust theme={null}
// programs/sigverify/src/constants.rs
pub const BACKEND_PUBKEY: &str = "<your-backend-pubkey>";
```

Since you're deploying your own copy of the program, you'll also need to
generate a new program keypair and update the program ID:

```bash theme={null}
# Generate a new program keypair
solana-keygen new --outfile target/deploy/sigverify-keypair.json --no-bip39-passphrase

# Get the new program ID
solana-keygen pubkey target/deploy/sigverify-keypair.json
```

Update the program ID in both `programs/sigverify/src/lib.rs` (`declare_id!`)
and `Anchor.toml` (`[programs.devnet]`) with the output, and update
`PROGRAM_ID` in `app/sdk.ts` to match.

```bash theme={null}
# Set your Range API key
echo "RANGE_API_KEY=your_api_key_here" > .env

# Build and deploy
anchor build
anchor deploy --provider.cluster devnet

# Fund the user wallet
solana airdrop 2 $(solana-keygen pubkey app/user_keypair.json) --url devnet

# Run the example
cd app && npx tsx index.ts
```

Expected output:

```
Wallet: <your-address> Risk score: 0 Level: Very low risk
Transaction confirmed: <tx-signature>
```

<Tip>
  For local development, you can use [Surfpool](https://www.surfpool.xyz/) to
  run a local Solana validator that auto-deploys your program on file changes.
</Tip>

***

## Customizing for Your Program

### Adjusting the Risk Threshold

Modify the screening threshold in your backend to match your risk policy:

```typescript theme={null}
// Block wallets with risk score >= 7 (out of 10)
if (riskResult.score >= 7) {
  throw new Error('Risk score too high, blocking access.');
}
```

### Adjusting the Attestation Lifetime

Update the constant in your onchain program:

```rust theme={null}
// Allow attestations up to 5 minutes old
pub const MAX_SCORE_LIFETIME: u64 = 300;
```

<Warning>
  Longer lifetimes reduce the number of API calls but increase the window for
  stale risk data. For high-value operations, keep the lifetime short (60
  seconds or less).
</Warning>

### Adding the Risk Score Onchain

You can extend the message format to include the risk score itself, allowing
your program to enforce thresholds onchain:

```rust theme={null}
// Extended message format: "{timestamp}_{pubkey}_{riskCallId}_{riskScore}"
// Then in your verification logic:
if risk_score > MAX_ALLOWED_RISK_SCORE {
    return Err(CustomErrorCode::RiskTooHigh.into());
}
```

### Production Considerations

* **Rotate backend keys**: Have a key rotation strategy. Update
  `BACKEND_PUBKEY` in your program via an upgrade or a config PDA.
* **Use HTTPS everywhere**: Ensure your backend-to-Range-API communication uses
  TLS.
* **Log `riskCallId`**: Store the mapping between `riskCallId` and screening
  results for compliance audits.

***

## 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 used in the screening step.
  </Card>

  <Card title="Onchain Risk Verifier" icon="shield-halved" href="/risk-api/guides/onchain-risk-verifier">
    Oracle-based alternative using Switchboard On-Demand for fully decentralized
    verification.
  </Card>

  <Card title="Example Repository" icon="github" href="https://github.com/rangesecurity/sigverify-example">
    Complete working implementation with Anchor program and TypeScript SDK.
  </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>
