Noble Gas Rewards
Noble Gas v2, Building the Operating System for a Car Wash and Gas Station Business
A Benmore Technologies case study.

Client: Jack Frisbie, Director of Finance & Real Estate, Noble Gas · FDE: Arkash Jain · Site: noble-portal.com · App Store · Google Play · Platform Guide
1. Introduction
Overview
Noble Gas runs 16 fuel and car wash sites across Connecticut and Massachusetts. Noble Gas v2 is the loyalty platform Benmore built for the chain: a Flutter app on iOS and Android, a Django backend on AWS, a public website, a cashier web app, and a live TCP integration with the Gilbarco Passport point-of-sale at every register. A member keys their phone number at the pump, the discount comes off the price per gallon, and the points land in the app before they finish pumping.
The register integration went live chain-wide on 17 August 2026. In the 30 days that followed, 2,964 members signed up, the platform credited $235,307 of fuel sales identified at the pump, and the loyalty host handled 44,541 POS frames. Every number below was pulled from the production database and the admin dashboard on 16 September 2026.
At a glance
| Timeline | First commit March 2025 · store submission May 2026 · AWS migration June 2026 · chain-wide go-live 17 August 2026 |
| Members | 3,372 total · 2,964 in the last 30 days · 768 in the last 7 days |
| Activity | 2,857 WAU · 94% logged in within 30 days · 2,535 transacted within 30 days |
| Spend tracked | $254,964 tier-qualifying spend · $235,307 fuel credited at the pump |
| Points | 489,479 issued (100 pts = $1) · 21,486 credited in the last 24 hours |
| Delivery | 2,375 commits · 198 merged PRs · backend v1.15.0 · mobile 1.4.7 |
| Backend scale | 23 Django apps · 107 models · 258 migrations · 149 API paths |
| Mobile scale | 258 Dart files · 74k lines · 113 screen files |
| Tests | 5,793 backend tests at 82.6% coverage · 1,637 mobile tests · 9 Maestro flows |
Challenge
- Credit a pump fill-up in real time over a binary TCP protocol whose four vendor spec drafts contradict each other and the hardware
- Make points behave like money: never minted twice, never spent twice, never lost mid-transaction
- Ship an app a driver trusts at a glance, one-handed, outdoors, on a weak signal
- Give the finance director one page that shows what the program costs and returns
- Deliver it with a two-engineer core team, where a push to
devdeploys production
2. The Problem
Background
- 16 sites, Gilbarco Passport registers, DRB car wash kiosks, and no loyalty layer connecting any of them to a customer
- The v1 codebase was a scaffold: login, points balance, barcode. No POS integration, no car wash redemption, no engagement loop, no analytics
- Competing chains discount fuel at the pump. Noble had the hardware and nothing talking to it
- The client framed the program as a spend from day one: every point issued has to earn its keep
Note. The owner set the point budget on 7 August 2026: free points at or under $10 per member per year, per source; referrals at $2. That directive became test assertions. The prize-wheel test pins the annual dollar cost, not a points constant, because a daily spin costs 365 times the per-spin number.
Pain Points
- No identity at the pump. No way to recognise a customer at a dispenser, so no per-gallon discount and no fuel points
- Manual car wash redemptions. Wash codes lived in the DRB portal and were read off a screen by a person
- No accounting model for points. v1 added and subtracted integers on a profile row; a kiosk timeout or a voided sale could strand or duplicate value
- No retention mechanics. Nothing brought a member back between fill-ups
- No operational visibility. Site identify rates, quiet pumps and daily issuance were all unanswerable without a query
- Unreliable vendor specs. PP-1026E, PP-1027C, PP-1028 and PP-1051B disagree on byte order, tag names and ID types; a host built from them rejects every real frame
3. Our Solution
Discovery Process
- Started from the client's own vocabulary: "earn on every fill-up", "product codes do not change from state to state", "1 or 2 a day" for voucher caps
- Captured real POS traffic at East Windsor in June 2026 before writing the accrual parser; the captured frames, not the PDFs, are the wire reference
- Proved in the DRB portal that a prepaid wash code cannot grant a membership month, which split car wash into two products with two settlement paths
- Walked the commercial employer program with the owner against a rendered design page before writing a model
Two decisions that never moved
- Hold, never deduct, until the outside world confirms. Every redemption that depends on a register, kiosk or membership portal reserves points and settles on confirmation. Cancel, expire, decline and void all release the hold. Nothing on a release path mints.
- The register is a door onto the same rows the app uses. An armed reward is one
ArmedRewardrow whether it settles at a Passport, at the Rewards Desk, or expires. When a site's POS comes online, nothing changes for the member or the cashier.
Core Value Proposition
Noble Gas v2 gives a regional fuel chain the loyalty stack a national brand has: identification at the pump, a per-gallon discount that grows with spend, points that spend like cash on washes, memberships and c-store items, and a phone-first app with an engagement loop tuned to a stated annual budget per member. The finance director reads issuance, redemption and site behaviour on one admin page.
Proposed Solution

