Guide: Withdrawal Flow

Screen destination addresses before processing any outgoing transfer.

Why screen at withdrawal

Onboarding screening checks the wallet a user signs up with. But withdrawals go to a destination address — which may differ from the registered address and may change over time. A user can add new withdrawal addresses, including compromised or sanctioned ones.

Screen the destination at the point of each withdrawal request, not just at account creation.

Decision flowchart

User requests withdrawal to address X
        │
        ▼
POST /v1/screen (destination address)
        │
        ├─ CLEAN_NO_MATCH ──────────────▶ Execute withdrawal ✓
        │                                  Log result + timestamp
        │
        ├─ MEDIUM ──────────────────────▶ Hold withdrawal
        │                                  Queue for manual review (< 24h SLA)
        │                                  Notify user: "processing"
        │
        └─ HIGH / CRITICAL ─────────────▶ Block withdrawal ✗
                                           Freeze funds
                                           Alert compliance team
                                           Do not reveal reason to user

Code

import os
import requests

SCREENING_API_KEY = os.environ["SCREENING_API_KEY"]
BASE_URL = "https://anchorapi.dev/api/v1"

def screen(address: str, chain: str) -> dict:
    response = requests.post(
        f"{BASE_URL}/screen",
        headers={"Authorization": f"Bearer {SCREENING_API_KEY}"},
        json={"address": address, "chain": chain},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

def process_withdrawal(
    withdrawal_id: str,
    user_id: str,
    destination_address: str,
    chain: str,
    amount_usd: float,
):
    """
    Screen the destination address before processing a withdrawal.
    Never send funds to a flagged address.
    """
    result = screen(destination_address, chain)
    level = result["level"]

    # Log every screening, every time — not just hits
    log_withdrawal_screening(
        withdrawal_id=withdrawal_id,
        user_id=user_id,
        address=destination_address,
        chain=chain,
        amount_usd=amount_usd,
        level=level,
        score=result["score"],
        labels=[l["label"] for l in result["labels"]],
        scoring_version=result["scoring_version"],
    )

    if level in ("CRITICAL", "HIGH"):
        # Block: do not process, escalate immediately
        freeze_withdrawal(withdrawal_id)
        alert_compliance_team(withdrawal_id, result)
        raise WithdrawalBlockedError(
            f"Withdrawal {withdrawal_id} blocked: destination address flagged ({level})"
        )

    elif level == "MEDIUM":
        # Hold: manual review before release (LOW reserved; not currently produced)
        hold_withdrawal_for_review(withdrawal_id)
        return {"status": "pending_review"}

    else:
        # CLEAN_NO_MATCH: proceed to settlement
        execute_withdrawal(withdrawal_id, destination_address, chain, amount_usd)
        return {"status": "processed"}


class WithdrawalBlockedError(Exception):
    pass

Key practices

  • Screen before funds move. Call the API synchronously in your withdrawal pipeline, before triggering any settlement. Do not screen asynchronously — a delayed check is not a check.
  • Log every screening result, even CLEAN_NO_MATCH. You need proof that you screened at the time of each transaction, not just at signup.
  • Never disclose the block reason to the user. Say "your withdrawal is under review" or "we were unable to process this withdrawal at this time."
  • Handle API errors safely. If the screening API returns an error (5xx, timeout), block or hold the withdrawal — do not default to releasing funds.

Handling API errors in withdrawals

If the screening call fails, your withdrawal code should fail safe:

try:
    result = screen(address, chain)
except Exception as e:
    # Screening failed — do NOT release funds
    hold_withdrawal_for_review(withdrawal_id)
    alert_ops_team(f"Screening error on {withdrawal_id}: {e}")
    return {"status": "on_hold", "reason": "screening_unavailable"}

Related guides