Skip to main content
Privacy protocols on Solana - mixers, privacy pools, confidential transfer programs - face a unique challenge: they need to prevent sanctioned or stolen funds from entering the protocol while preserving user privacy for legitimate users. If tainted funds flow through your protocol, it taints the entire pool and exposes your project to regulatory risk. Range’s Address Risk Score provides a straightforward solution: screen the depositor’s wallet before accepting funds. Block high-risk wallets at the door - before they can contaminate the pool.
Prerequisites: You need a Risk API key and familiarity with the Address Risk Score endpoint. For on-chain enforcement, see the Onchain Risk Verifier guide.

Why Screen Deposits?

Without screeningWith screening
Stolen funds enter your privacy poolStolen funds are rejected before deposit
OFAC-sanctioned addresses mix freelySanctioned addresses are blocked at the door
Your protocol becomes a laundering vectorYour protocol stays clean for legitimate users
Regulators target your protocolDemonstrable compliance controls
Honest users’ funds are tainted by associationPool integrity is preserved
Privacy and compliance are not mutually exclusive. Screening depositors (not tracking what happens inside the pool) lets you maintain privacy guarantees for legitimate users while blocking bad actors.

Architecture

User wants to deposit


┌──────────────────────────┐
│  Screen depositor wallet │
│  (Address Risk Score)    │
│                          │
│  riskScore 1–10          │
│  Includes sanctions +    │
│  ML + proximity analysis │
└──────────────────────────┘

      ├── Score ≥ 7 ──────► REJECT deposit
      │                     Show reason

      ├── Score 4–6 ──────► FLAG for review
      │                     Optional: require
      │                     additional verification

      └── Score 1–3 ──────► ALLOW deposit
                            Proceed normally
Address Risk Score already includes sanctions and blacklist screening. You do not need a separate sanctions check - OFAC-flagged and stablecoin-blacklisted addresses are factored into the risk score.

Step 1: Screen the Depositor

Before accepting a deposit, check the wallet’s risk score. This single API call covers malicious proximity, ML-based behavioral analysis, sanctions, and blacklist screening.
curl -G https://api.range.org/v1/risk/address \
  --data-urlencode "address=DEPOSITOR_ADDRESS" \
  --data-urlencode "network=solana" \
  -H "Authorization: Bearer your_api_key_here"
async function screenDepositor(address: string) {
  const params = new URLSearchParams({
    address,
    network: 'solana',
  });

  const response = await fetch(
    `https://api.range.org/v1/risk/address?${params}`,
    { headers: { Authorization: `Bearer ${API_KEY}` } },
  );

  return response.json();
}

Example Response - Flagged Wallet

{
  "riskScore": 9,
  "riskLevel": "Extremely high risk",
  "numHops": 1,
  "maliciousAddressesFound": [
    {
      "address": "SuspiciousAddr...",
      "distance": 1,
      "name_tag": "Exploit Funds",
      "entity": null,
      "category": "hack_funds"
    }
  ],
  "reasoning": "Address is 1 hop from known exploit funds.",
  "attribution": null
}
This wallet is 1 hop from known hack funds - it should be rejected.

Step 2: Make a Deposit Decision

interface RiskData {
  riskScore: number;
  riskLevel: string;
  reasoning: string;
  attribution: { name_tag: string; entity: string } | null;
}

interface DepositDecision {
  decision: 'allow' | 'reject' | 'flag';
  reason: string;
}

function evaluateDeposit(riskData: RiskData): DepositDecision {
  const { riskScore, reasoning, attribution } = riskData;

  // Known system addresses and verified entities are always safe
  if (attribution) {
    return {
      decision: 'allow',
      reason: `Verified entity: ${attribution.name_tag} (${attribution.entity})`,
    };
  }

  // High risk - reject
  if (riskScore >= 7) {
    return {
      decision: 'reject',
      reason: `Risk score ${riskScore}/10: ${reasoning}`,
    };
  }

  // Medium risk - flag for review or require additional verification
  if (riskScore >= 4) {
    return {
      decision: 'flag',
      reason: `Elevated risk (${riskScore}/10): ${reasoning}`,
    };
  }

  // Low risk - allow
  return {
    decision: 'allow',
    reason: 'Address screening passed',
  };
}

Step 3: Integrate into Your Deposit Flow

Off-Chain (Backend / Frontend)

