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.
Running a fleet on this hardware?
Levy Fleets runs scooters, e-bikes, mopeds and golf carts on the IoT hardware you already have, with integration pages for 29 vendors including Segway, Okai, Omni, Queclink, Invers and Comodule. Keyless unlock, geofencing, rider payments and rider support are included, and the Managed plan is a share of revenue with no per-vehicle software fee.