Warddocs
Patterns

Step-up routing

STEP_UP means "not enough to approve". The right friction depends on why: route on signals, not on the decision alone.

d = client.decide("payment", txn)

ROUTES = {                               # card analyzer signal codes
    "device_new_for_card": "otp",        # an OTP to the known phone resolves it
    "three_ds_absent": "three_ds",       # ask the issuer for a 3-D Secure challenge
    "sim_swap_recent": "review",         # an OTP would go to the attacker: a person decides
    "impossible_travel": "review",
}

if d.decision == "STEP_UP":
    route = next((ROUTES[s] for s in d.signals if s in ROUTES), "review")
    if route == "review" and d.score_kind == "calibrated" and d.score < 0.25:
        route = "otp"                    # low calibrated risk: soft friction first
    dispatch(route, d.id)

The same pattern applies to KYC: a PHONE_NOT_CONFIRMED reason means "send the code", not "send to compliance". Map reason codes to UI actions, and only queue for people what needs a person.

Always keep d.id with the action you took. When the step-up resolves, send feedback: those labels are what lets Ward step up less over time.