Troubleshooting
Rider Score is soft-fail by design - any error inside the scoring subsystem is logged and never blocks a ride from ending or starting. That makes most issues silent. Use this article as the diagnostic checklist when something looks off.
A ride has no score
Symptom: A completed ride exists in the rides table but has no row in ride_safety_signals.
Check, in order:
- Is the subaccount enabled?
subaccount_rider_score_settings.enabled = true. If false, no scoring runs. - Did the ride end in a scored state? Cancelled rides and rides that errored at end-time do not get scored.
- Was the ride too short? Rides under
min_ride_seconds(default 60s) ormin_ride_meters(default 200m) are excluded. - Is there a server log entry? The rider-score post-ride coordinator logs failures with the ride UUID. Check Sentry filtered by the rider-score subsystem.
- Run a manual recompute.
POST /api/internal/rider-score/recomputewith{ rideId: "..." }will re-evaluate just that ride.
The nightly cron /api/cron/rider-score-recompute will pick up any ride missed by the per-ride hook.
Rolling score does not match the trip scores I see
Symptom: A rider has three high trip scores in a row, but their rolling score barely moved.
Check:
- How many scored rides in the window? With three rides, the rider is still in Beginner cold-start - the rolling score is a placeholder.
- What is the halflife? If your halflife is set to 90 days (or higher), recent rides barely move the EWMA.
- What is the window? If the rider has historical bad rides from 60 days ago, those are still weighted in.
- Are short rides being excluded? The rolling score only sees rides above the
min_ride_*thresholds.
To validate, pull the rider's recent trip scores from ride_safety_signals ordered by computed_at desc. The math should be obvious from the EWMA formula in Scoring Formula.
Tier is stuck
Symptom: Rider's score crossed a tier boundary an hour ago but the badge has not changed.
Check:
rider_scores.computed_at- has the rolling score been recomputed since the last trip ended?- Push notification delivery - the tier badge updates after the push lands. If push delivery failed, the rider may need to pull-to-refresh.
pickTierForScoresnapshot - the function is unit-tested; the only way it returns the wrong tier is ifrider_score_tiersrows have overlapping ranges or a gap.- Manual recompute -
POST /api/internal/rider-score/recomputewith{ customerId: "..." }will force a tier reassignment.
Reward was not issued
Symptom: A Gold rider completed a qualifying ride, the trip score is good, but no wallet credit appeared.
Check rider_score_rewards for that ride. The status field tells you what happened:
| Status | Meaning |
|---|---|
issued | Credit issued, linked to a ride_refunds row and a wallet_transactions row |
pending | The reward row was inserted but the refund pipeline failed before the credit was issued. Manual intervention needed - never create a wallet credit by hand to "make it right"; instead recreate the refund chain (see CLAUDE.md). |
skipped_cap | The per-rider monthly cap was hit |
skipped_budget | The fleet-wide monthly budget was exhausted |
skipped_tier_inactive | The rider's tier has no per_ride_credit_cents set |
For pending, check Sentry for the underlying refund-pipeline error. The most common cause is a missing or invalid ride_refunds.id reference.
Never bypass the pipeline
If a reward is stuck in pending, fix the upstream issue and re-run the reward engine for that ride. Do NOT create a wallet credit by hand. Direct writes to wallet_balance or manual inserts into wallet_transactions will corrupt net_deposited and partner payouts. See CLAUDE.md refund guardrails.
Intervention did not fire
Symptom: Rider score dropped below 40 but no throttle cap appeared on their next ride.
Check:
- Ladder rule. Is step 4 enabled for your subaccount? Look at
rider_intervention_rules. rider_interventionsrow. Was a step-4 intervention opened? If yes, it should bestatus='open'at the time of the unlock.getActiveInterventionStateis the gate. It is called fromsrc/app/api/mobile/rides/start/route.ts. Check Sentry for any error from that path.- IoT throttle support. Step 4 reaches the vehicle via
disableVehicleThrottle(). Vehicles whoseiot_typedoes not support throttle commands skip step 4 silently and may escalate to step 5 instead. - Active appeal. If the rider filed an appeal on the triggering ride, the intervention is paused - this is correct behavior, not a bug.
Helmet selfie discount did not apply
Symptom: Rider verified their helmet but the unlock fee was not reduced.
Check:
rider_helmet_selfiesrow. Ispassed_atset? Isttl_expires_atin the future?consumed_at- ifhelmet_single_use=trueand the rider already used this selfie, it is no longer valid.apply-uplift.ts- the single bridge function for pricing modifiers. If it is not wired into the operator's pricing flow yet, the discount will not surface at checkout. See the known-gaps list in the implementation notes.- Subaccount setting - is
helmet_discount_unlock_fee_centsset to a non-zero value? - Wrong table. Older internal docs may reference
helmet_verifications(the CV pipeline's table). The rider-score helmet selfie table isrider_helmet_selfies. If a query is hitting the wrong table, the discount will look broken.
Reaction test keeps firing
Symptom: Rider passed the Safe Ride Check, but the app prompts them again on their next unlock.
Check:
reaction_repeat_hours- default 6. Did the rider unlock more than 6 hours after their last pass?reaction_window_*- was the unlock inside the night window? If yes, the test fires regardless of recent passes once the repeat hours have elapsed.reaction_testsrow - waspassed=true? If somehow it was recorded as fail (miss_count >= threshold), the cooldown applies.- Random trigger - if
reaction_random_trigger_pct > 0, a small percentage of all unlocks get the test, not just night unlocks.
Score does not change after I edit weights
Symptom: I updated rider_score_weights an hour ago but rolling scores look identical.
This is expected. Weight changes:
- Affect future per-trip scores immediately - any ride completing after the edit uses the new weights.
- Trigger a one-time full recompute of rolling scores via the next run of
/api/cron/rider-score-recompute(nightly). - Do NOT retroactively change stored trip scores - the
weights_snapshoton eachride_safety_signalsrow is preserved.
If you need an immediate rolling-score recompute, hit POST /api/internal/rider-score/recompute with { subaccountId: "..." }.
Audit log entry I expected is missing
Symptom: I lifted an intervention with a reason but I do not see the entry in Audit Log.
Check:
- Time range filter - the audit log defaults to last 30 days. Expand if needed.
- Actor filter - if you filtered by your own user_id, automatic system actions (cron, post-ride hook) will not appear.
- Action type filter - "intervention_lift" is distinct from "intervention_close" and "appeal_accepted". Try clearing the action filter.
Every Ops action through the dashboard writes to score_audit_log. If an entry is genuinely missing, file an internal ticket - this would be a bug.
Cron is not running
Symptom: Nightly recompute never appears to fire.
Check:
vercel.jsonhas entries for/api/cron/rider-score-recompute,/api/cron/rider-score-insurance-dispatch, and/api/cron/helmet-verification-cleanup. The known-gaps list flags this as a manual setup step.- Cron auth header - Vercel cron jobs use a shared secret. If the cron returns 401, the secret is wrong.
- Vercel dashboard > Cron jobs to see last-run status.
Still stuck?
Pull the affected rider's customer_uuid and the affected ride's UUID, then contact support@levyelectric.com. Including those two identifiers cuts triage time roughly in half.