Build Grow or Die

KINDAreplaces $24.08/mosaves $288.96/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A one-product profit dashboard whose hard problems are recording model usage that the provider only reports when asked, absorbing payment events that arrive duplicated and out of order, and carrying unknown data through every calculation instead of rounding it to zero.

estimated effort weekendthe files for this build are in the project pack

RuntimeNode 22 with TypeScript and Fastify, server-rendered pages, one processStorageSQLite in one file, append-only ledgers, no destructive updatesPaymentsStripe webhooks with signature verification and event-id idempotencyModel usageA thin wrapper returning the provider response unchanged, recording usage beside itPricesA versioned catalogue with effective dates, cost computed on the server

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the dashboard, the tracker endpoint and the webhook receiver.

    Get it nodejs.org or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. free to set up; transaction fees apply to real payments

    Why Revenue arrives as events, and the whole ledger design exists because those events repeat and arrive out of order.

    Get it Create a webhook endpoint for the events you care about and copy the signing secret. open ↗

    Verify The command line tool forwards a test event that your endpoint verifies

  3. API keypay per token

    Why The wrapper is the only place token counts can be observed, and the streaming behaviour is the reason this phase exists.

    Get it A key with a spend limit set in the provider console.

    Verify A streamed call records the same token counts as the same call unstreamed

  4. decidefree

    Why Every join in this build is on this column, and choosing it after the tracker ships means reprocessing everything.

    Get it Decide what an account is in your product, how the identifier is generated, and where your application knows it.

    Verify The identify call is wired into your product and the column never contains an address

  5. have readyfree

    Why Costs are computed from it and nobody is going to tell you when a figure changes.

    Get it Copy the current published prices for the models you use, with the date and the source URL, into the catalogue file.

    Verify Every model appearing in usage has a price entry covering that date

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- visitors(id, first_seen, last_seen) · a random identifier, no address, no fingerprint
- identities(visitor_id, account_id, bound_at) · set by your own identify call, never derived from an email
- events(id, visitor_id, account_id, name, occurred_at, props_json) · account_id stays null until a binding exists and null means unmatched, not anonymous
- revenue(id, stripe_event_id, account_id, kind, amount_minor, currency, occurred_at, received_at, raw_json) · append only, unique on the event id
- usage(id, account_id, model_id, input_tokens, cached_input_tokens, output_tokens, status, usage_known, latency_ms, occurred_at) · usage_known is false for interrupted or failed calls
- prices(model_id, effective_from, effective_to, input_minor_per_million, cached_input_minor_per_million, output_minor_per_million, source_note)
- windows(window, finalised_through, unmatched_events, unknown_usage_calls, unpriced_calls) · the holes, computed and displayed with every total

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/profit.dbEvents, revenue, usage and windows. Back it up; none of it can be refetched.
STRIPE_WEBHOOK_SECRETsecretrequiredwhsec_...redactedVerifies every incoming payment event before it is stored.
STRIPE_API_KEYsecretrequiredsk_live_...redactedUsed to fetch objects an out-of-order event referenced but did not contain.
MODEL_API_KEYsecretrequiredsk-...redactedPassed straight through by the wrapper and never recorded anywhere.
ADMIN_TOKENsecretrequireda long random stringThe only credential on the dashboard. Rotate it when you share a screenshot.
SETTLEMENT_LAG_DAYSrequired3A window is not finalised until this much time has passed.
PRICE_CATALOGUE_PATHrequired./config/prices.jsonDated prices per model. The source and check date live in the file.

The build, in order

  1. An identifier you issue, and honest gaps

    Product activity is recorded and bound to your own account identifier, with unbound activity visibly unbound.

    1. It is the only column every other table will join on.

      Files src/identify.tstests/identity.test.ts

    2. Otherwise the same visitor appears as two customers.

      Files src/events.ts

    3. It is a real number and it belongs on the page.

      Files src/windows.ts

    done when · tick each as it passes
    watch out
    • Binding on an email address because it is the field you already have merges two people who share a family address and splits one person who changed employer, and because the join looks clean nobody ever checks it. Issue your own identifier, pass it to the identify endpoint, and make the schema refuse to store an address in that column.
  2. A revenue ledger that survives duplicates and disorder

    Payment events can arrive twice, out of order or late without changing any total.

    1. It is the only duplicate detector the provider gives you.

      Files src/webhooks.tstests/idempotency.test.ts

    2. The endpoint has a deadline and your processing does not.

      Files src/webhooks.ts

    3. An edited ledger cannot be reconciled against anything.

      Files src/revenue.tstests/out-of-order.test.ts

    done when · tick each as it passes
    watch out
    • Ordering events by their created timestamp and assuming each arrives once produces a ledger that double-counts a retried delivery and applies a refund before its charge, and Stripe's own documentation says delivery is at least once, that order is not guaranteed, and that created must not be used to determine order or whether an event was already processed. Make the event identifier unique in the ledger and make every handler safe to run twice.
  3. Usage you actually measured

    Every model call records real token counts or is marked as unknown, and never as zero.

    1. It is not returned otherwise, and the default looks like success.

      Files src/llm/wrapper.tstests/streaming-usage.test.ts

    2. Those tokens were spent whether or not you were told.

      Files src/llm/wrapper.ts

    3. They price differently, so folding them in breaks the cost later.

      Files src/usage.ts

    done when · tick each as it passes
    watch out
    • Reading the usage object off a streamed response without asking for it returns nothing, and the obvious defensive code turns that nothing into zero tokens, which makes every margin on the dashboard flatteringly high and gives no hint that anything is missing. Set the option that returns usage, read the final chunk, and when it never arrives store the call as unknown so the number on the page carries a caveat.
  4. Prices that were true at the time

    Cost is computed on the server from a dated catalogue, and an unpriced model stays unpriced.

    1. A stored cost is a mistake you cannot correct.

      Files src/pricing.tstests/pricing.test.ts

    2. Yesterday's calls keep yesterday's price.

      Files src/pricing.ts

    3. Free is the most expensive default in this build.

      Files src/windows.ts

    done when · tick each as it passes
    watch out
    • Storing a cost figure on the usage row at write time freezes a mistake permanently, and the day you notice a price was wrong you have no way to recompute without knowing which rows used which figure. Store tokens only, compute cost from the dated catalogue on read, and let a corrected price fix history automatically.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more analytics.