Eight vendors who disagree about what money is
AdConfirm places advertising inside invoices and receipts, bills the advertiser per delivered impression, and pays the business its share. I co-founded it and built the platform: eight accounting and point-of-sale integrations, a sub-penny billing ledger, and Stripe Connect payouts.
What problem, and for whom
A small business sends invoices and receipts that are opened and read carefully, because they are about money the recipient owes or has just spent. That attention is worth something to an advertiser, and worth more than the same impression on a web page. To sell it you have to sit inside the business’s existing invoicing software rather than ask them to change it — which means integrating with whatever accounting or till system they already use.
So the product is an advertising network whose inventory lives in other people’s software, and whose revenue has to be split with the business that owns the document.
What I built
A pnpm monorepo: a marketing site, a dashboard for advertisers and businesses, and an Express backend that holds the integrations, the ad engine, the billing ledger and the payout worker. Eight integrations across accounting and point-of-sale systems, metered subscription billing through Stripe, and Stripe Connect for paying businesses out.
The hard part
Every one of the eight vendors disagrees with the others somewhere, and never in the same place twice:
- field names in Pascal case, with an explicit tenant identifier;
- an API minor version pinned in the URL, and a second, separate expiry for the refresh token itself;
- money as strings, under two different field names for the same concept;
- a money object in minor units, and no line items at all;
- a static token header rather than an OAuth refresh, with the shop domain as the tenant id;
- a currency symbol where every other adapter supplies an ISO code;
- a webhook signature header that still carries the company’s previous brand name;
- no webhook at all, so it is polled, with its payload keys read defensively in four capitalisations because it uses them inconsistently.
Six different signature headers, all HMAC over the raw body, which is why the router takes the body as bytes rather than parsed JSON — a detail that carries the whole verification step and is commented as such, because the first person to add a JSON body parser above it would break every webhook silently.
The architecture is type-driven normalisation. There is no central reconciler to drift out of date: the shared invoice type is the seam, and each adapter owns a private mapper that lands on it. The type is deliberately the intersection of what eight vendors can supply — 3 of the 8 cannot supply line items — so downstream code never depends on a field one vendor quietly omits.
The money
The best code in the project is the money. Billing at a two-pound cost per thousand impressions means one impression costs a fifth of a penny, and there is no way to hold that in integer pence. So the ledger’s unit is a millicent — a thousandth of a penny — and one impression is exactly 200 of them rather than a rounded fraction. The revenue split rounds the business’s share and gives the platform the remainder, so the two always sum to the gross with no drift. Converting to pence returns both the pence and the leftover, so repeated roll-ups through invoices and payouts never quietly shed fractions.
Charging is gated on delivery rather than on rendering: the ledger is only written when the message carrying the advertisement is confirmed delivered. Idempotency lives in the database as a conflict on the placement id, and the follow-through is the part worth copying — a duplicate insert returns no rows, so the delivery counters deliberately do not increment a second time either. The counters go through an atomic database function rather than a read and a write.
- co-founder; built the platform
- private
- TypeScript · Express · Next.js · Turborepo · Stripe Connect