advanced
rider-score
troubleshooting
debugging

Troubleshooting

Common Rider Score problems and how to diagnose them - missing scores, stuck tiers, ghost interventions, reward not issued.

Levy Fleets TeamMay 18, 20268 min read

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:

  1. Is the subaccount enabled? subaccount_rider_score_settings.enabled = true. If false, no scoring runs.
  2. Did the ride end in a scored state? Cancelled rides and rides that errored at end-time do not get scored.
  3. Was the ride too short? Rides under min_ride_seconds (default 60s) or min_ride_meters (default 200m) are excluded.
  4. 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.
  5. Run a manual recompute. POST /api/internal/rider-score/recompute with { 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:

  1. How many scored rides in the window? With three rides, the rider is still in Beginner cold-start - the rolling score is a placeholder.
  2. What is the halflife? If your halflife is set to 90 days (or higher), recent rides barely move the EWMA.
  3. What is the window? If the rider has historical bad rides from 60 days ago, those are still weighted in.
  4. 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:

  1. rider_scores.computed_at - has the rolling score been recomputed since the last trip ended?
  2. Push notification delivery - the tier badge updates after the push lands. If push delivery failed, the rider may need to pull-to-refresh.
  3. pickTierForScore snapshot - the function is unit-tested; the only way it returns the wrong tier is if rider_score_tiers rows have overlapping ranges or a gap.
  4. Manual recompute - POST /api/internal/rider-score/recompute with { 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:

StatusMeaning
issuedCredit issued, linked to a ride_refunds row and a wallet_transactions row
pendingThe 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_capThe per-rider monthly cap was hit
skipped_budgetThe fleet-wide monthly budget was exhausted
skipped_tier_inactiveThe 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:

  1. Ladder rule. Is step 4 enabled for your subaccount? Look at rider_intervention_rules.
  2. rider_interventions row. Was a step-4 intervention opened? If yes, it should be status='open' at the time of the unlock.
  3. getActiveInterventionState is the gate. It is called from src/app/api/mobile/rides/start/route.ts. Check Sentry for any error from that path.
  4. IoT throttle support. Step 4 reaches the vehicle via disableVehicleThrottle(). Vehicles whose iot_type does not support throttle commands skip step 4 silently and may escalate to step 5 instead.
  5. 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:

  1. rider_helmet_selfies row. Is passed_at set? Is ttl_expires_at in the future?
  2. consumed_at - if helmet_single_use=true and the rider already used this selfie, it is no longer valid.
  3. 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.
  4. Subaccount setting - is helmet_discount_unlock_fee_cents set to a non-zero value?
  5. Wrong table. Older internal docs may reference helmet_verifications (the CV pipeline's table). The rider-score helmet selfie table is rider_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:

  1. reaction_repeat_hours - default 6. Did the rider unlock more than 6 hours after their last pass?
  2. reaction_window_* - was the unlock inside the night window? If yes, the test fires regardless of recent passes once the repeat hours have elapsed.
  3. reaction_tests row - was passed=true? If somehow it was recorded as fail (miss_count >= threshold), the cooldown applies.
  4. 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_snapshot on each ride_safety_signals row 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:

  1. Time range filter - the audit log defaults to last 30 days. Expand if needed.
  2. Actor filter - if you filtered by your own user_id, automatic system actions (cron, post-ride hook) will not appear.
  3. 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:

  1. vercel.json has 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.
  2. Cron auth header - Vercel cron jobs use a shared secret. If the cron returns 401, the secret is wrong.
  3. 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.