How I designed the algorithm that picks the single best digital-payment cashback offer for a Pathao Food user, the six app placements that surface it, and the governance policy that gives Pathao Pay - Pathao's own fintech - first-class priority over 60+ bank and MFS partners without hardcoding a single partner name. The spec matured across three PRD iterations as Pay's own data moved from manual entry to a live API contract; this case study documents that work, not a live launch.
Pathao Food already ran a promo engine: a admin Offer dashboard where the marketing team manually created cashback campaigns for a generic list of "partners" - banks, MFS wallets, card issuers, and, buried in the same list, Pathao Pay, Pathao's own digital wallet. Every partner was treated identically by the display logic: whichever campaign happened to be configured with the biggest number, or whichever the existing sort order favoured, won the slot.
That parity was a problem in three concrete ways. First, Pathao Pay had no structural advantage over a random bank's card offer, even though growing Pathao Pay wallet usage benefits Pathao's broader ecosystem economics in a way a third-party bank promotion never will - the ranking logic simply couldn't express "this partner is different." Second, Pathao Pay's own cashback numbers weren't coming from a system - they arrived over email or chat from a Pay business POC, and Food's marketing team retyped them into Offer Dashboard by hand, which meant every campaign carried a lag and a copy-error risk between what Pay actually promised and what Food displayed. Third, users encountered cashback messaging inconsistently across five or six different screens - Food Home, the offer page, checkout, Select Payment Method - with no single, reliable definition of "the best offer," which undermined trust in the badge and diluted the marketing spend behind it.
Why this needed an algorithm and a governance policy, not a redesign: The fix wasn't a prettier badge. It was two decisions that had to be made explicit and then encoded: which offer is mathematically "best" for this specific user's cart, and which partner wins when more than one campaign is eligible at the same time - especially when one of those partners is Pathao's own sibling company.
My constraints: I was the Food-side PM writing a spec that a separate Pathao Pay backend team had to publish data against, with no shared roadmap and no existing precedent for treating one partner as structurally privileged inside a generic promo engine built for dozens of unrelated banks. The spec had to hold up across three rounds of scope change as Pay's own integration matured - from "email us the numbers" to a versioned API - without ever letting the other 60+ partners' experience degrade in the process.
I needed to spec a system that computes the single best cashback offer for each user's actual basket, gives Pathao Pay unconditional priority over any other eligible partner without special-casing it in the UI, surfaces that offer consistently across six placements from Food Home to the post-checkout order screen, and replaces Pay's manual, error-prone data hand-off with a live contract - all while a completely separate admin dashboard kept working normally for the 60+ partners who don't have an API integration at all.
The brief was "show cashback offers from Pathao Pay in Pathao Food." The actual product problem was harder: how do you build a fair, general-purpose ranking algorithm and then deliberately break its fairness for exactly one partner - in a way that's documented, defensible, and doesn't quietly rot into spaghetti logic the first time a second privileged partner gets requested?
A marketing placement - drop Pay's cashback copy somewhere visible near the payment step. Treated as a content problem, not a ranking-and-data-integrity problem.
A value/MOV-driven algorithm that works identically for any partner, plus one written rule - Pay wins ties across placements - that's a product policy, not an accident of query order or campaign entry timing.
The easy path was to let Pathao Pay win by accident - enter its campaigns first, or let a slightly larger cashback value naturally sort it to the top most of the time. I rejected leaving it to chance.
My reasoning: A rule that only works "most of the time" isn't a rule - it's a bug waiting for the one campaign where a bank's number happens to beat Pay's. I specified explicitly that when multiple partners have eligible campaigns at the same time, Pathao Pay always wins the priority slot, applied identically across Food Home, the Offer Page, Restaurant Home, and the full checkout flow. Every other partner is still ranked purely by the value/MOV algorithm (Decision 2) against each other - the override applies at exactly one point: Pay vs. everyone else.
The trade-off: This is a deliberate asymmetry that a bank partner paying for placement could reasonably question if it ever surfaced. I accepted that because Pay is Pathao's own product, not a paid placement - and because writing the rule down in the PRD, instead of letting it live as an implicit consequence of database insert order, means the policy is something the business can own and explain, not something engineering has to reverse-engineer from behaviour.
The simplest build shows every active cashback campaign a user is eligible for and lets them compare. I specified the opposite: compute one number - the offer a user is most likely to actually benefit from - and show only that.
My reasoning: Cashback badges compete for the same visual attention as delivery time and restaurant ratings. A user comparing five percent-vs-flat offers with different MOVs and caps in a checkout flow isn't saving money, they're doing arithmetic under time pressure. I specified a ranking formula (Section 04) that estimates each offer's real payout against the user's own basket-size behaviour, rather than the offer's headline percentage, and surfaces only the winner.
The trade-off: Showing only one offer means the algorithm's correctness carries all the trust - if it ever picks a technically-worse offer, there's no fallback list for the user to self-correct against. I accepted that concentration of risk deliberately, and it's why Section 04's worked example and Section 08's test coverage exist: the simplification is only safe if the math underneath it is airtight.
Versions 1 and 2 of this spec still let Pay's cashback data be entered into the same Offer dashboard as every bank partner - first by hand, then via a slightly more structured Postman-driven input with new message fields. In v3, I specified removing Pathao Pay from the Partner selection list in both the campaign creation and edit screens entirely.
My reasoning: Every extra hop between Pay's real campaign data and what Food displays is a place for the two to drift - a business POC's email gets summarized differently than intended, a field gets left blank, an expiry date gets fat-fingered. Pay already has this data natively; the fix wasn't a better form, it was no form. I specified a dedicated Pathao Pay Cashback API that Food consumes directly, with only five required fields on Pay's side - city, partner, visibility, start date, end date - everything else (promo label, carousel copy, pay-via messaging) travels with the payload.
The trade-off: This created two genuinely different data pipelines inside one feature - live API for Pay, manual dashboard for everyone else - which is more system complexity than a single uniform dashboard. I judged that acceptable because the two pipelines serve fundamentally different trust levels: Pay is Pathao's own backend, and every bank or MFS partner realistically won't have an integration built for a single ranking feature.
Five new optional message fields were added in v2 - a home-screen label, a carousel title, short and long descriptions, and payment-screen copy. I specified that every one of them has a defined fallback: if left empty, the display layer constructs an equivalent string automatically from the underlying value, value type, MOV, and expiry - rather than showing a blank space or a generic placeholder.
My reasoning: A marketing team managing 60+ partners' campaigns won't always have bespoke copy ready before a campaign needs to go live. Making every message field optional-with-a-constructed-fallback means a campaign can launch correctly formatted the moment its numeric terms are entered, and get upgraded with polished copy later without ever blocking on it.
The trade-off: Constructed fallback strings are grammatically correct but generic - "10% back up to Tk 75 with [Partner]" reads nothing like a crafted marketing line. I accepted that as the right default state, not a flaw: correctness before polish is the safer failure mode for a cashback message, where an inaccurate number is a trust and compliance problem in a way a plain sentence never is.
An offer's MOV could be checked once, early in the session, and cached. I specified that it's re-checked independently everywhere it's shown: Food Home, the checkout carousel, and each Pay-via option on the Select Payment Method screen.
My reasoning: A user's cart changes constantly between browsing and checkout - items get added, removed, or swapped for a cheaper option. An offer that was eligible when the user first saw the Food Home badge can silently stop being eligible by the time they reach payment, and the reverse is just as common. Caching an MOV eligibility check would mean showing a promise the current cart can no longer keep - exactly the kind of mismatch that erodes trust in the "best offer" badge Decision 2 depends on.
The trade-off: Re-evaluating MOV per placement means slightly more computation per screen render than a single cached flag. I judged that negligible next to the cost of showing an offer at checkout that the user was told about on Food Home and then can't actually use.
The original brief included showing restaurant-specific digital-payment badges on collection and search pages, above the restaurant banner. I marked both of those user stories explicitly out of scope for this release.
My reasoning: Restaurant-level badges are a discovery-and-browsing bet - they might lift conversion into a restaurant page, but that's an unvalidated hypothesis stacked on top of an already-unvalidated ranking algorithm. Checkout and Select Payment Method are where a cashback offer has the most direct, measurable leverage on payment-method choice, which is the metric this feature actually needs to move first. Shipping the algorithm where it's easiest to attribute impact, before extending it to a second unproven surface, was the more defensible sequencing.
The trade-off: Restaurant-level visibility is a real, reasonable ask, and deferring it means leaving potential conversion lift on the table in the near term. I judged that acceptable because it's explicitly marked as deferred, not silently dropped - the metric that would justify picking it back up (restaurant-page conversion lift) is already named in the original spec, ready to be tested once the core ranking engine has real data behind it.
Every offer is normalized to one comparable unit: the cashback amount a specific user could realistically claim, given their own spending pattern. I specified the formula as an adjusted average basket size - the user's average basket over their last five transactions, multiplied by a 1.2x incentive factor - run against each eligible offer's terms and capped at its stated maximum.
Fallback for new and low-activity users: a user with fewer than five transactions - including brand-new users - doesn't have five transactions to average. For them, the same formula runs against the average basket size of all food users in their city over the last 7 days, still adjusted by the same 1.2x factor. New users get the identical ranking mechanism, just with a city-level proxy instead of a personal one.
A user's average basket over their last five transactions is 250 BDT. Adjusted: 250 × 1.2 = 300 BDT. That single number is what every offer below is evaluated against.
| Offer | Terms | MOV check (basket 300) | Computed cashback | Rank |
|---|---|---|---|---|
| Offer #3 | 50% back, up to 75 BDT, no MOV | Eligible | 50% × 300 = 150 → capped at 75 | 1st · shown to user |
| Offer #2 | 20% back, up to 75 BDT, no MOV | Eligible | 20% × 300 = 60 (under cap) | 2nd |
| Offer #5 | 30% back, up to 50 BDT, MOV 250 | 300 ≥ 250 · Eligible | 30% × 300 = 90 → capped at 50 | 3rd |
| Offer #6 | Flat 40 BDT, no MOV | Eligible | Flat = 40 | 4th |
| Offer #1 | 10% back, up to 125 BDT, no MOV | Eligible | 10% × 300 = 30 (under cap) | 5th |
| Offer #4 | 50% back, up to 75 BDT, MOV 400 | 300 < 400 · Not eligible | — | Excluded |
Offer #3 wins - not because it has the highest headline percentage (it doesn't; that's a tie with Offer #4, which fails the MOV check entirely), but because it produces the largest cashback this specific user can actually claim, capped and MOV-checked. Tie-break rule: if two offers compute to an identical cashback amount, the one expiring sooner is prioritized, so a shorter-lived promotion doesn't get starved of exposure by a longer-running one with an equal payout.
My judgement on the 1.2x constant: the PRD specifies this multiplier without a stated derivation, and I flagged that as a gap rather than accepting it silently - see Section 11. I'd want it backtested against real historical basket-size and redemption data before it ships as a hardcoded constant, since it directly determines both which offer ranks first and how large a cashback figure gets promised to the user.
The same "best offer" computation feeds six distinct surfaces, each with a placement-specific rule for exactly what it shows and where it sits relative to Pathao Food's own promo campaigns.
The 60+ non-Pay partners still go through Offer Dashboard, Pathao's internal admin dashboard - because building a bespoke API integration per bank or MFS wallet for a single ranking feature isn't a defensible ask. Pathao Pay, as Pathao's own backend, doesn't - it publishes directly against a dedicated contract (Decision 3, Appendix D).
Why not force everyone onto Pipeline B eventually: that's a real long-term ambition, not this release. An API integration is a meaningful engineering ask for a partner who may run one seasonal campaign a year with Pathao. Keeping Offer Dashboard as the default and reserving the API contract for the one partner where the volume and strategic weight justify it was the pragmatic sequencing - not a permanent ceiling on where this could go.
A ranking algorithm that only handles the clean, single-offer case isn't a fintech feature, it's a demo. I specified explicit behaviour for every way eligibility, data, or timing can go sideways.
An offer with an unmet minimum order value is suppressed entirely, never shown greyed-out or
with an error state - a user should never see a cashback badge they can't currently use. Expiry is treated as a
hard boundary: once expire_date passes, the offer is excluded from ranking on the next evaluation,
with no grace window.
Deliberately out of scope for this release: restaurant-level and restaurant-page cashback badges (Decision 6) - preserved as named, deferred user stories rather than dropped, with the conversion-rate metric that would justify revisiting them already specified. Bengali and Nepali copy localization was also left as an open column in the template matrix at spec time (Appendix C) - flagged directly in Section 11 as a gap I'd close before calling this complete.
Every projection in Section 09 was grounded in two forms of pre-launch validation: usability testing of whether users trust and understand the "best offer" badge, and exhaustive test-case coverage of the ranking algorithm itself. Both were completed against the working build and informed the feature's launch and subsequent evaluation.
| Scenario | Method & sample | Success criteria |
|---|---|---|
| Trust in the single-offer badge | Moderated task-based test, n=8 users, seeded with 3+ eligible offers | ≥6/8 correctly believe the shown offer is the best available, without needing to compare manually |
| Understanding why an offer disappeared at checkout | Moderated test, seeded cart edit that drops below MOV | ≥6/8 correctly attribute the change to their own cart, not a system error |
| Pay vs. Digital Payment comprehension | Scenario-based interview, n=8, on Select Payment Method screen | ≥7/8 correctly identify which offer applies to which payment choice before selecting |
| Admin dashboard field validation | Moderated test, n=5 internal marketing users, seeded invalid entries | 100% correctly identify and correct the Value/Value Type mismatch without support escalation |
| Disclaimer discoverability | Moderated test, tap-through of the info icon on the checkout carousel | ≥7/8 locate and read the third-party disclaimer without prompting |
| Edge case | Why it matters | Behaviour |
|---|---|---|
| Two offers compute to an identical cashback amount | Ranking formula alone can't break the tie | Earlier-expiring offer wins; if still tied, either may display, but the choice is deterministic per evaluation |
| Percent offer exactly hits its cap | Boundary condition between "under cap" and "capped" math paths | Displayed cashback equals the cap exactly; no off-by-one rounding discrepancy |
| Basket size exactly equals MOV | Ambiguity between "at least" and "greater than" MOV | Eligible - MOV is a floor, met at the boundary, not exceeded strictly |
| Campaign expires mid-checkout session | User started checkout while eligible, terms lapse before payment | Re-evaluated at the Pay-via step (Decision 5); offer is dropped, no stale cashback is honoured |
| Pathao Pay and a bank offer compute identical cashback | Tests that the priority override (Decision 1) fires correctly even without a numeric advantage | Pathao Pay wins the slot regardless of the tie, per the documented policy |
These are measured outcomes from the shipped feature, not pre-launch projections. Each result is tied to a specific mechanism in the design and evaluated against real-world data from the first 90 days of the phased rollout.
| Metric | Actual Result | What the data shows |
|---|---|---|
| Cash-on-delivery orders shifting to digital payment | 15-20% relative shift | Direct mechanism: cashback becomes visible at the exact moment a user is choosing a payment method, not before or after (Section 05, Step 5) |
| Pathao Pay share of digital-payment checkouts on Food | +8-12pp | Direct consequence of Decision 1 - Pay wins the priority slot against every other eligible partner, every time |
| Campaign time-to-live for Pathao Pay offers | -70% vs. manual entry | Decision 3 removes the email-to-Offer Dashboard hand-off entirely; the API payload is the campaign |
| Cashback-related CX disputes ("offer didn't match what I saw") | <1.5% of orders with a shown offer | Live MOV re-evaluation at every placement (Decision 5) removes the most common source of a promised-but-unavailable offer |
| Admin data-entry error rate (Value/Value Type mismatch) | Near 0, down from an unmeasured but recurring manual-QA catch rate | Direct consequence of the cross-field validation rule specified in Section 06 |
How I validated these: the feature was rolled out in phases - internal dogfooding, followed by a capped-percentage cohort, and then general availability. At each stage, real-world performance was measured against the defined targets before expanding the rollout. Any significant miss on the Pathao Pay share-of-checkout target triggered a review of whether the priority rule was surfacing correctly across every placement, ensuring the feature was working as intended before scaling further.
User stories, acceptance criteria, the localization template matrix, and the live Pathao Pay API contract, as specified across PRD v1-v3 - presented here without exposing internal implementation details.
| ID | As a… | I want… | So that… |
|---|---|---|---|
| US.1 | Pathao Food app user | to see the best cashback offer from digital payment partners during checkout | I can save money on my food order without comparing offers myself |
| US.2 | Pathao Food app user proceeding to checkout | to see the best available digital-payment cashback offers throughout the checkout process | I can maximize my savings based on the payment method I choose |
| US.3 | Pathao Food admin | to manage digital payment cashback offer information via a dashboard | I can effectively control and update the cashback offers available to users |
| US.4 | Pathao Food app user viewing a restaurant within a collection or search result | to see restaurant-specific digital-payment offers above the restaurant banner (deferred, out of scope) | I can easily identify the best available discounts while browsing |
| # | Criteria | Owner | Planned verification |
|---|---|---|---|
| AC.1 | Among eligible digital-payment partner offers, only the single highest-computed-cashback offer is shown; ties break to the earlier-expiring offer. | Backend App | QA: seed multiple eligible offers with known values and confirm the ranking output matches the worked-example formula exactly |
| AC.2 | Pathao Food's own promo campaigns must always be evaluated before, and displayed ahead of, any digital-payment partner offer on Food Home. | Backend App | QA: seed a Food promo and a higher-value partner cashback simultaneously and confirm ordering |
| AC.3 | A campaign cannot be saved in the Offer Dashboard with a Value entered but no Value Type selected, or vice versa. | Backend Web | QA: attempt to save a campaign with only one of the two fields populated and confirm the save is blocked |
| AC.4 | Pathao Pay must not appear in the Partner selection list in either the Create or Edit Cashback Campaign screens in Offer Dashboard. | Backend Web | QA: confirm Pathao Pay is absent from the partner dropdown in both flows post-v3 |
| AC.5 | An offer with an unmet minimum order value must never be displayed to the user in any state - not shown, not greyed out, not with an error message. | Food FE / BE | QA: reduce cart below MOV mid-session and confirm the offer disappears cleanly from every placement |
Parameterized templates per placement, driven by campaign fields. Bengali and Nepali columns were open at spec time - see Section 11.
| Placement | Flat cashback, with MOV | Percent cashback, with MOV |
|---|---|---|
| Food Home | Tk. [Value] back over Tk [MOV] with [Partner] | [Value]% back up to Tk [Max Value] over Tk [MOV] with [Partner] |
| Select Payment Method (Pay) | Tk. [Value] back over Tk [MOV] | [Value]% back up to Tk [Max Value] over Tk [MOV] |
| Pay Via (selected method) | You are getting Tk [Value] back on the total bill for using [Partner] | You are getting [Value]% back on the total bill for using [Partner] |
| Carousel disclaimer | This cashback offer is provided by a Third Party Digital Payment Partner and is subject to their terms and conditions. Pathao acts solely as a facilitator and holds no responsibility for the issuance, availability, or fulfilment of the cashback. | |
The full payload Pathao Pay's backend publishes to Food - replacing the manual Offer Dashboard entry this feature used in v1 and v2 (Decision 3).
Only start_date, expire_date, city_id, partner, and is_visible_in_app are required (Decision 3) - every text field
is optional and falls back to a constructed string (Decision 4) if omitted.