Skip to main content
This guide shows how to integrate Risk API into a Solana wallet or dApp to protect users from malicious addresses and risky transactions. The flow screens recipients before sending and simulates transactions before signing.
Prerequisites: You need a Risk API key and familiarity with the Address Risk Score and Transaction Simulator endpoints.

Integration Flow

A typical wallet integration adds two risk gates to the send flow:
User enters recipient + amount


┌─────────────────────────┐
│  Screen recipient       │──── High risk? → Show warning
│  (Address Risk Score)   │
└─────────────────────────┘


  User constructs transaction


┌─────────────────────────┐
│  Simulate transaction   │──── Risky accounts? → Show warning
│  (Transaction Simulator)│     Failure? → Show error
└─────────────────────────┘


  User reviews risk info → Confirm or Cancel


  Sign and broadcast

Step 1: Screen the Recipient

Before the user constructs a transaction, check the recipient address for connections to known malicious actors.
async function screenRecipient(address: string, network: string = 'solana') {
  const params = new URLSearchParams({ address, network });
  const response = await fetch(
    `https://api.range.org/v1/risk/address?${params}`,
    { headers: { Authorization: `Bearer ${API_KEY}` } },
  );
  const data = await response.json();

  return {
    score: data.riskScore,
    level: data.riskLevel,
    reasoning: data.reasoning,
    isSafe: data.riskScore <= 3,
    isAttributed: data.attribution !== null,
    attribution: data.attribution,
  };
}

What to Show the User

ScoreUI Treatment
1–3Green indicator. Proceed normally. If attribution is present, optionally show the entity name (e.g., “Token Program - Solana”).
4–5Yellow warning. Show reasoning and let the user decide.
6–7Orange warning. Recommend against proceeding. Show reasoning and the number of hops to malicious addresses.
8–10Red block. Strongly warn. Show reasoning and maliciousAddressesFound details. Require explicit confirmation.
Check the attribution field - when present, it means the address is a verified non-malicious entity (like a system program or major exchange). You can display the entity name to reassure the user.

Step 2: Simulate the Transaction

After the user constructs but before they sign the transaction, simulate it to preview the effects and check for exploit risks.
async function simulateTransaction(encodedTransaction: string) {
  const response = await fetch(
    'https://api.range.org/v1/simulate/solana/transaction',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        payload: encodedTransaction,
        encoding_type: 'base64',
      }),
    },
  );
  return response.json();
}

Key Response Fields to Display

FieldWhat to Show
errorIf present, the transaction would fail - show the error message
asset_transfersToken and SOL movements with amount, mint, and change_type
expected_state_changesHuman-readable diffs via humanReadableDiff for each account
transaction_summaryFee breakdown via expected_fee and compute units consumed
transaction_riskExploit warnings and account risk scores
For detailed response parsing with code examples, see the Transaction Simulation Guide.

Exploit Risk Warnings

The simulator detects exploit patterns like address poisoning. If exploit_risks_detected is non-empty, display a warning:
function checkExploitRisks(simulationResult: SimulationResult) {
  const exploits =
    simulationResult.transaction_risk?.exploit_risks_detected || [];

  if (exploits.length > 0) {
    return {
      hasExploitRisk: true,
      warnings: exploits.map(
        e => e.description || 'Potential exploit detected',
      ),
    };
  }

  return { hasExploitRisk: false, warnings: [] };
}

Step 3: Display Risk Information

Combine the address screening and simulation results into a confirmation screen. The goal is to give users enough information to make an informed decision without overwhelming them.
┌─────────────────────────────────────────┐
│  Transaction Preview                    │
│                                         │
│  Sending: 100 USDC                      │
│  To: 7xKX...3mN2                        │
│  Fee: ~0.00025 SOL                      │
│                                         │
│  ┌─────────────────────────────────┐    │
│  │ ⚠ Recipient Risk: Medium (5/10) │    │
│  │ 3 hops from flagged address     │    │
│  └─────────────────────────────────┘    │
│                                         │
│  Balance Changes:                       │
│  · USDC: -100.00                        │
│  · SOL:  -0.00025 (fee)                 │
│                                         │
│  [Cancel]              [Send Anyway]    │
└─────────────────────────────────────────┘

Best Practices

  • Don’t block silently. Always explain why a transaction is flagged. Show the reasoning field.
  • Let users override with warning. Even high-risk transactions should be possible to send if the user explicitly confirms - they may know something the scoring doesn’t.
  • Show entity attribution. If the recipient is a known entity (exchange, protocol), display its name.
  • Cache address risk cautiously. Risk scores change as new intelligence is incorporated. For high-value transfers, always query fresh.

Step 4: Handle Edge Cases

Network Errors and Timeouts

async function screenWithFallback(address: string, network: string) {
  try {
    const result = await Promise.race([
      screenRecipient(address, network),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('timeout')), 5000),
      ),
    ]);
    return result;
  } catch (error) {
    // If the risk check fails, inform the user but don't block
    return {
      score: null,
      level: 'unknown',
      reasoning: 'Risk check unavailable - proceed with caution',
      isSafe: null,
    };
  }
}

Rate Limits

If you hit rate limits (429 responses), show a degraded state rather than blocking the user. See Rate Limits & Plans for retry strategies.

Unsupported Networks

Address Risk Score returns results for 18+ supported networks. For unsupported networks, partial results may be available through direct attribution or cross-chain propagation.

Complete Send Flow

async function protectedSend(
  recipient: string,
  amount: number,
  network: string,
) {
  // Screen the recipient
  const risk = await screenWithFallback(recipient, network);

  if (risk.score >= 8) {
    // Require explicit user confirmation for high-risk
    const confirmed = await showHighRiskWarning(risk);
    if (!confirmed) return { status: 'cancelled' };
  } else if (risk.score >= 4) {
    // Show warning for medium-risk
    const confirmed = await showMediumRiskWarning(risk);
    if (!confirmed) return { status: 'cancelled' };
  }

  // Build and encode the transaction (see Transaction Simulation guide for SDK examples)
  const transaction = await buildTransaction(recipient, amount);
  const encoded = encodeTransactionBase64(transaction);

  // Simulate before signing
  const simulation = await simulateTransaction(encoded);

  if (simulation.error) {
    return { status: 'error', reason: simulation.error };
  }

  const exploits = checkExploitRisks(simulation);
  if (exploits.hasExploitRisk) {
    const confirmed = await showExploitWarning(exploits.warnings);
    if (!confirmed) return { status: 'cancelled' };
  }

  // Show confirmation with all risk data
  const confirmed = await showConfirmation({ risk, simulation });
  if (!confirmed) return { status: 'cancelled' };

  // Sign and send
  return await signAndSend(transaction);
}
For SDK-specific transaction building and encoding examples using @solana/kit or @solana/web3.js, see the Transaction Simulation Guide.

What’s Next

Last modified on March 2, 2026