Screen wallets in your dApp frontend or backend before constructing the deposit transaction:
async function handleDeposit(depositorAddress: string, amount: number) {
  // Screen the wallet
  const riskData = await screenDepositor(depositorAddress);
  const evaluation = evaluateDeposit(riskData);

  if (evaluation.decision === 'reject') {
    return {
      status: 'rejected',
      message:
        'This wallet has been flagged for connections to malicious activity. ' +
        'Deposit not permitted.',
    };
  }

  if (evaluation.decision === 'flag') {
    // Optional: require additional verification for medium-risk wallets
    const verified = await requestAdditionalVerification(depositorAddress);
    if (!verified) {
      return {
        status: 'rejected',
        message: 'Additional verification required.',
      };
    }
  }

  // Proceed with deposit
  return await executeDeposit(depositorAddress, amount);
}

On-Chain (Solana Program)

For protocol-level enforcement that can’t be bypassed by calling the program directly, use the Onchain Risk Verifier. This verifies risk scores inside your Solana program using Switchboard oracles:
// In your deposit instruction handler:
let risk_score = verified_feed.value(); // 0–100 scale (original 0–10 × 10)

// Reject deposits from high-risk wallets
if risk_score >= 70 {
    return Err(ErrorCode::HighRiskDepositor.into());
}

// Proceed with deposit logic
On-chain enforcement is stronger than off-chain screening alone. Users can bypass your frontend, but they can’t bypass your program’s instruction logic. For maximum protection, use both: off-chain screening for UX (show the user why they’re blocked) and on-chain verification as the enforcement layer.
Alternative: Signed message attestation. Instead of using Switchboard oracles, you can implement a lighter-weight pattern where your backend signs an attestation message containing the screened wallet address and a timestamp. The Solana program then uses Ed25519 signature verification (via the Ed25519 precompile) and instruction introspection to confirm: (1) the message was signed by your trusted attestation key, (2) the depositor matches the approved address in the message, and (3) the timestamp is fresh. This approach trades the decentralized oracle model for a simpler trusted-backend pattern.

Choosing Your Risk Threshold

The right threshold depends on your protocol’s risk tolerance and regulatory posture:
ThresholdRejectsTrade-off
Score ≥ 8Only directly malicious or 1-hop addressesPermissive - blocks clear threats, allows most users
Score ≥ 7Addresses within 2 hops of malicious actorsBalanced - recommended starting point
Score ≥ 6Addresses within 2 hops (with multiple malicious connections)Strict - fewer false negatives, more false positives
Score ≥ 4Addresses within 3 hopsVery strict - suitable for regulated environments
Setting the threshold too low (e.g., ≥ 3) will reject a significant number of legitimate users. Many clean addresses are naturally within 4–5 hops of flagged addresses through shared exchange or program interactions. Start with ≥ 7 and adjust based on your data.

Handling Edge Cases

What If the Risk Check Fails?

Network errors or API timeouts shouldn’t block legitimate deposits indefinitely. Implement a fallback policy:
async function screenWithFallback(address: string) {
  try {
    const result = await Promise.race([
      screenDepositor(address),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('timeout')), 5000),
      ),
    ]);
    return evaluateDeposit(result);
  } catch (error) {
    // Fallback: allow with logging for manual review
    console.warn(`Risk check failed for ${address}: ${error}`);
    return {
      decision: 'allow',
      reason: 'Risk check unavailable - allowed with manual review flag',
      requiresReview: true,
    };
  }
}

Known System Addresses

Some addresses that interact with your protocol will be system programs, token programs, or exchange hot wallets. These will have an attribution field in the response - use it to fast-path known entities:
if (riskData.attribution) {
  // Verified non-malicious entity - skip further checks
  console.log(`Known entity: ${riskData.attribution.name_tag}`);
  return { decision: 'allow' };
}

Rate Limits

If you’re processing high deposit volume, see Rate Limits & Plans for scaling options. For on-chain enforcement via Switchboard oracles, rate limits apply to the oracle quote requests, not the on-chain verification.

Compliance Considerations

Screening deposits demonstrates that your protocol has taken reasonable steps to prevent misuse. Document your screening policy and keep records:
  • Policy document - Define your risk threshold and what happens when a deposit is rejected
  • Audit log - Store the risk score, reasoning, and decision for each screened deposit
  • Threshold rationale - Document why you chose your threshold and any adjustments over time
  • False positive handling - Have a process for users to contest rejections
async function logScreeningResult(
  depositor: string,
  riskData: RiskData,
  decision: DepositDecision,
) {
  await complianceDB.insert({
    timestamp: new Date().toISOString(),
    depositor_address: depositor,
    risk_score: riskData.riskScore,
    risk_level: riskData.riskLevel,
    reasoning: riskData.reasoning,
    decision: decision.decision,
    decision_reason: decision.reason,
  });
}

What’s Next

Last modified on March 2, 2026