| Surface | Who | What it does |
|---|---|---|
| Mobile app | Members | Live balance and barcode, catalogue, arm rewards, wash codes, memberships, locator, inbox, badges, streaks, wheel, referrals, Wallet passes |
| Register host | Passport POS | Identify member, apply ¢/gal discount, offer and settle armed rewards, credit points, replay-safe |
| Car wash | DRB kiosks and plans | Assign kiosk codes, hold points for memberships, confirm on DRB's feeds |
| Rewards Desk | Cashiers | Scan member, earn, redeem, session strip; auth failures hidden as 404 |
| Website | Public | Landing page, 26-screenshot guide, support, /download/ QR redirect |
| Admin | Owner and ops | KPIs, trends, tier health, socket health, RFM, churn; every rate and price editable without a release |
Technology Stack
| Layer | Technology |
|---|---|
| Mobile | Flutter 3.6 · Provider · Firebase (FCM, Crashlytics, Analytics) · Mixpanel · Google Maps · Sentry · Google and Apple Sign-In |
| Backend | Python 3.12 · Django 6.0 · DRF · SimpleJWT · Django Channels · Celery + beat (49 scheduled tasks) |
| Data | PostgreSQL 17.9 + PostGIS on RDS behind RDS Proxy · ElastiCache Redis 7 · S3 + CloudFront |
| POS | Custom TCP host, Conexxus 1.2 XML, CRC-32 framed, on 2 Lightsail edge nodes with watchdog and failover |
| Car wash | DRB SmartCodes and EWA enrolment · OAuth client-credentials · HMAC-signed callbacks |
| Infra | AWS ECS Fargate · ALB · Terraform · CodePipeline with a Test stage · CloudWatch alarms · Sentry |
| Web | Django templates, vanilla JS, no build step · WhiteNoise · Resend · Mailchimp |
| Wallet | Apple PassKit .pkpass · Google Wallet loyalty objects |
| Quality | pytest, ruff, mypy, ast-grep, gitleaks · flutter analyze, flutter test, Maestro, Playwright |
4. Implementation
The pump: Gilbarco Passport loyalty host
- Wire contract learned from hardware. Little-endian 28-byte header,
Request/Responsetag suffixes, opaque string sequence IDs, and the POS-declared interface version echoed on every reply - Two doors, one funnel. REST webhook and TCP host both land in
create_points_earning_transaction, idempotent on an event id, wallet row locked before crediting - Fails safe, never silent. A handler crash on
GetRewardsreturns an empty-rewards response rather than dead air, so the register never freezes mid-sale
| Metric (30 days to 16 Sep 2026) | Value |
|---|---|
| Sites registered with the host | 10 |
| POS frames since go-live | 44,541 (about 1,437 a day) |
GetRewards frames · member resolved |
12,356 · 11,214 (90.8%) |
FinalizeRewards frames |
6,397 |
| POS earn events · processed | 6,840 · 6,828 (99.8%) |
| Fuel transactions credited | 4,986 · $235,307 · avg $47.19 |
| In-store purchases credited | 1,771 · $19,657 |
| Shift period closes | 1,555 (756 in 30 days) |
The masked ID that cost a quarter of identifies. The host masks the loyalty ID in its response so phone numbers never print on receipt journals. The Passport echoes that masked string back on the next
GetRewardsin the same visit. Over 30 days, 322 of 1,294 frames echoed a masked ID and resolved nobody. Fix:GetRewardsmints aLoyaltySequenceIDand persists the member link; repeat frames and the finalize both resolve through it. 7,378 links exist today.One fill asked twice. A customer-activated fill sends a prepay-shaped
GetRewardsat card insert and a postpay-shaped one at completion. Both were answered, so the same gallons were discounted twice: Enfield, 27 August, a $67.00 sale debited $66.97. Fix:prepay_fuel_offer_already_made, keyed on the sequence link with a member + site + pump fallback, behind a kill switch.The spec said per gallon; the register applied it flat. PP-1051B defines a postpay
amountOffas dollars off the unit price. Over 60 days, 70 of 70 clean pay-inside sales had exactly $0.03 taken off, across 1.3 to 40.3 gallons. Fix: send the total withRewardLimitquantity 1, correct under both readings and matching the only real-worldAddRewardcapture on file.
The money model
- Five wallet primitives (
add_points,redeem_points,hold_points,release_hold,confirm_held_redemption) and no raw arithmetic on the columns available_points = balance − holdgates every spend path; a database constraint floors the balance at zero- One lock order,
ArmedRewardthenUserProfile, shared by the expiry sweeper, the cashier desk and the POS settle path - Four redemption doors (desk barcode, wash code, membership coupon, POS arm) share one eligibility gate that counts across both redemption tables
| Wallet (16 Sep 2026) | Value |
|---|---|
| Points issued, lifetime | 489,479 (= $4,895) |
| Points in balances · on hold · spent | 472,740 · 1,698 · 16,679 |
| Armed rewards | 161: 77 redeemed · 48 cancelled · 31 expired · 2 declined · 3 live |
| Armed by kind | 103 merchandise · 58 fuel |
Tiers on spend, not points
| Tier | Trailing 12-month spend | Earn rate | Pump discount |
|---|---|---|---|
| Bronze | $0 | 1.00× | 3¢ / gal |
| Silver | $1,500 | 1.25× | 5¢ / gal |
| Gold | $4,000 | 1.50× | 10¢ / gal |
- Base earn is 1 pt per $1 on fuel, 2 pts per $1 in-store and car wash
- Check-ins, wheel, referrals, birthdays and badges earn spendable points that are tier-inert: engagement cannot buy a fuel discount
- One implementation resolves tier; one nightly job demotes, because a member who stops buying never writes a row
- Today: 3,349 Bronze, 7 Silver, 2 Gold. $254,964 of qualifying spend across 1,729 members, one month into the ladder
Everyone to Gold. An early release made qualification spend-based without seeding thresholds. Every threshold read $0, so every member resolved to the top tier. Fix: a fail-closed guard. Exactly one tier may sit at $0; two or more means an unseeded ladder and nobody is promoted past entry. Pinned by
test_fail_closed_when_thresholds_unseeded.
Car wash and memberships
- Kiosk codes. 1,000 DRB SmartCodes imported, assigned with
select_for_update(skip_locked=True), reconciled every 15 to 30 minutes, expired at 7 days - Memberships. 1,001 coupons imported; points hold until the exact coupon appears on DRB's transaction feed. The 99¢ intro month is a real membership DRB renews itself
- Three-valued verification. Used, genuinely unused, or unknown. Unknown fails closed, because releasing a hold on a used coupon gifts a free month
- No copy or price in the app. Admin fields with
{intro_price}/{renewal_price}placeholders mean a price change needs no release
The mobile app
- Balance first. Home streams from
/ws/points/with jittered reconnect; a freshness label marks anything stale - Two taps to a reward. Arm, wash code, membership: one confirm screen each, with cost, hold and expiry stated before the tap
- Sessions that survive bad networks. Refresh-token rotation off, session cleared only on a definitive 401, nothing on the startup path awaiting the network
- Design system. Urbanist type, green with gold reserved for the prize moment, WCAG 2.1 AA targets, 44 pt tap targets, reduced-motion alternatives
- Locator. One PostGIS row per site; the nearest-site socket answers per type (gas, car wash). 19 sites seeded, 15 operating, 3 coming soon
- Wallet. 849 members (25%) hold an Apple or Google Wallet pass
![]() Home |
![]() Rewards |
![]() Arm fuel discount |
![]() Reward armed |
![]() Wash code |
![]() Membership |
![]() Locator |
![]() Notifications |
![]() Badges |
![]() Streak |
![]() Refer a friend |
![]() Redeemed codes |
The cached null that crashed every cold start. Release 1.4.5 reported six Crashlytics issues that were three root causes. A WebSocket frame carrying
"points_balance": nullwas cached to secure storage, so the null overwrote the seeded map on every launch of Home and Profile; a backend fix alone could not rescue a poisoned device. Fix: null-safe reads everywhere,use_build_context_synchronouslypromoted to an error, and an ast-grep rule forsetStateafterawait.
Engagement, on a budget
| Mechanic | Config (admin-editable) | To date |
|---|---|---|
| Daily check-in | 1 pt, 2 pts from day 7, 20 pt bonus every 30 days | 4,734 check-ins · 1,881 members · longest streak 45 days |
| Badges | 10 badges, 1,000 pt ladder | 1,989 awarded · 1,733 First Fill-Up |
| Spin the wheel | 6 prizes, 2.6 pts per spin | 5,250 spins |
| Referrals | 150 / 50 pts, 20 per referrer per 30 days | 52 referrals · 42 rewarded |
| Birthday | 100 pts a year | 80 awarded |
| Push | FCM, category-routed | 21,845 sent · 3,149 devices (76% iOS) |
The wheel that cost $228 a year. The original grid paid 62.5 points per spin, about $228 per spinner per year and double the whole program's budget at 15,000 members. Cut to 2.6 points per spin ($9.49 a year). The seeder is create-only, so the change shipped only once
seed_wheel_prizes --updateran in every environment; a test now asserts the annual dollar figure.
Admin analytics


