Billing AI-assisted work
An AI coding agent changes the unit you are selling. You used to sell an hour of typing. Now you sell an hour of judgment, during much of which you are waiting. That is still worth money, but the old way of counting hours breaks in two directions at once, and both are invisible until someone reconciles.
This skill is the set of rules that survive contact with a real practice.
The core problem: one hour, three clients
While an agent builds for client A you switch to client B, then C. Three stopwatches run. At the end of the day they total nine hours against a six-hour day.
Nobody typed nine hours. Billing all three at full rate is not "being busy", it is charging three clients for the same hour, split across three invoices where none of them can see it. It survives precisely because no client can detect it.
The rule: wall clock is the only real resource, and it is counted once.
Each of N concurrent timers counts 1/N of the elapsed time. One hour with three timers running is twenty minutes billed to each.
This holds whether or not the timers belong to the same client. Two timers on two projects for the same client is the same double count, just harder to notice.
Store three numbers, not one
A single hours column cannot survive an audit. Store:
| Field | Meaning |
|---|---|
measured_hours |
the raw stopwatch reading, untouched |
hours |
the billable number after division |
concurrent_timers |
the divisor |
concurrent_timers must be captured at stop time. It cannot be reconstructed later: nothing in the record tells you what else was running six weeks ago. This is the single most common omission and it makes every past entry unauditable.
Keep started_at and ended_at too. An entry with only a date and a number of hours cannot be placed on a day, so it can never be checked against a calendar.
Rounding: the direction flips
Most timers round up to the next quarter hour. That is defensible for a single timer.
For a divided entry, round to the NEAREST quarter, never up. Rounding up re-inflates exactly the share you just divided. Three timers rounded up individually can restore most of the double count you removed.
Segments, not sessions
The divisor changes during the day. The moment a timer starts or stops, every running timer enters a new segment.
If you let one timer span a period where the divisor went from 1 to 3, you must pick one divisor for the whole span, and both choices are wrong.
So: when the set of running timers changes, close the current segment for all of them and open a new one. A solo hour stays whole; a three-way hour divides by three. The entries are small and slightly noisy, and they are correct.
The most common real-world case: a timer runs alone for an hour, then two more start. Close the solo hour at divisor 1 before starting the others, or that hour silently loses two thirds of its value.
Attention is not the divisor
You will be tempted to bill the client you are concentrating on and discount the ones where an agent is grinding. Resist making that the billing rule: it is unverifiable, it varies minute to minute, and it will not survive a client asking how it was measured.
Instead, record attention separately from billing. A focus log, one row per period with a timer id and a focused-at/unfocused-at pair, costs almost nothing and answers a different question: what were you actually doing.
Two things it earns you:
- It recovers lost work. When a timer is killed or a session is reconstructed, the focus log is the only evidence of what happened. It is worth its cost the first time this happens.
- It surfaces when the rule is wrong for you. If the log shows one client getting all the attention and a third of the clock for weeks, that is data for changing the rule deliberately, not a reason to fudge entries.
Be aware of the asymmetry: the concurrency rule was built to stop over-billing, and in the agent-waiting case it under-bills the one client you are truly serving. Know which way it is cutting.
The waiting hour is not free to you
A common objection: "I was only waiting for the agent, I should not charge for it."
Waiting has a cost. The agent's tokens are billed to somebody, and if the client's subscription pays for the build then your time is the only thing you are selling. Check which:
- Model subscriptions you pay for personally are your overhead, spread across everything.
- Builder credits often sit on the client's workspace where you are only a member. Then the waiting costs you time, not money.
Get this straight before deciding what waiting is worth. The answer differs per client and it changes what you should charge.
Your own products are an expense, not free time
Time on your own product is not billable, and logging it as zero makes it invisible. Log it at your full internal rate as an expense. Two fields carry it: an is_internal flag and an internal_cost computed as hours × rate, on hours already divided by the concurrency rule, with no second division.
The same applies to a partnership where you take no fee. It is an investment, and the total is the number that matters when the terms are renegotiated. If it is recorded as zero, you will argue from memory.
Failure modes that eat hours silently
These are the ones that actually happen. Every one of them loses money in the dark.
Auto-stop that writes only the banked time. A server-side cleanup kills a timer after N hours of no activity and records only the paused/accumulated seconds, discarding everything since the last resume, with no start or end time. A two-hour block becomes a fifteen-minute entry. Treat any auto-stop entry as a data-loss event: reconstruct the real span from the focus log and say what you restored.
The UI stop path that ignores the split. The concurrency fields exist in the schema, and the function that saves an entry writes none of them. Every stop through the interface lands unsplit and undated. Verify the write path, not the schema.
A rate of zero or null. An entry with no rate bills nothing and looks normal in a list. Check for rate_at_time IS NULL OR = 0 on every insert, especially entries created outside the main flow.
Banked time crossing midnight. A paused timer resumed the next day carries yesterday's seconds into today's entry. Either flush the banked time to the correct day or zero it, but never let it ride silently.
A break that nobody paused. The stopwatch does not know you walked the dog. When the person says the measurement is wrong, the person is right: adjust hours and leave measured_hours untouched, then record in the description both what was measured and who decided the difference. Never present a reconstruction as a measurement.
Reconciling against reality
A time record that cannot be read backwards is not a record.
- A scheduled block with no entry, no timer and no completion mark did not happen. Move it or unschedule it. Leaving it makes the calendar a lie.
- Where a client keeps their own hours sheet, it will drift from your system. Compare month by month, not in total: a matching total can hide an over-count in one month cancelling an under-count in another.
- When the client's sheet and your system disagree, the client pays from their sheet. Fix the sheet or accept the number in it, and never quietly bill the difference.
What to tell a client
Say the rule out loud, in one sentence, before it ever comes up: when work runs in parallel, the hour is counted once and divided between them.
A client who hears it unprompted trusts every later number. A client who discovers it has a different conversation.