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.
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
| Score | UI Treatment |
|---|
| 1–3 | Green indicator. Proceed normally. If attribution is present, optionally show the entity name (e.g., “Token Program - Solana”). |
| 4–5 | Yellow warning. Show reasoning and let the user decide. |
| 6–7 | Orange warning. Recommend against proceeding. Show reasoning and the number of hops to malicious addresses. |
| 8–10 | Red 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
| Field | What to Show |
|---|
error | If present, the transaction would fail - show the error message |
asset_transfers | Token and SOL movements with amount, mint, and change_type |
expected_state_changes | Human-readable diffs via humanReadableDiff for each account |
transaction_summary | Fee breakdown via expected_fee and compute units consumed |
transaction_risk | Exploit warnings and account risk scores |
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: [] };
}
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.
Recommended UI Pattern
┌─────────────────────────────────────────┐
│ 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);
}
What’s Next