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_claimscap - Bounty is
openand 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
maintenanceorrecalled - Juicer is within their
claim_radius_mof 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_sessionsrow is inserted inclaimedstate - 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:
| Check | Limit | What 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 timestamp | Within 5 min of the Claim tap | Warning logged, +3 fraud-score |
| Photo hash collision | Hash must not match any prior pickup photo from this Juicer | Block — repeated stock-photo attempt, +20 fraud-score |
| Vehicle ID visible (OCR) | Vehicle number must be OCR-readable | Soft 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_socreaches ≥95% → state advances toready_to_dropand 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:
| Check | What's Checked | What Happens If It Fails |
|---|---|---|
| Drop zone polygon | GPS must be inside an active drop zone (PostGIS ST_Contains) | Block — drop rejected, claim stays open |
| Charge duration plausibility | Elapsed charge time vs. SoC delta must be at least 0.6 min per percent (no instant-full claims) | Block + manual review |
| Drop photo hash | Must not match a prior drop photo | Warning, +20 fraud-score |
Pack not in maintenance | The pack mustn't have been flagged during the session | Block + 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_urlrecorded- A
juicer_payoutsrow is queued inqueuedstate (oron_holdif 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:
| Column | Meaning |
|---|---|
| State | queued / paid / on_hold / reversed / canceled |
| Amount | Net to Juicer in cents |
| Platform fee | Levy's cut |
| Stripe transfer ID | Once paid; links out to the Stripe dashboard |
| Session ID | Click 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.statusflips back toopen, thejuicer_sessionsrow moves toexpired, 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_showscounter 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.