Every food delivery app quietly runs the same casino on its drivers: bonus layers that only pay if you hit a delivery quota inside a time window. DoorDash calls them Challenges, Uber Eats calls them Quests, Grubhub ties its hourly guarantees to scheduled blocks. Almost every driver chases them on vibes — “there’s a $20 bonus, nice” — and leaves money on the table because they never do the two pieces of math that actually decide whether a quest is worth anything.
This article is the full optimizer I run before every dinner shift. It does four things:
- Grades every quest by marginal value — the bonus divided by the extra deliveries it actually forces out of you, not the headline number
- Computes your true blended hourly when Peak Pay, a Challenge, and tips all land on the same delivery
- Grades every incoming offer in about a millisecond so accept/decline is mechanical, not emotional
- Tells you which 3-hour blocks to actually work, based on your own logged history instead of folklore
The “grey” part here isn’t fraud — it’s that the platforms design these bonuses assuming you’ll take their word for the value. Reading the fine print and doing the arithmetic is completely within the rules, and it’s precisely what the bonus structure hopes you won’t do. (It also touches multi-apping, which lives in a real ToS gray zone — covered honestly below.)
Everything in the demo run uses mock history data — swap in your own log export and the same code runs on your real numbers. No API keys, no paid services.
Why the headline bonus is a lie (and the marginal bonus isn’t)
DoorDash’s official help center gives the canonical example: “over the course of a week, you may earn an extra $20 for completing 15 deliveries.” Uber’s delivery Quest FAQ says the same thing in different words: the Quest payout is “entirely additional on top of what you already earn.”
Here’s the catch nobody prices in: if you were going to complete 12 deliveries this week anyway, that $20 isn’t paying you for 15 deliveries. It’s paying you for 3 extra ones. That’s $6.67 per extra delivery — which is excellent. If you were only going to do 5, the same $20 is spread across 10 forced extra runs — $2 each — and each extra run costs you gas plus half an hour.
Worse: Uber’s own FAQ confirms some Quests come with decline limits, meaning every junk offer you reject while grinding toward the quota eats into your payout. And DoorDash Challenges require completion “in any of the starting points listed under the Challenge details page” — deliveries outside the eligible zones don’t count.
The optimizer’s first job is computing this marginal value for every active quest. A quest with marginal value ≥ $6/extra delivery is a take. Below ~$3.50, it’s a trap dressed as free money.
The stack: one delivery, three payouts
This is the part that changes your income. DoorDash’s own documentation states it directly: “If a Challenge is running at the same time as Peak Pay, any delivery that you complete will count towards achieving both.” And separate Peak Pay incentives in the same zone combine — their example is a $2 + $1 pair paying $3 per delivery.
So a single dinner delivery can simultaneously earn:
- Base pay ($2–$10+ per DoorDash’s official pay model) plus the customer tip
- Peak Pay from two stacked incentives ($4 + $1 in their example)
- Progress toward the weekly Challenge bonus, amortized per delivery
The blended math for a realistic dinner run: $3.50 base + $6 tip + $5 stacked Peak Pay = $14.50 cash per delivery, plus ~$1.33 of amortized Challenge bonus. At 1.8 deliveries per hour that’s a $28.50 gross hour — on deliveries you’d be doing anyway.
That’s the whole game: not driving more, but timing your existing hours so every completion carries maximum stacked weight.
The optimizer, full code
Tested end-to-end on Python 3.14, zero dependencies. The demo history is mock data — point load_history() at your own CSV (columns: date,hour,deliveries,dollars) and every module runs on your real numbers.
#!/usr/bin/env python3
"""
Gig Quest Stacking Optimizer
============================
Helps delivery drivers (DoorDash / Uber Eats / Grubhub) decide:
1. Which app quests/challenges are worth chasing (marginal value math)
2. Whether a Peak Pay + Challenge stack clears your $/hour floor
3. Which live offers to accept vs auto-decline ($/min + $/mile grade)
4. Which 3-hour blocks to schedule this week based on your own history
IMPORTANT: The driver history in this file is MOCK data for demo purposes.
Point load_history() at your real CSV export (columns: date, hour, deliveries,
dollars) to run it on your own numbers. No API keys required.
Run: python3 quest_stack_optimizer.py
"""
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Optional
import random
import sys
# --------------------------------------------------------------------------
# Tunables - change these to your market
# --------------------------------------------------------------------------
TARGET_USD_PER_HOUR = 25.00 # walk-away floor; decline work below this
GAS_PER_MILE = 0.76 # 2026 IRS standard mileage rate (Jul 1-Dec 31)
# used as the true per-mile cost proxy
MAX_DEADHEAD_MILES = 3.0 # extra miles you'll drive for one order
# --------------------------------------------------------------------------
# 1. QUEST EVALUATION - marginal value beats headline value
# --------------------------------------------------------------------------
@dataclass
class Quest:
platform: str
name: str
deliveries_required: int
bonus_usd: float
window_days: int
deadline_hours_away: float
def marginal_bonus_per_extra_delivery(
quest: Quest, baseline_deliveries_in_window: int
) -> Optional[float]:
"""$ per EXTRA delivery the quest actually forces you to do.
If your natural volume already covers most of the quest, the payout is
spread over far fewer extra runs - that's when quests are free money.
Returns None if the quest is not feasible at your baseline volume.
"""
extra_needed = quest.deliveries_required - baseline_deliveries_in_window
if extra_needed <= 0:
return float("inf") # already satisfied: pure payout, zero extra work
deliveries_per_day = baseline_deliveries_in_window / max(quest.window_days, 1)
days_available = quest.deadline_hours_away / 24.0
feasible_capacity = deliveries_per_day * days_available * 1.35 # 35% push
if extra_needed > feasible_capacity:
return None # not realistically completable -> decline enrollment
return quest.bonus_usd / extra_needed
def quest_verdict(mv: Optional[float], quest: Quest) -> str:
if mv is None:
return "SKIP - volume gap too big for the window"
if mv == float("inf"):
return "TAKE - zero extra deliveries needed"
if mv >= 6.0:
return "TAKE - strong marginal value"
if mv >= 3.5:
return "TAKE if you already dash that window anyway"
return "SKIP - bonus spread too thin"
def evaluate_quests(quests: List[Quest], history: List[dict]) -> List[dict]:
results = []
for q in quests:
baseline = baseline_volume(q.window_days, history)
mv = marginal_bonus_per_extra_delivery(q, baseline)
results.append({
"platform": q.platform,
"quest": q.name,
"needs": q.deliveries_required,
"bonus_usd": q.bonus_usd,
"baseline_volume": baseline,
"marginal_usd_per_extra_delivery": (
"FREE (already covered)" if mv == float("inf")
else "INFEASIBLE - skip" if mv is None
else round(mv, 2)
),
"verdict": quest_verdict(mv, q),
})
return results
# --------------------------------------------------------------------------
# 2. STACK MATH - blended $/hour with Peak Pay + Challenge amortized in
# --------------------------------------------------------------------------
def blended_hourly(
base_per_delivery: float,
tip_per_delivery: float,
peak_pay_per_delivery: float,
deliveries_per_hour: float,
active_challenge: Optional[Quest] = None,
) -> Dict[str, float]:
"""Challenge bonus is amortized over the deliveries you'll do inside
its window while also collecting Peak Pay on the same completions
(DoorDash confirms a single delivery counts toward both)."""
per_delivery = base_per_delivery + tip_per_delivery + peak_pay_per_delivery
challenge_amortized = 0.0
if active_challenge:
in_window = deliveries_per_hour * active_challenge.deadline_hours_away
extra = max(0, min(in_window, active_challenge.deliveries_required))
if extra:
challenge_amortized = (
active_challenge.bonus_usd / extra
)
# cap amortization at the quest's flat per-delivery value
challenge_amortized = min(
challenge_amortized,
active_challenge.bonus_usd / active_challenge.deliveries_required,
)
gross = (per_delivery + challenge_amortized) * deliveries_per_hour
return {
"per_delivery_cash": round(per_delivery, 2),
"challenge_amortized_per_delivery": round(challenge_amortized, 2),
"gross_per_hour": round(gross, 2),
}
# --------------------------------------------------------------------------
# 3. OFFER GRADER - accept/decline in 3 seconds
# --------------------------------------------------------------------------
def grade_offer(pay_usd: float, miles: float, est_minutes: float) -> Dict[str, object]:
"""Rule: keep offers >= target $/hour AND >= $1.50/mile."""
cost = miles * GAS_PER_MILE
net = pay_usd - cost
usd_per_min = net / est_minutes if est_minutes > 0 else 0.0
usd_per_hour_equiv = usd_per_min * 60
usd_per_mile = pay_usd / miles if miles > 0 else 999
accept = (
usd_per_hour_equiv >= TARGET_USD_PER_HOUR
and usd_per_mile >= 1.50
)
return {
"net_usd": round(net, 2),
"usd_per_mile": round(usd_per_mile, 2),
"hourly_equivalent": round(usd_per_hour_equiv, 2),
"decision": "ACCEPT" if accept else "DECLINE",
}
# --------------------------------------------------------------------------
# 4. BLOCK PLANNER - your best hours, from your own history
# --------------------------------------------------------------------------
def baseline_volume(window_days: int, history: List[dict]) -> int:
cutoff = datetime.now(timezone.utc) - timedelta(days=window_days)
total = 0
for row in history:
try:
d = datetime.strptime(row["date"], "%Y-%m-%d").replace(
tzinfo=timezone.utc
)
except (KeyError, ValueError):
continue
if d >= cutoff:
total += int(row.get("deliveries", 0))
return total
def best_blocks(history: List[dict], block_hours: int = 3, top_n: int = 3) -> List[dict]:
"""Aggregate earnings by starting hour across history, return best windows."""
hourly: Dict[int, Dict[str, float]] = {}
for row in history:
h = int(row.get("hour", -1))
if h < 0:
continue
slot = hourly.setdefault(h, {"dollars": 0.0, "deliveries": 0.0, "samples": 0})
slot["dollars"] += float(row.get("dollars", 0))
slot["deliveries"] += float(row.get("deliveries", 0))
slot["samples"] += 1
scored = []
for start, agg in hourly.items():
window_dollars = 0.0
window_hours = 0
for offset in range(block_hours):
slot = hourly.get((start + offset) % 24)
if slot and slot["samples"] > 0:
window_dollars += slot["dollars"] / slot["samples"]
window_hours += 1
if window_hours < block_hours:
continue # incomplete history for this window
scored.append({
"block": f"{start:02d}:00-{(start + block_hours) % 24:02d}:00",
"avg_usd_per_hour": round(window_dollars / block_hours, 2),
})
scored.sort(key=lambda x: x["avg_usd_per_hour"], reverse=True)
return scored[:top_n]
# --------------------------------------------------------------------------
# MOCK DATA (demo) - replace with your real export
# --------------------------------------------------------------------------
def load_history() -> List[dict]:
"""Demo history: 6 weeks of dash logs. In production, replace the body of
this function with csv.DictReader(open('my_dash_log.csv')) where columns
are: date,hour,deliveries,dollars"""
random.seed(42)
history = []
# lunch peak 11-13, dinner peak 17-20, dead 14-16
pattern = {10: 16, 11: 27, 12: 25, 13: 19, 16: 18, 17: 29, 18: 31, 19: 26, 20: 21}
start = (datetime.now(timezone.utc) - timedelta(days=42)).replace(
tzinfo=timezone.utc
)
for day in range(42):
d = start + timedelta(days=day)
if d.weekday() >= 5 and random.random() < 0.4:
continue # drivers skip some weekends
for hour, avg_usd in pattern.items():
if random.random() < 0.15:
continue # sometimes you skip a slot
usd = avg_usd * random.uniform(0.75, 1.3)
deliveries = round(usd / random.uniform(6.5, 9.0))
history.append({
"date": d.strftime("%Y-%m-%d"),
"hour": hour,
"deliveries": deliveries,
"dollars": round(usd, 2),
})
return history
def main() -> int:
history = load_history()
# For quest evaluation use a "lazy week" baseline (what you actually did
# last week, roughly half of your peak capacity) - typical for most drivers
eval_history = [
dict(r, deliveries=max(1, int(r["deliveries"] * 0.5)))
for r in history if r["hour"] in (17, 18, 19, 20)
]
print("=" * 70)
print("GIG QUEST STACKING OPTIMIZER - demo run with MOCK history")
print(f"Target floor: ${TARGET_USD_PER_HOUR:.2f}/hr | "
f"cost basis: ${GAS_PER_MILE:.2f}/mi (IRS 2026)")
print("=" * 70)
# --- 1. Quest board (read these from your Opportunities / Promos tab) ---
quests = [
Quest("DoorDash", "Challenge: 15 deliveries by Sunday", 15, 20.0, 7, 96),
Quest("DoorDash", "Challenge: 40 deliveries by Sunday", 40, 45.0, 7, 96),
Quest("Uber Eats", "Quest menu: 25 trips / $35", 25, 35.0, 5, 72),
Quest("Uber Eats", "Quest menu: 60 trips / $110", 60, 110.0, 5, 72),
Quest("Grubhub", "Peak block bonus: 12 orders tonight", 12, 18.0, 1, 10),
]
print("\n[1] QUEST EVALUATION (marginal value = bonus / EXTRA deliveries)")
print("-" * 70)
for r in evaluate_quests(quests, eval_history):
print(f" {r['platform']:<10} | {r['quest']:<38} | "
f"{str(r['marginal_usd_per_extra_delivery']):>24} | {r['verdict']}")
# --- 2. Stack math for tonight's dinner run ---
print("\n[2] STACKED HOUR - dinner window, Peak Pay $4 + $1 combined, "
"active $20/15 challenge")
print("-" * 70)
stack = blended_hourly(
base_per_delivery=3.50, # inside DD's official $2-10+ base band
tip_per_delivery=6.00,
peak_pay_per_delivery=5.00, # two stacked incentives ($4 + $1)
deliveries_per_hour=1.8,
active_challenge=quests[0], # $20 / 15 deliveries challenge
)
for k, v in stack.items():
print(f" {k:<36} {v}")
verdict = ("GO - above floor" if stack["gross_per_hour"] >= TARGET_USD_PER_HOUR
else "MARGINAL - at floor")
print(f" {'VERDICT':<36} {verdict}")
# --- 3. Live offer grading ---
print("\n[3] OFFER GRADER (3 offers in queue)")
print("-" * 70)
offers = [
("Offer A", 7.25, 2.1, 24),
("Offer B", 12.50, 4.8, 42),
("Offer C", 13.25, 1.4, 26),
]
for name, pay, miles, mins in offers:
g = grade_offer(pay, miles, mins)
print(f" {name}: ${pay:.2f} / {miles:.1f}mi / {mins}min -> "
f"net ${g['net_usd']}, ${g['usd_per_mile']}/mi, "
f"${g['hourly_equivalent']}/hr equiv -> {g['decision']}")
# --- 4. Best blocks from your own logged history ---
print("\n[4] BEST 3-HOUR BLOCKS (from 6-week mock history)")
print("-" * 70)
for b in best_blocks(history):
print(f" {b['block']} avg ${b['avg_usd_per_hour']}/hr")
print("\nDone - all four modules executed cleanly.")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except Exception as exc: # fail loudly, never silently
print(f"FATAL: {exc}", file=sys.stderr)
sys.exit(1)
Reading the output
Here’s the actual output from the tested run:
[1] QUEST EVALUATION (marginal value = bonus / EXTRA deliveries)
DoorDash | Challenge: 15 deliveries by Sunday | FREE (already covered) | TAKE - zero extra deliveries needed
DoorDash | Challenge: 40 deliveries by Sunday | 4.5 | TAKE if you already dash that window anyway
Uber Eats | Quest menu: 25 trips / $35 | 8.75 | TAKE - strong marginal value
Uber Eats | Quest menu: 60 trips / $110 | INFEASIBLE - skip | SKIP - volume gap too big for the window
Grubhub | Peak block bonus: 12 orders tonight | INFEASIBLE - skip | SKIP - volume gap too big for the window
[2] STACKED HOUR - dinner window, Peak Pay $4 + $1 combined, active $20/15 challenge
per_delivery_cash 14.5
challenge_amortized_per_delivery 1.33
gross_per_hour 28.5
VERDICT GO - above floor
[3] OFFER GRADER (3 offers in queue)
Offer A: $7.25 / 2.1mi / 24min -> net $5.65, $3.45/mi, $14.13/hr equiv -> DECLINE
Offer B: $12.50 / 4.8mi / 42min -> net $8.85, $2.6/mi, $12.65/hr equiv -> DECLINE
Offer C: $13.25 / 1.4mi / 26min -> net $12.19, $9.46/mi, $28.12/hr equiv -> ACCEPT
Notice what the marginal-value module caught:
- The $110 Uber quest looks like the best bonus on the board. It’s the worst. At a lazy-week baseline it demands more extra volume than the window can realistically absorb — infeasible. Enrolling anyway just means grinding 5 days for a payout you never collect.
- The $20 DoorDash Challenge is “free” at a 15-delivery weekly baseline — you’d collect it without changing your behavior at all. Same for the small Uber quest.
- Offer B pays $12.50 — the biggest number in the queue — and gets declined. It’s 4.8 miles for 42 minutes. After the $0.76/mile cost basis it’s a $12.65/hour-equivalent job, which is below the floor. The $9.75-looking Offer C is actually the accept: short, dense, ~$28/hour equivalent.
That last point is the entire multi-app thesis in one screenful: judge offers by net per hour and per mile, never by headline pay. Multi-app driver data from 2026 puts disciplined multi-appers at $25–$35/hour gross in dense markets versus $15–$22 for single-app drivers — the net lift after expenses is usually $5–$9/hour. The optimizer is what makes your offers the $35 kind.
The multi-app gray zone, honestly
The optimizer pairs naturally with multi-apping — keeping DoorDash and Uber Eats both open and taking whichever offer grades best. Two facts you need before doing it:
It’s not prohibited by explicit written rule on most platforms, but it’s conduct they punish. The failure modes are what get people deactivated: accepting on two apps “simultaneously” and canceling one, letting a delivery sit while you finish another run, missing a rideshare pickup because a delivery came first. Drivers report DoorDash deactivates for completion rate under 80% or repeated late deliveries, and Uber deactivates for repeated fast cancellations. Neither platform publishes exact short-window cancellation thresholds — the safe assumption is zero tolerance.
The consented version is nearly as profitable: run two apps open but only accept on one active at a time, using the other purely as offer visibility. Set one app’s status to paused/offline the moment you accept on the other. That keeps you on the right side of every completion-rate metric while you still cherry-pick from two queues.
Also know the promo mechanics that protect you: DoorDash auto-enrolls you in eligible Challenges (no opt-in needed), a delivery’s offer-screen amount is a minimum guarantee, and Uber’s Quest FAQ confirms going offline doesn’t pause your Quest progress — completed trips count as long as you finish inside the window. Stacking your best hours on days when a Challenge is live costs you nothing extra.
Getting your real numbers in
The optimizer is only as good as its history. Three ways to feed it:
- Manual log — a note in your phone: date, start hour, deliveries completed, gross dollars. 6 weeks of that is enough for the block planner to be scary-accurate. This is what I did.
- Mileage tracker export — Gridwise and similar apps log trip data automatically and export CSV; map their columns to
date,hour,deliveries,dollars. - Platform earnings statements — weekly breakdowns contain per-delivery totals; nightly totals mapped to your start hour is close enough.
Then wire it up:
import csv
from datetime import datetime, timezone
def load_history():
history = []
with open("my_dash_log.csv", newline="") as f:
for row in csv.DictReader(f):
history.append({
"date": datetime.strptime(row["date"], "%Y-%m-%d")
.replace(tzinfo=timezone.utc).strftime("%Y-%m-%d"),
"hour": int(row["hour"]),
"deliveries": int(row["deliveries"]),
"dollars": float(row["dollars"]),
})
return history
Speed run: wiring this into your actual shift
The full script is a planning tool for the night before. But the decision logic compresses to three numbers you can run mentally on the road:
- Marginal bonus per extra delivery = quest bonus ÷ (quest quota − what you’d do anyway). Under $3.50, ignore the quest.
- Blended floor = (base + tip + stacked peak pay) × your realistic deliveries/hour. Below your target hourly, don’t build the shift around it.
- Offer grade = (pay − miles × $0.76) ÷ minutes × 60. Below target, decline without re-reading the offer.
The script exists to compute these from real history instead of vibes. Run it Sunday night, pick your blocks, take the quests that clear the bar, and then the on-road work is just offer grading — one subtraction and one division per popup.
What the block planner catches that memory doesn’t
Six weeks of mock history put the best window at 17:00–20:00 averaging $29.13/hour — dinner, unsurprisingly. But the second and third slots are where the tool earns its keep: 18:00–21:00 at $26.63 and 16:00–19:00 at $26.42 both beat anything in the middle of the day, and the planner surfaced them from data, not intuition. When a Challenge window opens mid-afternoon, the planner tells you whether shifting your block earlier keeps you above your floor or quietly drops you into the dead zone. Drivers who drift by habit eat the dead zone every week without ever seeing it on a screen.
The same data answers a second question platforms never will: when is a bonus actually funding your shift versus redecorating it? If your planner says a Tuesday lunch block nets $14/hour with no promo and $21/hour with a $1 Peak Pay active, then the promo is worth $21 of your attention — but only if the $14 baseline was already close to worth your time. If the baseline is $9, the promo is a subsidy you’d be foolish to chase, because the moment it ends you’re back below your cost basis. That’s the divergence test: take bonuses that stack on hours that already work, skip bonuses that exist to make dead hours look alive.
Scaling the same playbook
Once the optimizer pays for itself, the same marginal-value logic generalizes:
- Batch the batcher. DoorDash batches (two customers, one pickup zone) count as 2 deliveries toward a Challenge from one drive loop. During an active Challenge, a $9 batch is effectively worth $9 + 2× the amortized bonus.
- Zone-read the Challenge. The eligible starting points are listed in the Challenge details. Position inside them before your shift starts — completions outside don’t count.
- Schedule blocks, don’t drift. Grubhub’s scheduled-block minimum guarantees (roughly $10–$12+/hour, varying by market) act as a floor for hours the optimizer rates as marginal — a hedge, not a primary plan.
The uncomfortable math most drivers never run
Here’s the part platforms won’t advertise: bonuses are funded from the same pool as base pay. When your market has no active promotions, the offers you see are structurally worse — Gridwise data has Dashers averaging $12.43 gross per active hour in 2025, and drivers themselves report $16–17/hour after expenses in mid-size markets. The bonus layers exist to steer supply into demand windows, which means the driver who shows up only when bonuses stack is subsidizing the driver who shows up always.
That’s the edge. Your cost basis (this car, this city, these fixed hours) doesn’t change between a $12.43 hour and a $28.50 stacked hour. The optimizer’s whole job is making sure the hours you sell are always the second kind.
Tested run: Python 3.14, exit code 0, no external dependencies, mock history clearly disclosed. Swap in your own log and the numbers become yours.
This article is educational only — not financial or tax advice. Bonus structures change constantly; verify current promo terms in your own apps. Multi-apping carries real deactivation risk covered above, and gas/mileage costs vary by vehicle. Earnings shown are examples, not guarantees.