One utility bill, several sub-meters: each tenant gets their exact share, with both readings and the meter photo, on WhatsApp.
One utility contract often serves several units: a house split into flats, a building with a shop downstairs, student rooms. The bill arrives in one name. The landlord reads the sub-meters (or guesses), does the arithmetic on paper and collects cash. Nobody keeps the readings, tenants dispute the price, the landlord quietly eats the fixed charges, the VAT and the higher tariff tiers, and when a tenant leaves there is no closing reading to settle against the deposit. OneMeter turns reading day into a photo per meter and the bill into a split that adds up to the cent, at cost: no markup, no late fee, no penalty, ever.
- Properties with their units and sub-meters, one utility contract each (electricity, water or gas), with a card per property showing the cycle: meters read this round, bill entered, statements sent, shares paid, money still owed.
- Reading round: take a photo of each meter; with an Anthropic key the vision model reads the
digits (
{value, digitsRead, uncertain}) and the landlord confirms or corrects them; without one the number is typed. The main meter is read too. A reading lower than the one before is flagged for review; a counter that turned over is recognised when its number of digits is known. - Tenant reading links: one tap sends every tenant who has not read yet their own link on WhatsApp
(or as a wa.me link while no WhatsApp account is connected). The tenant's page (
/r/:token) takes the photo, shows the suggested number, and sends it; the landlord checks it against the photo. - Enter the bill: period, total, fixed part, main meter readings and a photo of the paper bill. The split updates as you type: a table with use, fixed and shared parts per tenant and a plain-words explanation of the price per kWh or m³. Confirm freezes it; a split can be reopened until money is recorded against it.
- Statements: each tenant gets a WhatsApp message in their language with their amount and a link
to their statement (
/s/:token): both readings with photos, consumption, price, fixed part, shared part, how it was worked out, the bill photo and what is paid. It shows nothing about the neighbours. - Payments ledger: cash or bank, part payments, a receipt on WhatsApp if wanted, what each tenant still owes. Nothing is ever added to a share.
- Move-out and move-in: a closing reading with a photo, an estimate of the tenant's last days priced like the last bill, how much of the deposit to keep back, and a deposit note. The next bill gives the leaving tenant a final statement for their days only; empty days are the landlord's; the deposit kept back can settle it. The new tenant starts from an opening reading (or the closing one).
- History: use per unit per bill, bill totals, price per kWh over time, share of fixed charges and of losses, and every bill.
- Settings: the two sharing rules per property, a helper (manager role) who does reading day while bills, payments and tenants stay with the landlord, languages, the landlord's phone for tenants.
- English and Albanian throughout, including the WhatsApp texts, sent in each tenant's own language.
The split (backend/app/Split, pure PHP, no database) works on integer thousandths of a kWh or
m³ and integer cents, so nothing drifts:
- Consumption per unit = current − previous reading. If the counter shows less than last time, it rolled over when the meter's digit count is known and the jump is under half a turn (99 870 → 120 on five digits is 250); otherwise the unit is flagged and the bill cannot be split.
- Tenancies cut the period. A move-out on day 13 with a closing reading and a move-in on day 16 with an opening reading give three slices of the same meter: the old tenant's, the landlord's empty days, the new tenant's. Days always add up to the period.
- Variable part = bill total − fixed part. It is priced against the main meter when it was read (and counted at least as much as the sub-meters together), otherwise against the sub-meters.
- Shared use and losses = what the main meter counted beyond the sub-meters, at the same price. Per property it is shared in proportion to use, equally, or absorbed by the landlord and shown as such.
- Fixed charges are shared equally, by floor area or by the number of people, and within a unit by days, so a part-period tenant pays only for their days.
- Every column is rounded with the largest-remainder method: each slice gets the floor of its exact share and the leftover cents go to the largest remainders. The shares plus the landlord's own part equal the bill total exactly; the engine refuses to return anything else.
Readings belong to a round: the ones taken after those the last split bill used, in the last 25 days. A bill period runs from the reading its previous bill ended on to the round reading closest to the day after the period, so nothing is counted twice or lost.
Tenants never sign in. A tenancy carries an unguessable token for its reading link; each share carries one for its statement. Photos on those pages are served through signed, expiring URLs. Every query in the signed-in app is scoped to the landlord's organisation.
The demo replays a year through the app's own services with the clock turned back: reading days with GD-drawn meter photos (the exact digits on odometer drums, slightly askew, lit like a stairwell), bills arriving with photos of the paper bill, statements sent, tenants paying or not, a tenant leaving in month seven and another moving in.
- Backend: Laravel 12 (PHP 8.2), PostgreSQL, Sanctum session cookies, GD for the demo photos, the Anthropic PHP SDK for reading meter photos, WhatsApp Cloud API through an outbox.
- Frontend: Vue 3.5, Vite, TypeScript, Pinia, vue-router, vue-i18n, Phosphor icons, Chivo and Chivo Mono.
Prerequisites: PHP 8.2 with pdo_pgsql, gd, intl and mbstring, Composer, Node 22 and PostgreSQL.
# database
createuser onemeter --pwprompt # password: onemeter
createdb -O onemeter onemeter
createdb -O onemeter onemeter_test # for the tests
# backend on :8122
cd backend
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate --seed # the demo year takes ~25 s: it draws ~100 photos
php artisan serve --port=8122
# frontend on :5122 (proxies /api to the backend)
cd frontend
npm install
PORT=5122 BACKEND_URL=http://127.0.0.1:8122 npm run devOpen http://localhost:5122 and sign in:
| Who | Password | |
|---|---|---|
| Vera Shehu, landlord | demo@onemeter.test |
demo1234 |
| Ilir Shehu, helper for reading day | manager@onemeter.test |
demo1234 |
The demo dates are relative to the day you seed, so a reading round is always in progress.
Tenants' pages open from the links in Messages (/r/… and /s/…).
All in backend/.env (see .env.example):
| Variable | What it does | Without it |
|---|---|---|
APP_PUBLIC_URL |
Base URL of the web app, used in the links sent to tenants | http://localhost:5173 |
APP_DEMO |
Seeds the demo landlord and enables the reply simulator | true |
MESSAGING_DRIVER |
log keeps messages in the outbox; whatsapp sends them |
Outbox only; each message has an "Open in WhatsApp" link |
WHATSAPP_TOKEN, WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_APP_SECRET, WHATSAPP_VERIFY_TOKEN |
WhatsApp Cloud API credentials and webhook | Messages are not sent, only kept |
WHATSAPP_TEMPLATE_READING_REQUEST, …_STATEMENT_READY, …_PAYMENT_RECEIVED |
Approved template per message, as name|param,param; approve each in en and sq |
Defaults to onemeter_reading_request, onemeter_statement_ready, onemeter_payment_received with the parameters in config/product.php |
ANTHROPIC_API_KEY, ANTHROPIC_MODEL |
Reads the digits on meter photos | The photo is kept and the number is typed |
cd backend && php artisan test # 111 tests: 56 on the split engine, 55 on the API
cd frontend && npm test # 27 tests on meter arithmetic and Albanian formatting
npm run type-check && npm run build-onlyThe split engine's tests cover rollover, the three ways of sharing losses, the three fixed-charge rules, part-period tenants, rounding to the exact cent and 300 random bills that must always balance. The API tests cover readings flagged as lower than before, tenant links that only show their own unit, statements that leak nothing about the neighbours, payments and what is still owed, the whole tenant change with the deposit, roles, and another landlord never reaching any of it.
backend/
app/Split/ the pure split engine: meters, segments, largest remainder
app/Metering/ readings, rounds, bill splitting, statements, payments, tenant changes
app/Http/Controllers/
database/seeders/Demo/ the demo year and the GD-drawn meter and bill photos
lang/{en,sq}/ WhatsApp texts and error messages
tests/Unit/Split/ engine tests
tests/Feature/ API tests
frontend/
src/views/ screens (public/ holds the tenant pages)
src/components/ meter readout, reading capture, split table, UI kit
src/lib/ API client, formatting (with Albanian fallbacks), meter arithmetic
src/i18n/ English and Albanian
docs/screenshots/
MIT, see LICENSE.







