Tidewatt — Technical Specification
Constraint file: `../JAMAICA_TRUST.md` Launch geography: Jamaica, JPS Rate 10 households (post-paid and prepaid). Forbidden: collecting JPS credentials or JPS payments.
System architecture
[PWA] camera → bill/voucher image IndexedDB: last extract + tariff snapshot
v [API] POST /bills/extract GET /tariff/current ← public OUR/JPS table we maintain POST /watch/plan ← this-week actions (no actuation required)
+---> Official pay HINTS only (URLs/text for jpsco.com, Paymaster, Bill Express)
X---> JPS customer login X---> Card checkout for "clear your balance"
Optional phase 2: smart-plug actuation, Rate 40 TOU for shops. Not in the launch binary.
Core APIs (v1)
| Method | Path | Purpose |
|---|---|---|
GET | /tariff/current | Dated fuel, energy, customer charge, GCT — sourced from public PDFs |
POST | /bills | Image of bill or prepaid receipt |
POST | /bills/{id}/extract | Line items + userMustConfirm: true |
PUT | /bills/{id} | Corrected kWh, remaining credit, meter type |
GET | /watch | Days-of-light estimate + confidence |
POST | /watch/plan | Three actions for *this* week; comfort locks |
POST | /alerts/whatsapp-self | Opt-in “credit low” to a number the user typed (not harvested) |
GET | /receipts | What we stored, when |
DELETE | /household | Purge |
Pay links are static official URLs, not open redirects.
Data models
type MeterKind = "postpaid_rt10" | "prepaid_rt10";
interface TariffSnapshot { id: string; effectiveDate: string; sourceUrl: string; fuelJmdPerKwh: number; energyJmdPerKwh: number; customerChargeJmd: number | null; gctRate: number; notes: string; }
interface BillExtract { id: string; meterKind: MeterKind; kwh: number | null; remainingCreditJmd: number | null; remainingKwh: number | null; fuelLineJmd: number | null; confidence: number; confirmedByUser: boolean; }
interface WeekPlan { id: string; actions: { text: string; savesEstimate: "unknown" | "small" | "medium"; unsafe: boolean }[]; daysOfLight: number | null; daysOfLightConfidence: number; }
We never store a JPS password. Account number is optional and shown as “only if you want a reminder of *your* number — we cannot pay this.”
Security framework
- DPA 2020 notice. Bills are financial + household data.
- Extract models isolated; no training on customer bills without a separate written opt-in (default off).
- Anti-clone: official origin listed on JPS-adjacent partner desks; 876 to report fake “Tidewatt pay” pages.
- CSP; no skimmers; no third-party payment iframe.
PWA specification
| Item | Spec |
|---|---|
| Manifest | name: Tidewatt, start_url: /watch, display: standalone |
| Offline | Last bill math + last plan |
| Low data | One photo, aggressive compression |
| Footer | “We never take JPS payments.” + official pay names |
| SMS/WhatsApp | Double opt-in; stop keyword |
Scalability
Tariff table is tiny. OCR is the cost. Cache snapshots by hash of the image so retries are cheap. 100k prepaid-curious households still fit a boring Postgres + object store.
Honest limit
If JPS changes bill layout, extract breaks. Correction UI is the product, not a perfect model.