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 userCode
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):
passKey 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"}