A two-week AI capacity pilot ledger: one row per task, two numbers you never add together
An engineering field note on separating recovered output from avoided spend, with a CSV ledger and a short Node script.
Most write-ups of AI capacity pilots fail in the same place: at the end, someone adds "tasks finished" and "money saved" into one number, and nobody can say afterwards which tasks the number came from.
This note is about the boring part that stops that from happening: the ledger. The point of a two-week pilot is to find out whether moving prepared tasks to teammates who have their own AI tools and some spare time produces anything you can defend. If the ledger is built right, the answer falls out of it. If it isn't, no amount of analysis at the end will fix it.
Below is the ledger format, a small script that reads it, a worked example with invented data, and the things this method can't tell you.
The roles, so the ledger columns make sense
Every task in the pilot has three people attached to it, and the ledger records all three.
- Prepared by: the person who owns the problem. They write the brief, the acceptance criteria, and the test or check that decides whether it's done.
- Executed by: a teammate who takes the task and runs it with their own authorized tools: their own repo access, their own Claude Code or Codex seat. Nobody lends anybody a login, and no one's usage allowance moves. The task moves; the accounts stay put.
- Reviewed by: the person who gives the verdict. Often the author, sometimes a third person. Delivery is not acceptance; only this person's verdict counts.
That split is what an AI work handoff actually is in practice, and it's also what makes the numbers honest. If the executor's work is judged by the person who prepared it, you get a real "accepted / rejected" signal instead of a self-report.
The ledger
One row per handed-off task, including the ones that failed. Here's the header and a six-row example. All of this data is invented to illustrate the method; it isn't from a real team.
task,prepared_by,executed_by,reviewed_by,verdict,counterfactual,paid_alternative_usd,prep_min,review_min,rework_min
T1 flaky date test,Ana,Ben,Ana,accepted,wait,0,25,20,0
T2 empty-state copy,Ana,Chris,Dee,accepted,contractor,180,30,15,0
T3 CSV export bug,Dee,Ben,Dee,accepted,same_person_later,0,20,35,30
T4 settings refactor,Ana,Chris,Ana,rejected,wait,0,40,45,0
T5 API retry docs,Dee,Chris,Dee,accepted,api_overflow,30,15,10,0
T6 search filter UI,Ana,Ben,Dee,abandoned,wait,0,35,0,0
Two columns do most of the work.
counterfactual answers "what would have happened to this task without the handoff?" It has four values:
wait: it would have sat in the queue past the end of the pilot.same_person_later: the author would have done it themselves before the pilot ended.contractor: the team had actually planned to pay someone outside.api_overflow: the team would have bought extra API usage or credits for it.
The rule that matters: write the counterfactual when the task is handed off, not at the end. At the end of two weeks, everyone remembers every task as urgent. The value you write before anyone knows how it turned out is the one worth keeping.
verdict is the reviewer's call: accepted, rejected, or abandoned. Rejected and abandoned tasks stay in the ledger. Their prep and review minutes were spent, so they count toward cost.
The script
pilot.mjs reads the CSV and prints two separate results. It sorts each accepted task into one bucket:
- accepted +
wait→ recovered output (work that got done that otherwise wouldn't have, no cash involved) - accepted +
contractororapi_overflow→ avoided spend (a real planned purchase didn't happen) - accepted +
same_person_later→ neither, by default; it's reported as an upper bound
// node pilot.mjs pilot-ledger.csv [hourly_rate]
import { readFileSync } from "node:fs";
const [file = "pilot-ledger.csv", rateArg = "60"] = process.argv.slice(2);
const rate = Number(rateArg); // your own loaded cost per person-hour
const [head, ...lines] = readFileSync(file, "utf8").trim().split("\n");
const keys = head.split(",");
const rows = lines.map((l) => Object.fromEntries(l.split(",").map((v, i) => [keys[i], v])));
const PAID = new Set(["contractor", "api_overflow"]);
let recovered = 0, recoveredIfLaterCounts = 0, grossAvoided = 0, minutes = 0;
for (const r of rows) {
minutes += Number(r.prep_min) + Number(r.review_min) + Number(r.rework_min);
if (r.verdict !== "accepted") continue; // delivered is not accepted
if (r.counterfactual === "wait") recovered++;
if (r.counterfactual === "wait" || r.counterfactual === "same_person_later") recoveredIfLaterCounts++;
if (PAID.has(r.counterfactual)) grossAvoided += Number(r.paid_alternative_usd);
}
const labour = (minutes / 60) * rate;
const usd = (n) => (n < 0 ? "-$" : "$") + Math.abs(Math.round(n));
const tasks = (n) => `${n} task${n === 1 ? "" : "s"}`;
const byVerdict = rows.reduce((m, r) => ((m[r.verdict] = (m[r.verdict] ?? 0) + 1), m), {});
console.log(`tasks handed off: ${rows.length} ${JSON.stringify(byVerdict)}`);
console.log(`recovered output: ${tasks(recovered)} (${recoveredIfLaterCounts} if "same person later" counts)`);
console.log(`gross avoided spend: ${usd(grossAvoided)}`);
console.log(`pilot labour: ${(minutes / 60).toFixed(1)} h x ${usd(rate)} = ${usd(labour)}`);
console.log(`net avoided spend: ${usd(grossAvoided - labour)}`);
It's a naive CSV parser, so keep commas out of the task names. If your ledger lives in a spreadsheet, export it as CSV and run it as is.
Worked example
Running it on the six invented rows with a $60/hour planning rate:
$ node pilot.mjs pilot-ledger.csv 60
tasks handed off: 6 {"accepted":4,"rejected":1,"abandoned":1}
recovered output: 1 task (2 if "same person later" counts)
gross avoided spend: $210
pilot labour: 5.3 h x $60 = $320
net avoided spend: -$110
Four of six tasks were accepted, which sounds like a decent pilot. The ledger says something more specific:
- Recovered output is one task, T1. That's the only accepted task that would really have waited. T3 would have been done by Dee anyway; counting it is optimistic, so it's shown separately.
- Gross avoided spend is $210, from T2 (a planned $180 contractor job) and T5 ($30 of API overflow).
- Net avoided spend is negative. Prep, review and rework took 5.3 hours across all six tasks, including the rejected refactor and the abandoned UI task. At \(60/hour, that's more than the \)210.
That's a legitimate result, not a failed pilot. The team got one task done that would have waited, avoided two purchases, and spent more time coordinating than the purchases would have cost. The useful follow-up questions come straight from the rows: why did T4 take 85 minutes and still get rejected? Was T6 a bad candidate from the start?
Run it again with 0 as the rate and net avoided spend becomes $210. That shows how much the conclusion depends on how you value the team's time, which is worth saying out loud when you present it.
What to record during the two weeks, and when
- At handoff: prepared_by, executed_by, reviewed_by, counterfactual, paid_alternative_usd, prep_min. Write the brief link next to the row.
- At delivery: nothing goes in the verdict column yet. Note what the executor handed back (PR, test output, notes) next to the row.
- At review: verdict, review_min.
- If it comes back for changes: add to rework_min. Don't create a second row.
Keep credentials, tokens and customer data out of the ledger entirely. It only needs to say who did what, never how they logged in.
Limits
- Six rows, or even thirty, prove very little. Two weeks will tell you whether your team can write briefs that transfer and whether review time is manageable. It won't tell you what a year looks like.
- The counterfactual is still a judgment call, just one made earlier. If people consistently mark tasks
waitto make the pilot look good, the ledger can't catch that. A second person agreeing to the label at handoff helps. - The hourly rate is a planning assumption. Use whatever your finance owner uses and say what it includes.
- Seat costs don't appear anywhere. The subscriptions were already paid for before the pilot and they'll be paid after it. Using spare capacity on a seat you already have is not a saving, so the script doesn't treat it as one.
- Quality after acceptance isn't tracked. If an accepted task causes a bug a week later, this ledger won't know. For a longer pilot, add a column for it.
For the larger economic picture, including the three kinds of value people tend to blur and a ten-seat worked example, Wagglet's field note on using the Claude Code and Codex capacity you already pay for is the longer version of the argument. This ledger is the piece I'd set up on day one, before any of that analysis is possible.
Disclosure: I'm involved with the Wagglet project. This post was drafted with AI assistance and reviewed and edited by me. The script and its output above are real; the team, tasks and numbers in the ledger are invented for illustration.

