Guide: Onboarding Flow

Screen wallet addresses at user signup before granting platform access.

When to screen

Screen immediately when a user registers a wallet address — before account activation, before any deposits, and before any permissions are granted. This is the lowest-cost intervention point.

Decision flowchart

User submits wallet address
        │
        ▼
POST /v1/screen
        │
        ├─ CLEAN_NO_MATCH ──────────────▶ Activate account ✓
        │
        ├─ LOW / MEDIUM ────────────────▶ Flag for manual review
        │                                  Queue for compliance team
        │                                  Send "under review" email
        │
        └─ HIGH / CRITICAL ─────────────▶ Reject account ✗
                                           Log for reporting
                                           Do not reveal reason

Code

import os
import requests
from enum import Enum

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

class OnboardingDecision(str, Enum):
    ALLOW = "allow"
    REVIEW = "review"
    REJECT = "reject"

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 onboarding_decision(wallet_address: str, chain: str) -> tuple[OnboardingDecision, dict]:
    """
    Screen a wallet at user signup.
    Returns (decision, screening_result) for audit logging.
    """
    result = screen(wallet_address, chain)
    level = result["level"]

    if level == "CLEAN_NO_MATCH":
        decision = OnboardingDecision.ALLOW
    elif level in ("CRITICAL", "HIGH"):
        decision = OnboardingDecision.REJECT
    else:  # MEDIUM (LOW is reserved; not currently produced)
        decision = OnboardingDecision.REVIEW

    return decision, result

# In your signup handler:
def handle_signup(user_id: str, wallet_address: str, chain: str):
    decision, result = onboarding_decision(wallet_address, chain)

    # Always log the screening result for compliance audit trail
    log_screening_event(
        user_id=user_id,
        address=wallet_address,
        chain=chain,
        level=result["level"],
        score=result["score"],
        labels=[l["label"] for l in result["labels"]],
        scoring_version=result["scoring_version"],
        decision=decision,
    )

    if decision == OnboardingDecision.ALLOW:
        activate_account(user_id)

    elif decision == OnboardingDecision.REVIEW:
        flag_for_manual_review(user_id)
        send_pending_email(user_id)  # "Your account is under review"

    elif decision == OnboardingDecision.REJECT:
        reject_account(user_id)
        # Do not reveal the reason to the user — standard compliance practice

Compliance notes

  • Always log the full result — score, level, labels, scoring_version, and timestamp — for your audit trail. Regulators may ask for evidence of the screening.
  • Do not reveal rejection reasons to users. Disclosing that an address is on a sanctions list can constitute tipping off, which may violate laws in some jurisdictions.
  • MEDIUM/LOW is not auto-pass. Route these to a compliance queue for manual review within your defined SLA (e.g., 24 hours).
  • CLEAN_NO_MATCH is not a guarantee. Screening should be one layer of your compliance program, not the only one.

Related guides