pay.split_bill — Full Intent Specification
INTENT NAMESPACE: pay
INTENT NAME: split_bill
FULL ID: pay.split_bill
VERSION: v1.0.0
STATUS: live
TTBS WEIGHTS: time 0.30 · taste 0.15 · budget 0.35 · safety 0.20
LAST UPDATED: 2026-05-14
Split a known amount across multiple known recipients, generate UPI collect requests (or shareable UPI links / QR), and reconcile as each leg settles. Distinct from pay.send_money_upi because: (a) one logical payee on the user's side maps to N actual UPI VPAs; (b) each leg has its own state machine (pending / paid / expired / declined); (c) the surface must reconcile partial settlement (3 of 5 paid) and let the user nudge the remaining; (d) TOMO never holds the money — every leg flows directly between the payer's UPI app and the user's UPI VPA, mediated by an RBI-licensed PSP partner; (e) splits can be equal, item-wise, share-weighted, or custom-per-person.
1. NATURAL LANGUAGE COVERAGE
Classifies IN
- "split ₹2400 dinner bill 4 ways"
- "share trip cost ₹18000 among 6 friends"
- "split this Swiggy order with Rohit and Anu"
- "I paid the cab, collect ₹160 each from 3 people"
- "split rent ₹45000 — me 20000, roommate 25000"
- "send collect request ₹500 to mom for groceries"
- "split per dish, I had only paneer"
- "everyone owes me ₹350 for movie tickets"
- "make a UPI link, ₹250 each, 4 people"
- "split airport cab — Rohit 40%, me 30%, Anu 30%"
Classifies OUT — borderline NO
- "send ₹500 to Rohit" →
pay.send_money_upi(single-recipient one-shot) - "recharge my phone" →
pay.utility_bill_pay - "pay restaurant bill, I'm at the table" → could be
food.book_dine_inpayment flow if intent context is dining - "loan money to Rohit, he'll pay later" → not in v1 (lending requires interest/promise legal framing)
- "settle group expenses for our 3-day trip" → group ledger, multi-bill — out of v1 scope (single bill only)
- "split with stranger via QR" → out of scope (stranger requires identity binding)
MULTI-INTENT TRIGGERS
- "split dinner bill and book cab back" →
pay.split_bill+mobility.book_intracity_ride - "split airport cab and add fuel-up next time I drive" →
pay.split_bill+ future fuel intent - "split this Swiggy order with Rohit" → triggered automatically from
food.order_deliverypost-completion
2. INPUT — TOMO → PROVIDER
{
"intent": "pay.split_bill",
"intent_version": "v1.0.0",
"request_id": "req_sp_4r9q_2026-05-14T20:15:00Z",
"user_session_id": "anon_user_token_or_uid",
"bill": {
"total_inr": 2400,
"label": "Dinner at Cafe Jubilee Hills",
"occurred_at_iso": "2026-05-14T20:00:00+05:30",
"merchant_name_optional": "Cafe Jubilee Hills",
"source_intent_id_optional": "food.book_dine_in:req_din_…"
},
"user_collect_vpa": "keerthi@hdfcbank",
"split_method": "equal",
"split_method_allowed": ["equal", "custom_amount", "share_weighted", "per_item"],
"participants": [
{"vpa": "rohit.k@okaxis", "display_name": "Rohit", "amount_inr": 600},
{"vpa": "anu@upi", "display_name": "Anu", "amount_inr": 600},
{"vpa": "vivek@paytm", "display_name": "Vivek", "amount_inr": 600},
{"vpa": "self", "display_name": "Me", "amount_inr": 600}
],
"expiry_minutes": 60,
"reminder_policy": {
"nudge_after_minutes": 30,
"max_nudges": 2
},
"user_constants": {
"user_vpa_resolved": "keerthi@hdfcbank",
"psp_partner": "psp.npci_authorized_aggregator_001"
}
}
Field rules
total_inrMUST equalΣ participants[].amount_inr. Provider validates with 1-paise tolerance — elseERR_AMOUNT_MISMATCH.- Each
participants[].vpaMUST resolve via UPI directory; unresolvable → drop participant + flag to user. selfis a literal sentinel for the user's own share — no collect leg generated.expiry_minutes≤ 60 enforced (NPCI collect request TTL).split_methodENUM strict.
3. PROVIDER TOOLS
The pay-split MCP server (pay-split-mcp) exposes exactly these tools. The PSP partner is RBI/NPCI-licensed; TOMO is a routing surface, never a fund holder.
split.resolve_participants
Resolves each VPA against the UPI directory. Returns verified_name for confirmation (NPCI rule: must be shown before any collect).
{
"tool": "split.resolve_participants",
"input": {"vpas": ["rohit.k@okaxis", "anu@upi", "vivek@paytm"]},
"output": [
{"vpa": "rohit.k@okaxis", "verified_name": "ROHIT KUMAR", "status": "active"},
{"vpa": "anu@upi", "verified_name": "ANUSHA RAO", "status": "active"},
{"vpa": "vivek@paytm", "verified_name": "VIVEK SHARMA", "status": "active"}
]
}
split.create
Creates the bill and N collect-request legs. Returns one split_id + N leg_ids.
split.send_collect_leg
Triggers actual UPI collect to a participant's UPI app. Each leg is independent.
split.send_share_link
Generates a UPI deep-link / QR for the participant if they prefer to pay-self vs respond-to-collect.
split.nudge_pending
Sends a polite reminder via SMS/WhatsApp/in-app to unpaid participants. Respects max_nudges.
split.cancel_leg
Cancels a pending leg. No-op if already paid.
split.status
Read-only snapshot of all legs.
split.refund_leg
Refund a paid leg if user initiates (rare — e.g. payer pays wrong amount). Through PSP's UPI refund rail.
4. RESPONSE SHAPE — PROVIDER → TOMO
{
"intent": "pay.split_bill",
"request_id": "req_sp_4r9q_2026-05-14T20:15:00Z",
"split_id": "spl_8K2pq7",
"total_inr": 2400,
"user_share_inr": 600,
"collect_target_inr": 1800,
"expiry_at_iso": "2026-05-14T21:15:00+05:30",
"legs": [
{
"leg_id": "leg_8K2pq7_a",
"participant": {"vpa": "rohit.k@okaxis", "verified_name": "ROHIT KUMAR", "display_name": "Rohit"},
"amount_inr": 600,
"state": "pending",
"collect_sent_at_iso": "2026-05-14T20:15:02+05:30",
"share_link": "upi://pay?pa=keerthi@hdfcbank&pn=Keerthi&am=600&tn=Dinner_Cafe_JH&cu=INR",
"tier_signals": {"vpa_age_days": 1240, "psp_trust_band": "high"}
},
{
"leg_id": "leg_8K2pq7_b",
"participant": {"vpa": "anu@upi", "verified_name": "ANUSHA RAO", "display_name": "Anu"},
"amount_inr": 600,
"state": "pending",
"collect_sent_at_iso": "2026-05-14T20:15:02+05:30",
"share_link": "upi://pay?pa=keerthi@hdfcbank&pn=Keerthi&am=600&tn=Dinner_Cafe_JH&cu=INR",
"tier_signals": {"vpa_age_days": 720, "psp_trust_band": "high"}
},
{
"leg_id": "leg_8K2pq7_c",
"participant": {"vpa": "vivek@paytm", "verified_name": "VIVEK SHARMA", "display_name": "Vivek"},
"amount_inr": 600,
"state": "pending",
"collect_sent_at_iso": "2026-05-14T20:15:02+05:30",
"share_link": "upi://pay?pa=keerthi@hdfcbank&pn=Keerthi&am=600&tn=Dinner_Cafe_JH&cu=INR",
"tier_signals": {"vpa_age_days": 2100, "psp_trust_band": "high"}
}
],
"summary_strip": "3 collect requests sent · ₹1,800 incoming · expires in 60 min",
"tier_choices": {
"OK": "split equally · 4 ways · ₹600 each",
"GOOD": "split equally and nudge after 30 min (default)",
"GREAT": "split equally + nudge + auto-issue share link to each participant if collect declines"
}
}
Field rules
verified_nameMUST appear in UI for every leg before user taps "Send".state∈ {pending,paid,expired,declined,cancelled,refunded}.share_linkis the UPI URI fallback — works on every UPI app, no NPCI handshake required.tier_signals.psp_trust_band∈ {high,medium,low} based on PSP fraud heuristics;lowwarns user in UI.
5. CONTROLLED VOCABULARIES
split_method
equal · custom_amount · share_weighted · per_item
leg.state
pending · paid · expired · declined · cancelled · refunded
psp_trust_band
high · medium · low
All STRICT ENUM. Anything else → ERR_VOCAB.
6. TTBS DIMENSIONS
TIME (weight 0.30)
- Time-to-collect-sent (must be sub-second after user confirms)
- Reminder cadence honored
- Expiry handled cleanly (auto-issue share-link upgrade on expiry)
- TIME score = 1 − collect_send_latency_seconds / 5
TASTE (weight 0.15)
- UI clarity: verified_name + amount + state per leg
- Nudge tone honesty ("Rohit, ₹600 for dinner") — never aggressive
- One-tap to re-send link if collect declined
- TASTE score = readable_state_table_flag × nudge_tone_band
BUDGET (weight 0.35)
- ZERO fees on UPI rail to user — TOMO surfaces this fact
- Custom-split rounding handled to last paise without "hidden adjustment"
- BUDGET score = 1.0 if no fees; deducted if PSP charges hidden MDR
SAFETY (weight 0.20)
- VPA verified-name match before send
- PSP NPCI-licensed
- Per-leg state visible to user — no batch obfuscation
- TOMO never holds funds — explicit footer disclosure
- Anti-impersonation: warning if
verified_namemismatches display_name user typed - SAFETY score = vpa_directory_verified × psp_license_valid × name_mismatch_alert_flag
HARD FILTERS (apply before send)
- PSP partner must be NPCI-listed — else reject.
- Each VPA must resolve to a
verified_name— else drop leg. Σ legs = total_inr(1 paise tolerance) — else reject entire split.expiry_minutes≤ 60 — else reject.- User's own
verified_namemust match user's KYC profile.
7. COMPLETION CONTRACT
Per-leg success
- UPI collect request delivered to participant's UPI app within 5s of
split.create. - Participant responds (accept → paid; decline → declined; ignore → expired after TTL).
- On
paid, PSP fires UPI settlement webhook to TOMO. - TOMO updates
leg.stateand re-renders the split widget.
Split-level success
- All legs reach a terminal state (
paid/expired/declined/cancelled/refunded). - User can mark a split as "settled" once
Σ paid_legs ≥ collect_target_inr.
CPC webhook (HMAC-SHA256, 5-min replay window)
Note: split_bill is a free service — TOMO charges nothing on UPI P2P. The CPC payload exists only for analytics + audit, with tomo_commission_inr: 0.
{
"event": "pay.split_bill.completed",
"intent_id": "pay.split_bill",
"request_id": "req_sp_4r9q_2026-05-14T20:15:00Z",
"split_id": "spl_8K2pq7",
"total_inr": 2400,
"legs_total": 3,
"legs_paid": 3,
"legs_expired": 0,
"legs_declined": 0,
"tomo_commission_inr": 0,
"psp_partner": "psp.npci_authorized_aggregator_001",
"completed_at_iso": "2026-05-14T20:42:00+05:30",
"signature_hmac_sha256": "…"
}
Failure cases
psp_offline→ user warned, can retry; never silently fail.vpa_invalid→ drop leg, notify user inline with exact reason.leg_expired→ auto-suggest sending share-link as fallback.name_mismatch_severe→ block send; ask user to confirm or correct.
8. WIDGET — UOE → UI
{
"widget": "SplitBillWidget",
"header": {
"title": "Dinner at Cafe Jubilee Hills",
"total_strip": "Total ₹2,400 · my share ₹600 · collecting ₹1,800",
"expiry_strip": "Expires in 59:48"
},
"regions": {
"region_1_intelligence": ["3 of 3 VPAs verified", "all on high-trust PSPs", "no name mismatches"],
"region_2_summary": "Equal split, 4 ways, ₹600 each",
"region_3_visual": null,
"region_4_now_pin": "Send 3 collect requests now",
"region_5_tomo_choices": [
{"tier": "OK", "label": "send collect only", "reason": "fast"},
{"tier": "GOOD", "label": "send collect + nudge in 30 min", "reason": "balanced"},
{"tier": "GREAT", "label": "send collect + share-link fallback if declined", "reason": "highest completion"}
]
},
"legs_table": [
{"name": "Rohit", "verified": "ROHIT KUMAR", "amount": "₹600", "state": "pending", "action": "Resend"},
{"name": "Anu", "verified": "ANUSHA RAO", "amount": "₹600", "state": "pending", "action": "Resend"},
{"name": "Vivek", "verified": "VIVEK SHARMA", "amount": "₹600", "state": "pending", "action": "Resend"}
],
"footer_disclosures": [
"TOMO never holds your money. Each leg goes directly from your friend's UPI app to your bank.",
"UPI is free for you — no fees deducted by TOMO or the PSP.",
"We always show the name the bank has on file before you confirm — protects against typos."
]
}
UI invariants:
- Verified name always visible per leg.
- Per-leg state pill updates live.
- "Resend" / "Cancel" available per leg.
- Footer disclosure about TOMO-not-holding-money is mandatory.
9. CACHING POLICY
- VPA → verified_name: cache 24h on-device. Refresh before any collect send (UPI directory can change).
- PSP partner status: cache 1h.
- Split state: live websocket subscription on PSP webhook. Optimistic UI update permitted with rollback on webhook contradiction.
- Past splits (read-only ledger): cache 30 days on-device, encrypted.
- No PAN, no bank account number, no balance cached.
10. ERROR CODES
| Code | Meaning | UI surface |
|---|---|---|
ERR_AMOUNT_MISMATCH |
Σ legs ≠ total_inr |
"Numbers don't add up — please re-check shares" |
ERR_VPA_UNRESOLVED |
Directory lookup failed | "We couldn't find [vpa] — typo?" + retry input |
ERR_NAME_MISMATCH_SEVERE |
Verified name very different from display name | Modal asking user to confirm or correct |
ERR_PSP_OFFLINE |
NPCI/PSP rail unavailable | Retry banner; never silent fail |
ERR_EXPIRY_OUT_OF_BOUNDS |
expiry_minutes > 60 |
"Max 60 min — adjust?" |
ERR_LEG_ALREADY_PAID |
Resend/cancel attempted on paid leg | "This one's already settled" |
ERR_KYC_BLOCK |
User's own UPI VPA in KYC freeze | Plain redirect to user's bank app |
ERR_SELF_SHARE_MISSING |
No self sentinel in participants |
"Did you forget to include yourself?" |
ERR_DUPLICATE_VPA |
Same VPA twice in participants | "[vpa] appears twice — merge?" |
ERR_TRUST_BAND_LOW |
PSP fraud heuristic flagged participant | Warn but allow user to proceed with explicit confirm |
11. SANDBOX → PRODUCTION CHECKLIST
- Sandbox
split.resolve_participantsreturns mockverified_names within 200ms. - Sandbox
split.createenforcesΣ legs = total(test ±1 paise both sides). - Sandbox
expiry_minutesrejected at 61. - Sandbox each leg's state machine cycles: pending → paid (webhook), pending → expired (TTL), pending → declined (user simulated), pending → cancelled (user-initiated).
- Sandbox
split.nudge_pendingrespectsmax_nudgescap. - Sandbox
split.refund_legreverses a paid leg correctly. - Sandbox CPC webhook fires with
tomo_commission_inr: 0always. - Production PSP partner NPCI license verified (live RBI directory cross-check, weekly).
- Production name-mismatch threshold tuned (Levenshtein distance band) — false-positive < 1%.
- Production legal disclosure visible in widget footer — every render.
- Production rate-limiting: max 20 collect requests per user per hour to prevent abuse.
- Production share-link fallback tested across PhonePe, GPay, Paytm, BHIM, Cred.
12. ANTI-FABRICATION RULES
- NO synthesized "verified_name" — only directory-returned name shown.
- NO
paid_placement/ partner-bidding on which PSP routes a user — PSP is the user's bank-side UPI rail, not a marketplace. - NO "you saved ₹X by splitting" — UPI is free; no fee narrative.
- NO showing leg as
paiduntil PSP settlement webhook fires (no optimistic-only success). - NO hiding declined or expired legs — surface every terminal state.
- NO auto-nudge beyond
max_nudges— respects participant boundary. - NO bundling split with promo/cashback notifications — keeps the payment surface clean.
- NO "settle later" credit/loan suggestion — out of scope, regulatory grey.
- NO commission line item — TOMO charges nothing on P2P UPI splits.
- NO marketing language ("smart split", "intelligent split") — boring plain UI is the doctrine.
13. REGULATORY FRAMING
- NPCI UPI 2.0 spec — collect requests, deep-link URI scheme, 60-min TTL ceiling, mandatory name-display before send.
- RBI PSS Act 2007 — only NPCI-licensed PSPs route UPI traffic; TOMO uses partner, never operates rail itself.
- PMLA Rules 2005 — KYC is at the bank/PSP, never at TOMO.
- DPDPA 2023 — VPA and verified_name are personal data; cached on-device only; cleared on session-end if user opts out.
- Consumer Protection Act 2019 — refund handling on partial-paid splits must be transparent; TOMO surfaces refund button per leg.
- TOMO does NOT issue a Payment Aggregator license — partner does. TOMO is an "outsourced UPI initiation surface" per partner agreement, no fund-holding role.