intermediate
juicer
bounty
claim

Bounty Claim and Drop

End-to-end flow for a Juicer claiming a bounty, picking up the vehicle, charging it, and dropping it in zone — including the photo and GPS validation at both ends

Levy Fleets TeamMay 18, 20268 min read

Bounty Claim and Drop

This is the full lifecycle of a single bounty from the Juicer's perspective, including exactly what the platform validates at each step.

The Five Phases

open → claimed → picked_up → charging → ready_to_drop → completed
                                                          ↓
                                                       payout

A bounty can also drop into expired (claim TTL hit), canceled (operator action), or disputed (fraud flag plus operator review).

Phase 1: Open

The bounty engine runs every 5 minutes. For each enabled subaccount, it walks every candidate vehicle (low SoC, peak-shortfall prediction, or SoH rotation flag) and decides whether to create or update a bounty row. The bounty has:

  • A payout amount in cents
  • A surge multiplier
  • An expiry timestamp (TTL)
  • The vehicle's current location (GIST-indexed for proximity queries)
  • A status of open
  • A predicted peak shortfall (Phase 4)

Open bounties surface on the Juicer's map.

Phase 2: Claim

The Juicer taps Claim on a pin.

The platform calls the database function claim_bounty(p_bounty_id, p_juicer_id, p_claim_ttl_seconds). This function takes a FOR UPDATE lock on both the bounty and the Juicer row, then validates all of:

  • Juicer status = 'active'
  • Juicer kyc_status = 'verified'
  • Juicer is under their max_concurrent_claims cap
  • Bounty is open and not expired
  • Bounty's vehicle has fresh telemetry (last report within 30 min)
  • Bounty's vehicle is not in an active ride
  • Bounty's installed pack is not flagged maintenance or recalled
  • Juicer is within their claim_radius_m of the vehicle (default 5 mi, configurable per subaccount)
  • The subaccount matches (cross-tenant claims rejected)

If any check fails, the whole transaction rolls back and the Juicer gets a specific error message. Two Juicers cannot both succeed — the row lock guarantees that.

On success:

  • bounties.status = 'claimed', claimed_by, claimed_at, expires_at (60-minute TTL by default)
  • A new juicer_sessions row is inserted in claimed state
  • A best-effort IoT command is fired to disable the vehicle's throttle

The vehicle disappears from every other Juicer's map immediately.

Phase 3: Pickup (Photo + GPS Validation)

The Juicer drives to the vehicle and takes a pickup photo with the vehicle's ID label visible. They tap I've got it.

The platform validates:

CheckLimitWhat Happens If It Fails
Photo GPS drift≤50 m from vehicle telemetry GPS = clean
50–200 m = warning (logged, +5 fraud-score)
>200 m = block
Pickup rejected, claim stays open until TTL
Photo timestampWithin 5 min of the Claim tapWarning logged, +3 fraud-score
Photo hash collisionHash must not match any prior pickup photo from this JuicerBlock — repeated stock-photo attempt, +20 fraud-score
Vehicle ID visible (OCR)Vehicle number must be OCR-readableSoft check today (Phase 4 will harden)

If everything passes, the session moves to picked_up. The pickup photo URL and hash are stored on the juicer_sessions row.

Why Validate Twice

Pickup validation prevents three failure modes at once: (1) Juicer claims a vehicle they're not actually near (radius spoof), (2) Juicer uploads a stock photo from a different city (hash collision), (3) Juicer waits days between claim and pickup (timestamp drift). All three were observed during Lime/Bird pilots; all three are blocked here.

Phase 4: Transport and Charge

The Juicer transports the vehicle home or to a swap station. They plug it into a standard wall charger.

The Juicer app periodically prompts: Still charging?. The default cadence is every 30 minutes. The endpoint is POST /api/juicer/bounties/[id]/charge-checkin and accepts a current_soc value.

  • Two missed check-ins in a row → claim expires, vehicle returns to the open pool, small fraud-score demerit.
  • A check-in where current_soc < previous_soc (battery went down) → flagged, manual review.
  • A check-in where current_soc reaches ≥95% → state advances to ready_to_drop and the Drop button activates.

The session tracks pickup_soc, the latest reported SoC, and the elapsed charge duration in seconds.

Phase 5: Drop (Photo + GPS Validation)

The Juicer drives to one of the operator's drop zones and parks the vehicle. They take a drop photo and tap Complete.

The platform validates:

CheckWhat's CheckedWhat Happens If It Fails
Drop zone polygonGPS must be inside an active drop zone (PostGIS ST_Contains)Block — drop rejected, claim stays open
Charge duration plausibilityElapsed charge time vs. SoC delta must be at least 0.6 min per percent (no instant-full claims)Block + manual review
Drop photo hashMust not match a prior drop photoWarning, +20 fraud-score
Pack not in maintenanceThe pack mustn't have been flagged during the sessionBlock + dispute

The drop-zone check uses an active-only zones_containing_point PostGIS RPC over the subaccount's zone table. Drop zones are a subset of the operator's existing zone model — operators flag specific zones as Juicer-eligible from the dashboard.

If all checks pass:

  • bounties.status = 'completed'
  • juicer_sessions.state = 'completed', drop_soc, drop_gps, drop_photo_url recorded
  • A juicer_payouts row is queued in queued state (or on_hold if any fraud flag was logged during the session)
  • The throttle-disable command is reversed — the vehicle returns to normal operation

Phase 6: Payout

Payouts are drained from queued to Stripe Connect every 15 minutes by the juicer-payouts cron. Idempotency keys keep retries safe.

For the full payout flow, including platform fees, dispute resolution, and the 1099 process, see Juicer Payouts.

What the Operator Sees

The dashboard Juicers page shows live sessions per Juicer with state and elapsed time. The Payouts page shows the ledger:

ColumnMeaning
Statequeued / paid / on_hold / reversed / canceled
AmountNet to Juicer in cents
Platform feeLevy's cut
Stripe transfer IDOnce paid; links out to the Stripe dashboard
Session IDClick to see the full claim → drop trail with photos and GPS

Operators can hold, release, reverse, or cancel a queued payout from the dashboard.

What an Expired Claim Looks Like

  • TTL hits (default 60 minutes after claim) → bounties.status flips back to open, the juicer_sessions row moves to expired, the Juicer takes a 3-point fraud-score demerit.
  • Two missed charging check-ins → same outcome, plus a "didn't charge" note in the session.
  • The Juicer's lifetime_no_shows counter increments. Operators see this on the roster.

Common Issues

  • "GPS too far from vehicle" — Juicer is more than 200 m from the vehicle when they tap pickup. The vehicle's last telemetry timestamp is shown for context; if the vehicle just moved, wait for a fresh signal.
  • "Drop outside zone" — the drop GPS is outside any active Juicer-eligible drop zone. The Juicer needs to relocate the vehicle into a zone.
  • "Charge duration implausible" — fewer than 0.6 minutes per percent of SoC delta. Either the Juicer is misreporting check-ins or they didn't actually charge it.
  • "Bounty already claimed" — another Juicer beat them to it. Atomic claim guarantees only one wins.
  • "Vehicle in ride" — the rental started between claim and pickup. The claim expires and the Juicer gets notified.