Driver tips
Status: built and shipped behind per-market config.
Decision date: 2026-09-05.
Code: backend/src/tips/, WalletService.tipDriver, migration 1700000064000-Tips.
What was wrong
ride_ratings.tip_minor had existed since migration 35. The rating endpoint wrote it,
the rider receipt printed it, and nothing ever posted it. No ledger entry, no wallet
movement, no line in the driver's earnings. A rider tapped "₹20", read
"Submit + tip ₹20", and the driver was paid exactly the fare.
That is the worst shape a money bug can take: both parties were confidently told
something different, and neither had any way to find out. It is also why the obvious
fix — "post the tip inside submit()" — was never made. Three questions blocked it, and
the answers are in §3, §4 and §5.
1. What the research says
Twenty-six sources per platform, three parallel searches; the full citation list is at the end. The findings that actually changed the design:
Timing. Every operator that documents it takes the tip after the trip. Uber offers it during the ride, at drop-off in the rating flow, and later from the receipt (Uber's own help pages give three mutually inconsistent windows — 30, 40 and 90 days — and I could not determine which is authoritative). Lyft: 2 hours on the Rate & Pay screen, 30 days from history. Bolt: 2 hours. Grab: 72 hours. Careem: 24 hours. Gojek: during or after.
The pre-ride tip is dead, and it was killed twice. Rapido, Uber India, Ola and inDrive all shipped an "Advance Tip" prompt shown before matching, with copy implying a driver was more likely to accept. India's CCPA issued a notice to Uber in May 2025; on 11 August 2026 MoRTH ordered all four to remove it, under clause 14.15 of the Motor Vehicle Aggregator Guidelines 2025, which permits a tip feature only after the journey completes and requires the entire tip to reach the driver with no aggregator deduction. Shanghai made DiDi do the same thing in January 2017.
Cash rides: the single most consistent finding in the research. Grab, Gojek, Careem, Uber and Bolt all gate in-app tipping on a cashless payment method and tell cash riders to tip in cash. Uber's own US cash-trips launch post: "While a rider will not be able to leave a tip in-app, they can still tip directly with cash." Bolt: "If you selected cash as your payment method, you can tip a driver in cash." Lyft is card-only and won't even let you tip from Lyft credit. Uber India launched tipping in 2020 Paytm-trips-only. There is no precedent among the researched platforms for an in-app tip control on a cash trip.
Commission: 0%, universally. Uber ("zero service fees on tips"), Lyft ("100% of tips go to drivers"), Bolt ("commission fees don't apply to tips"), Grab, Careem, Gojek, Ola and Rapido all state it publicly. It is law in India (clause 14.15) and in Ontario (Digital Platform Workers' Rights Act, in force 1 July 2025: no deductions from amounts earned, including tips, regardless of employment classification). The one documented exception is GO in Japan, which nets payment-processing costs off the tip before paying the driver.
Payout. Careem: instant to Careem Pay. Gojek: straight into the driver's GoPay wallet. Uber: "Tips will be available immediately" for Instant Pay. Lyft: tips are Express-Pay eligible. Bolt and Ola: with the payout cycle. Instant-to-wallet is best in class.
Editing. Uber rides: you can add to a tip, never reduce one — "After that, you can only add to the tip you initially gave… You won't be able to adjust the tip amount." Tip-baiting exists on Uber Eats, where a pre-order tip can be changed for an hour after delivery — Uber shipped a "tip guarantee" in November 2025 to cover couriers for it. Bolt is the outlier that lets a rider remove a tip within 2 hours. Lyft does not document editing at all.
Rating coupling. Uber requires a rating before you can tip. Lyft decouples them entirely. Bolt and Grab gate the tip on a high star rating (Bolt 4–5, Grab 5); Careem hides it at ≤3 stars.
Culture. Ordinary: US, Canada, UK, India, the Gulf, Egypt, the Philippines. Not expected: Australia, New Zealand, Brazil, Mexico, Vietnam, Thailand, Malaysia, Nigeria. Actively awkward: Japan, South Korea, Singapore, China. Two instructive cases:
- Japan — the cultural claim is true and the product claim is false. Tipping is a genuine social error there, and yet Uber (2021), GO (2023) and DiDi (Sept 2026) have all shipped a tip control, mostly for inbound tourists. So Japan is not a market where the feature is impossible; it is a market where the operator should decide.
- South Korea — Kakao T piloted tipping in 2023. 71.7% of surveyed users opposed it. It was never made permanent. That is what shipping a market ON because the software can looks like.
Tax — and here the sources are unusually good
One rule survives every jurisdiction: a payment is taxable only if it is consideration for a supply. A tip the passenger was free to withhold, free to size, and that reaches the driver, is not consideration.
- EU/UK. CJEU C-16/93 Tolsma para 14: consideration requires "a legal relationship … pursuant to which there is reciprocal performance". Para 17 — voluntary payments fail it because the payer "determine[s] [the amount] as they wish" and pays on "subjective motives". The mirror case, C-404/99 Commission v France, held compulsory service charges taxable, and para 43 says it is immaterial that the money is shared out in full to staff. Best single citation for this business: HMRC VAT Notice 700/25 §1.2, which is about taxis and private hire cars specifically: "Tips or gratuities voluntarily given by passengers are not regarded as payment for a supply and are outside the scope of VAT."
- UAE. FTA Taxable Person Guide VATG001 §5.5: a service charge is further consideration and follows the ride's liability; "if the customer, however, freely gives a tip over and above the total charge, no VAT is due on the tip and it is outside the scope of VAT."
- Saudi Arabia. ZATCA's Recreation & Entertainment Guideline §7.4 is more explicit than the UAE's: a "truly discretional additional amount" is "not Consideration for the services, and is outside the scope of VAT", and where collected by card "the amount of the discretionary tip must be clearly marked on the invoice." Caveat: ZATCA's condition is that the customer "has not been provided any 'suggested' tip amount", and the tips guidance sits in the hospitality guideline — ZATCA's transport guideline says nothing about tips.
- India — the weakest link, and we should be honest about it. s.15(1) CGST fixes value
at "the price actually paid or payable"; s.15(2)(c) only catches amounts "charged by the
supplier". Both point away from taxing a tip. But s.2(31) defines consideration
inclusively as any payment "in respect of, in response to, or for the inducement of" the
supply — textually wider than the CJEU test, and India has no Tolsma. There is no
CBIC circular, no AAR ruling and no departmental clarification on tips under GST. The
supporting analogies are the Delhi High Court's 2025 service-charge judgment (voluntary
tipping lawful; mandatory service charge is not) and ITC Ltd v CIT (SC, 2016), which
held a hotel receives tips "in a fiduciary capacity as trustee" — the strongest Indian
framing for a platform that passes 100% through. India's e-invoice schema does have the
right field if we ever need it:
ValDtls.OthChrgsits outside item-level assessable value and outside the tax computation. - US. Voluntary tips are not sales-taxable; mandatory gratuities usually are. NY TSB-M-09(7)S is transportation-specific: "If a passenger gives a gratuity directly to the driver voluntarily, that is not a charge for transportation service and is not subject to sales tax." Rideshare levies are defined on "gross trip fare" = base + distance + time (NY Tax Law §1291(g); SC §58-23-1610(A)), which excludes a tip by construction. Nevada's NRS 372B.140 is drafted inclusively and is silent on tips — unresolved.
Two rules fall out of the tax research that constrain the UI, not just the ledger:
- No default-selected or pre-filled tip. California Reg 1603(h)(3)(C) presumes an amount the retailer adds to the bill is mandatory — and says explicitly that labelling it "suggested", "optional" or "may be removed" does not change that. ZATCA conditions its out-of-scope treatment on there being no "suggested" amount. IRC §224(d)(2) requires the amount be "determined by the payor"; the final regs (TD 10044) Example 9 confirm a suggestion the customer can change or eliminate still qualifies. A zero default with suggestions displayed beside it survives all three.
- Never call it a "service charge." In the UK, UAE, Saudi Arabia and New York that word alone converts it into taxable consideration.
2. The model
A tip is a payment, not a preference. It is given after the trip, from the rider's own wallet, to the driver's wallet, in full, immediately, and once.
Every clause is load-bearing and each is defended below.
Post-trip only, architecturally. There is no pre-ride or in-ride tip in this platform
and no configuration that can produce one: the endpoint refuses any ride that is not
COMPLETED. That is not an operator preference — a "tip to get matched faster" is a
dispatch-priority auction wearing the word gratuity, it is banned in India and China, it
was the trigger for a national dark-patterns investigation, and under both India's s.2(31)
and US §224(d)(2) it stops being a tip in law. If a market wants priority pricing, that
belongs in the fare.
Never on the rating. Uber makes you rate before you can tip. We do the opposite, deliberately: the rating is submitted first and committed, and only then is the tip attempted. A tip that cannot be paid must never cost a rider their rating, and a driver's star average must never be a toll gate on a gratuity. This is Lyft's model. We also do not copy Bolt's and Grab's star gates: a rider who gives three stars and still wants to tip for the heavy suitcase should be able to.
Instant, and withdrawable. The credit lands in the driver's wallet the moment it posts, and payouts draw from that balance — Careem/Gojek/Uber-Instant-Pay class, not Ola's next-cycle. This is also why our tipping window is short (§6): we are not holding the money, so there is nothing to reverse out of.
No commission, and no knob for one. WalletService.tipDriver posts exactly two legs.
There is no bps anywhere in the method and no third posting, so no configuration mistake can
route a slice of a gratuity into platform_commission. Nor do we deduct payment-processing
cost the way GO Japan does — and here we have an advantage worth stating: our tip is an
internal transfer between two ledger accounts. The rider's money was already in their
wallet; no gateway is touched, so there is no processing cost to absorb or pass on. The
100% is real, not a subsidy.
3. Cash rides — decided: not offered by default; a wallet-funded tip is an operator opt-in
A cash fare never passes through this platform. There is no instrument to charge, so an in-app tip on a cash trip can only come from one place: the rider's wallet balance.
We could offer that. Grab, Gojek, Careem, Uber and Bolt all decided not to, and their reason mostly does not apply to us (they lack a wallet to charge; we have one). But their riders' experience is identical to ours would be: money leaving an account they did not expect to be touched, minutes after they already paid the driver in notes. That is a surprise debit, and it is a bad one in exactly the cash-heavy markets where the feature would fire most often.
So the shipped default is cashRideTips: 'off', and an operator can set 'wallet' per
market. When it is off, the rider app does not render a dead control: it renders one
sentence — "You paid this trip in cash — you can tip your driver in cash too" — which is
Grab's and Careem's own guidance and is actually useful. When it is on, the rider is told
plainly, before committing, that the tip comes from their wallet balance and not from the
fare they already paid (fromWalletOnNonWalletRide in the API, tipFromWallet in the
catalogs).
Corporate rides are grouped with cash rides under the same switch. A company's money cannot fund a gratuity its employee chose personally — that is an expense-policy problem, not a payments one. When the switch is on, the tip debits the rider's own wallet, which is exactly Uber's rule for business trips ("charged to the rider's personal account"); when it is off, no control appears, which is Bolt's rule ("business payment method → no tip option"). One knob, both precedents.
What we did not build: a card-on-file charge for a cash ride. This platform has no stored-card rail for rides — cards fund the wallet — so this is the same decision.
4. Insufficient balance — decided: refuse cleanly, with the shortfall, and offer a top-up
There is no partial tip, no IOU and no half-succeeded anything. TipsService.tip() reads the
balance and refuses before the ledger is touched, returning a structured error the app can
act on:
{ "code": "insufficient_balance", "availableMinor": "1500",
"requiredMinor": "2000", "shortfallMinor": "500" }
The rider app turns that into "Add ₹5.00 to your wallet to send this tip" plus an Add
money button that routes to the top-up flow. The rating is already saved by then, so
nothing is lost by going and coming back — and TipsService is idempotent on the ride, so
the tip can be completed later from the trip receipt.
Three defences, not one:
TipPickerwarns as soon as the rider selects an amount above their balance — before they press anything. (Only after they select: a balance line above an untouched tip sheet is nagging, not help.)TipsServicerefuses with the shortfall, posting nothing.- The ledger's own
allow_negative = falsecheck backstops the race between the read and the write. A rider wallet cannot go negative even if two requests interleave.
5. Editing — decided: a rating can be edited; a tip cannot
This is the question the task said was legitimate to answer either way. Here is the argument.
A tip is not an attribute of a rating. It is a payment that lands in a withdrawable balance the instant it posts — the driver may have cashed it out before the rider gets home. There is no window during which taking it back is free for the person it was given to, which is precisely the difference between us and every platform that does allow a change: Uber Eats holds the tip for an hour before paying it, and that hold is the entire mechanism that makes tip-baiting possible. Bolt allows removal within 2 hours; it is the sole outlier among the researched ride platforms, and it is the design that produced tip-baiting in US delivery. Uber on rides permits adding only — never reducing — for exactly this reason.
So:
- Re-submitting the same amount is an idempotent no-op. A retry, a flaky network, or an
old client resubmitting a rating that carries
tipMinorall land here. Nothing posts twice. - Submitting a different amount is refused in words (409,
already_tipped, with the amount that stands). - There is no reduce and no withdraw. An operator can still make a rider whole through the admin wallet adjustment, which is the honest instrument for a genuine mistake, and it leaves an audit trail rather than silently un-paying a driver.
Where this diverges from Uber: Uber lets a rider add to a tip after the trip. We do
not, and the cost of that is a rider who wants to give more must ask support. The gain is
that a ride carries exactly one tip entry, ever — the ledger idempotency key is
ride-tip-<rideId> with no amount in it, and ride_tips has a unique index on ride_id.
That makes the double-credit failure class structurally impossible rather than merely
tested-against, and given that the bug being fixed here was a money bug, buying that
guarantee for the cost of a rare support ticket is the right trade. If top-ups are wanted
later, the shape is known: monotonic totals with the key ride-tip-<rideId>-<newTotal>.
On ride_ratings being an UPSERT. The rating path no longer writes tip_minor at all.
It could not: overwrite it and a rider silently erases a tip they already gave; add to it and
the same rating submitted twice charges twice. Neither reading is right, because the amount
was never a payment. TipsService is now the single writer of that column and keeps it
equal to what the ledger actually moved, so the legacy column stops contradicting reality.
6. The window
72 hours from drop-off, market-configurable (1–8760h). That is Grab's window and the longest any researched operator runs; Careem and GO Japan use 24. We deliberately do not copy Uber's ~30 days: Uber holds the tip until its payout runs, whereas we credit a withdrawable balance immediately, and a month-old surprise debit against a rider's wallet is a different thing from a month-old adjustment to a pending payout.
The window is what makes the trip receipt a real tipping surface: a rider who dismissed the rating screen can still tip two evenings later, which is where most of Uber's, Grab's and Careem's late tips come from.
7. Tax and the invoice — decided: the tip never appears on the issued tax invoice
Two independent reasons, and they agree.
The legal one. A voluntary gratuity is not consideration for the transport supply, so it
is outside VAT/GST everywhere the research reached: Tolsma and HMRC 700/25 §1.2 for the
UK/EU, FTA VATG001 §5.5 for the UAE, ZATCA R&E §7.4 for Saudi Arabia, TSB-M-09(7)S for New
York. The tax invoice documents the taxable supply; the tip is not part of it, so it does not
belong in subtotalMinor and must never enter the tax computation.
The mechanical one. TaxService.issueForRide runs at completion, assigns a gapless
per-(country, series) number, and files with the authority. The tip can arrive 72 hours
later. A filed e-invoice — IRP-signed in India, ZATCA-cleared in Saudi Arabia — cannot be
edited; changing one means a credit/debit note. Re-issuing a numbered fiscal document to add
a non-taxable gratuity would be a compliance hazard invented to solve a problem that does not
exist.
What the rider actually sees. The receipt screen shows fare lines, then tax, then the tip
as its own line below the tax, then a total that adds all three. invoice_total_minor is
the tax invoice total (fare + tax) and excludes the tip; the receipt adds the tip back so
the arithmetic on screen is self-consistent. This matches ZATCA's own instruction that a
card-collected discretionary tip "must be clearly marked on the invoice", and NY
TSB-M-09(7)S Example 9's layout: taxable base = fare + tolls + surcharges, voluntary gratuity
outside it.
Not built, deliberately: an OthChrg / tax-category-O line inside the fiscal document.
It would be defensible in India, but ZATCA has never connected tips to the Fatoora rules at
all, and adding a line to a filed document is strictly worse than omitting an out-of-scope
amount from it.
Flagged for the operator, not solved here:
- India, s.194-O TDS. CBDT Circular 20/2021 is silent on whether a tip belongs in the "gross amount" an ECO deducts 0.1% from. Genuinely unresolved; needs Indian counsel. The conservative choice costs 0.1% and is creditable to the driver.
- US, from TY2026. §6050W(f)(2), as amended by OBBBA, requires the 1099-K payee statement
to break out cash tips separately with an occupation code — Treasury Tipped Occupation Code
802, which expressly names "platform/app-based rideshare driver". Tips not separately
reported are not eligible for the driver's §224 deduction (Treas. Reg. §1.224-1(d)(1)).
This platform issues no 1099s, so nothing is broken today;
ride_tipsplus thetipledger reason give per-driver, per-period tip totals, which is exactly the input such a report needs. - UK. If drivers are limb-(b) workers (Uber BV v Aslam [2021] UKSC 5), app-collected tips are likely "employer-received tips" under ERA 1996 Part 2B. We already satisfy the substance: s.27G requires payment by the end of the following month (we pay instantly) and s.27C(6) disregards any deduction (we deduct nothing). The records duty (s.27J, three years) is met by the immutable ledger. No source confirms the Act has ever been applied to a ride-hailing platform — every worked example in the statutory Code is hospitality.
8. Market configuration
markets.config.tipping, the same jsonb pattern as parcel, rental, intercity and
fareIntegrity. Shape, defaults and merge live in backend/src/tips/tipping-config.ts; the
market entity carries it as a loose index so the tips module owns its own contract.
| field | meaning |
|---|---|
enabled |
master switch; read strictly (=== true) |
presetsMinor |
the ladder, minor units, ascending, ≤6 |
maxMinor |
ceiling on one ride's tip |
cashRideTips |
'off' | 'wallet' — §3 |
windowHours |
1–8760, default 72 — §6 |
Fails closed in both senses. A missing market row, a malformed section or a lookup that
throws yields enabled: false: no control renders and the endpoint refuses. A tip is a real
debit against a real wallet, and the cost of guessing wrong the other way is charging someone
in a market whose operator never switched the feature on.
Shipped ON in: US, CA, GB, IN, AE, SA, QA, KW, OM, EG, PH — the markets where tipping a
driver is an ordinary local custom and the incumbent ships one. Everywhere else ships OFF,
already priced, one click from on. Japan (not currently a shipped market) would ship off, and
JPY's zero-decimal handling is covered by the preset derivation.
Where the preset amounts come from. Not a currency conversion: step = niceTipStep(0.15 × the market's own economy minimum fare) snapped to the 1-2-5 ladder, with presets [s, 2s, 5s] and max = 10 × top. The market's minimum fare already carries local cost of living
(markets/fare-presets.ts applies both FX and a local price index). The check that the
formula measures the right thing is that it independently reproduces what the real apps show:
| market | derived | what the incumbents show |
|---|---|---|
| India | ₹10 / ₹20 / ₹50, max ₹500 | Ola & Rapido's ladder |
| US | $1 / $2 / $5, max $50 | Uber US exactly; Lyft caps tips at $50 |
| Brazil | R$1 / R$2 / R$5 | — |
| Indonesia | Rp 5,000 / 10,000 / 25,000 | Gojek's published average tip is Rp 5,000 |
| Philippines | ₱20 / ₱40 / ₱100 | ₱20–50 is the reported norm |
| Saudi Arabia | SAR 2 / 4 / 10 | 5–10 SAR described as courteous |
| Kuwait | KWD 0.200 / 0.400 / 1.000 | (3-decimal currency handled) |
9. The three surfaces, which now agree
Three surfaces disagreeing about money is the bug class already found in this codebase, so each reads the same ledger:
- Rider receipt (
RidesService.receiptForRider) — joinsride_tips, notride_ratings.tip_minor.NULLhonestly means "not tipped" and is not coalesced to zero. - Driver earnings (
WalletService.driverEarnings) — tips are their own per-bucket figure (tipsMinoron today/week/month/lifetime) and their own per-trip amount (tipMinor), never folded intonetMinor. The screen states once that the driver keeps 100%. The earnings query previously summed reasoncashbackfor its tips figure, which nothing in the codebase has ever posted — the number was structurally zero. - Admin trip settlement (
TripMoney) — the settlement query now selectsle.reason, so the panel finds the fare's driver leg rather than the tip's, and renders the tip as its own line hinted "100% to the driver". "Settled" now means the fare settled, so a tip cannot silence the completed-but-unsettled warning.
10. Deliberately not built
- Custom tip amounts. Presets only, like Grab ("customised tipping is not yet available"). Operators reprice the ladder per market. Adding a free-text amount is a contained change if wanted.
- Adding to an existing tip. §5.
- Rating-gated tips (Bolt 4–5★, Grab 5★, Careem hidden ≤3★). Deliberately rejected — a gratuity should not be hostage to a star rating.
- Driver opt-out of in-app tips. Uber has this; we do not.
- Tip matching. Careem's Ramadan campaign matched every tip ≥ AED 10 dirham-for-dirham, reaching 31,000+ captains. The most copyable growth mechanic in the research, and out of scope here.
- Restricting tips from promotional credit. Uber forbids tipping from promotions; Lyft
forbids it from Lyft credit. This platform has one rider wallet balance with no
promotional sub-ledger — a referral reward (
ReferralService.creditReward) lands as ordinary spendable balance and is indistinguishable from a top-up at tip time. So a referral credit can fund a tip. The driver is paid real money either way, so this is a platform-cost question rather than a fraud hole, but it is a real divergence and closing it would need a restricted-balance concept the wallet does not have. - US tip reporting. §7.
Sources
Platform documentation — Uber: in-app tips · Uber: editing a tip amount · Uber: cash trips launch · Uber India tipping launch (2020) · Uber: tip guarantee · Lyft: how to tip your driver · Lyft: Express Pay · Bolt: how to tip my driver · Bolt: how to check my tips · Grab MY in-app tipping · Grab: where tips are credited · Careem: tipping my Captain · Careem tip matching · Gojek engineering: how we do driver tips · Ola: in-app tipping, globally · Rapido: tip your Captain (2018) · GO Japan tipping (fees deducted) · Uber Japan tipping (2021)
Regulation — MoRTH bans pre-ride tipping, Aug 2026 · Free Press Journal: the MoRTH advisory · CCPA notice to Uber over "Advance Tip" · Shanghai orders DiDi to stop pre-pickup tips (2017) · Ontario Digital Platform Workers' Rights Act · Employment (Allocation of Tips) Act 2023 · Statutory Code of Practice on tips · Uber BV v Aslam [2021] UKSC 5
Tax — CJEU C-16/93 Tolsma ·
CJEU C-404/99 Commission v France ·
HMRC VAT Notice 700/25 §1.2 — taxis and private hire ·
HMRC VATSC06130 ·
UAE FTA Taxable Person Guide VATG001 §5.5 ·
ZATCA Recreation & Entertainment Guideline §7.4 (archived) ·
CGST Act s.15 ·
CGST Act s.2(31) ·
NIC e-invoice schema — ValDtls.OthChrg ·
ITC Ltd v CIT (SC 2016) ·
NY TSB-M-09(7)S — transportation services ·
CDTFA Regulation 1603(h) ·
26 U.S.C. §6050W ·
TD 10044 — tipped occupations, TTOC 802
Culture — Kakao T tipping pilot, 71.7% opposed · Tipping in Japan · Tipping in Singapore · Mexico's tipping culture · Baksheesh in Egypt
Open questions, carried forward honestly: India has no authority on GST and tips; India s.194-O and tips is unresolved; ZATCA's "no suggested amount" condition is unqualified and its tips guidance sits in the hospitality (not transport) guideline; neither ZATCA nor the UAE FTA has connected tips to e-invoicing; no source confirms the UK Tipping Act has been applied to ride-hailing; Uber publishes three different post-trip tipping windows; Nevada's TNC excise statute is silent on tips.