Skip to content

Ilia DudaCo-op Jan 2027

Eight vendors who disagree about what money is

Co-Founder · May 2026 – present

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.

Fig. 1
Eight schemas, one target typeAdConfirm · adapters and what the shared type cannot carry
Eight schemas, one target typeEight accounting and point-of-sale integrations — Xero, QuickBooks, FreeAgent, Sage, Square, Zettle, Shopify and Epos Now — each map onto one internal interface called InvoiceData. Six authenticate by OAuth refresh and two by a static key. 3 of the 8 cannot supply line items at all, so the shared type is a lowest common denominator rather than a union of what the vendors offer.XerooauthQuickBooksoauthFreeAgentoauthSageoauthSquareoauthZettleoauthShopifystaticEpos NowstaticInvoiceDataone target type3 of 8 cannot supply line items,so the shared type is a lowest commondenominator, not a union
Each vendor’s adapter maps its payload onto the one shared invoice type. The marks in indigo are the vendors that cannot supply line items, so the shared type leaves line items 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.


role
co-founder; built the platform
repo
private
stack
TypeScript · Express · Next.js · Turborepo · Stripe Connect