| KPI (16 Sep 2026) | Value |
|---|---|
| WAU, last 7 days | 2,857 |
| Top 7-day activity sources | check-in 767 · first-fill-up badge 427 · wheel prize 342 |
| Points credited, 24 hours | 21,486 |
| RFM segments | champions 785 · promising 355 · at risk 332 · hibernating 1,436 |
| High churn risk | 766 of 3,370 |
Delivery pipeline and observability
- One linear pipeline.
Source → Test → Build → Migrate and deploy ECS → Deploy edges, queued, never racing.Testruns the full suite and holds the release on red - Mobile releases stop one step short of users. A version tag builds both platforms; iOS lands in TestFlight, Android as a Play draft
- Edge logs ship to CloudWatch and back a silent-pump detector and a site-offline alarm
- Hash-chained audit log, append-only by Python guards and PostgreSQL triggers, verified daily: 14,275 events
The deploy race that shipped nothing. Deploy was gated on a GitHub Actions job polled by a second workflow with the opposite
cancel-in-progresspolicy. Back-to-back merges cancelled each other's run and a clean merge landed ondevundeployed while everything showed green. Replaced by one pipeline with aTeststage: the failure class is structurally impossible, not guarded against.Compliance posture. CCPA and GDPR deletion requests run through a read-only, tamper-evident log (28 processed). Marketing consent is captured per channel. Audit retention defaults to six years. The team is aware of PCI and TCPA obligations; the platform handles no card data and sends no SMS. Nothing here is a certification.
5. Results
Product Outcomes
- 3,372 members in one month of chain-wide operation: 2,964 in the last 30 days, 768 in the last 7
- 94% logged in within 30 days; 2,535 transacted
- $235,307 of fuel and $19,657 of in-store spend credited across 6,757 spend transactions
- Tracked spend tripled month over month: $68,421 in August to $186,543 in the first 16 days of September
- 51% of members have fueled with the app; 56% have checked in at least once
- Daily active users rose from about 100 to about 440 across the 30-day window
- 77 rewards redeemed, 849 wallet passes, 5,250 wheel spins, 52 referrals
Technical Achievements
- A Conexxus loyalty host built from captured traffic, live at 10 sites on 2 edge nodes
- 90.8% pump identify success, after a measured 24.9% loss on repeat frames before the sequence-link fix
- 99.8% ingestion success across 6,840 POS earn events, idempotent under replay
- Zero-mint accounting: hold-first settlement across four redemption doors, a database floor, one lock order, arming velocity limits
- 5,793 backend tests at 82.6% coverage and 1,637 mobile tests; two critical defects that passed a green suite were caught by reverting the fix
- A deploy pipeline that cannot race, edges sequenced after migrations, CloudWatch alarms, a hash-chained audit log
Business Impact
- Fuel discount at the pump, chain-wide. 3¢ to 10¢ per gallon on every identified fill, the mechanic national chains use
- Every free point is budgeted. Each engagement source sits under about $10 per member per year, and the tests fail if a config change breaks that
- Issuance is visible. $4,895 of points issued against $254,964 of qualifying spend, readable on
/admin/ - Operations live in admin. Earn rates, tier thresholds, arming limits, grants, membership copy and prices, code inventory: no release needed
- The car wash is a redemption target. 1,000 kiosk codes and 1,001 membership coupons in inventory, two taps away
- Employer fuel programs have a system. Rosters, gallons ladders and voucher receipts replace a per-company spreadsheet; 2 employers onboarded
Read client reviews on Trustpilot ↗
6. Lessons Learned
- Capture the wire before trusting the spec. One June capture at one site settled four contradictory drafts
- Budget engagement in dollars per year, never per event. A daily mechanic multiplies every per-event number by 365
- A hold is not a spend. Reserving points until the outside world confirms let register, kiosk and membership share one wallet with no minting path
- Verify a fix by reverting it. A test that passes without its fix certifies the bug
- Seeders and config are part of the release. Create-only seeders and unseeded ladders caused the two most visible incidents; both now fail closed
- Measure the complement. Counting one outcome's rows gave a wrong fleet-level answer twice
7. Conclusion
Noble Gas v2 turned a scaffold with a barcode into the operating layer for a 16-site fuel and car wash chain: identification and discount at every Passport, points that behave like money across register, kiosk and phone, an app members open daily, and an admin page where the finance director reads issuance against spend. One month after go-live, 3,372 members, $255k of tracked spend and 44,541 register frames say the plumbing holds.
Roadmap
- Merchandise rewards settled at the register once the inside
GetRewardsframe carries a basket - Silver and Gold filling in as trailing 12-month windows mature
- Commercial employer rollout beyond the first 2 organisations
- Tier-based free fuel-reward grants switched on per tier
- Push delivery outcomes persisted so the FCM failure KPI reads a number
- The 3 coming-soon sites going live in the locator
The architecture for all of it is already in place.
⭐ A Note on the Mission
Noble Gas is a regional chain competing with national brands on the same corners. The national brands have loyalty at the pump because they have vendor budgets to buy it. Noble has it because a small team read the frames off the wire and built the host.
Everything in this platform exists to make a point worth exactly what it says: earned on a real fill, held until a real confirmation, spent on a real wash or a real discount, and counted on a real dashboard. Earn on every fill-up.



















