"Implement Account Deletion" Cost 100 Million Tokens
One of the most expensive coding-agent tasks I have run had a wonderfully ordinary title:
A7: Implement administrative deletion and account lifecycle services
By the time it was finished, the Cline task had consumed more than 100 million tokens.
The working context had grown to roughly 500,000 tokens.
And the pull request was, in the end, about deleting things.
That was surprising enough that I went back and asked a more useful question than “why did the agent use so many tokens?”
What exactly had we asked it to build?
The answer turned out to be a good example of something I increasingly see in serious agentic software engineering:
the issue title is often a terrible description of the real problem.
“Delete account” sounds like a button and a SQL statement.
The actual task was a small distributed-systems problem involving authorization, credential revocation, database locking, external API uncertainty, retries, idempotency, stale authentication, destructive-operation safety, and a pile of invariants that all needed to remain true when things failed halfway through.
The 100 million tokens were not interesting because they were large.
They were interesting because they made the hidden size of the task visible.
What “delete account” actually meant
OpenOrc already had lower-level deletion primitives.
It knew how to delete OpenOrc-owned Workspaces, Projects, Repository mappings, archived Tasks, and Connections from Postgres.
Issue #97 sat above those primitives.
It had to turn deletion into an authorized application service while preserving a very deliberate boundary:
OpenOrc may delete OpenOrc state.
It must not casually extend that authority into the external systems it coordinates.
Deleting a Repository mapping must not delete the GitHub repository.
Deleting a Task must not delete its issue, branch, commits, pull request, or CI state.
Deleting a Connection may revoke the OpenOrc-owned credential used to control that runtime, but it must not wander into the runtime and delete provider credentials, MCP configuration, or filesystem state.
That already makes “delete” less like:
DELETE FROM thing;
and more like:
prove authority
↓
identify exactly what OpenOrc owns
↓
remove only that state
↓
preserve external artifacts
↓
prove nothing escaped the ownership boundary
Permanent account deletion made it considerably more interesting.
The awkward transaction you cannot have
An OpenOrc account is rooted in Supabase Auth.
Deleting the Auth user causes the corresponding OpenOrc Profile and its owned application graph to disappear through the database’s deletion semantics.
But OpenOrc also stores Workspace runtime-control credentials in Supabase Vault.
Those credentials need to be revoked before the account disappears.
So the operation has two distinct parts:
Postgres / Vault
↓
revoke OpenOrc-owned runtime credentials
disconnect affected Connections
commit
↓
Supabase Auth Admin API
↓
permanently delete Auth user
The obvious problem is that the second operation is a network request.
You do not hold a database transaction open while waiting for an external service to answer.
So this cannot be one atomic transaction.
And once it is not one atomic transaction, failure semantics become part of the feature.
Suppose credential revocation commits successfully and the Auth Admin request then fails.
The account still exists.
The credentials must remain revoked.
A retry must be safe.
Suppose instead the delete request times out.
Did Supabase receive it?
Did it delete the user and lose the response?
Did nothing happen?
Those states are materially different, and blindly retrying a destructive operation is not a satisfactory answer.
The required behavior became:
known success
→ account deleted
known failure
→ account remains
→ credentials stay revoked
→ explicit retry is safe
uncertain outcome
→ do not blindly replay
→ read the exact Auth user back
→ reconcile whether deletion happened
→ continue only from confirmed state
At this point we are some distance from the settings-page button that says “Delete my account.”
Then concurrency arrives
The first design was not enough.
Consider account deletion enumerating every Connection, deleting its Vault secret, and then committing credential revocation.
What happens if another request creates or configures a Connection while that enumeration is happening?
You can get this:
deletion enumerates credentials
↓
concurrent request creates new credential
↓
deletion removes everything it saw
↓
Auth user is deleted
↓
new Vault secret is now orphaned
The feature needs a barrier.
That led to explicit root locking and an account-wide mutation guard.
Once account deletion begins, authenticated Owner mutations capable of introducing new affected state must fail closed.
The implementation eventually gained durable account-deletion attempt state on the Profile, locked enumeration, carefully ordered root locks, and a service-level operational guard that other mutation paths had to compose.
Now “delete account” included concurrency control.
Then retries become concurrent too
Retries sound simple until two of them happen.
An earlier account-deletion attempt may be left in an uncertain state. Later, the system reconciles that attempt through the Auth Admin read surface.
But what if two retriers perform that reconciliation at the same time?
A stale reconciliation result must not authorize a destructive replay after another retrier has already advanced the state.
So the deletion attempt received an identity.
State transitions became conditional on the exact attempt UUID being reconciled.
The service used compare-and-swap semantics against that attempt.
Expired active attempts used a lease based on the database clock rather than the application clock.
In simplified form:
attempt A becomes uncertain
↓
retrier 1 reconciles A
retrier 2 reconciles A
↓
only a transition still bound to A
may authorize the next operation
This is the kind of requirement nobody puts in the issue title.
It is also the kind of requirement an independent Reviewer is very good at forcing into the open.
A deleted user may still have a valid JWT
There was another unpleasant detail.
Supabase access tokens are stateless JWTs.
Deleting the user does not reach backward through time and erase already-issued tokens. An old access token can remain cryptographically valid until it expires.
So successful account deletion could not simply mean:
JWT signature valid
→ authenticated
OpenOrc also needed its Profile-resolution boundary to fail closed.
After the Auth user disappears, the database cascade removes the OpenOrc Profile.
A still-valid old JWT must not be able to recreate that Profile, because its corresponding Auth user no longer exists.
No separate OpenOrc session blacklist was necessary, but the authentication boundary had to preserve that invariant.
Another small sentence in the acceptance criteria.
Another thing that makes “delete account” not small.
The pull request tells the real story
The finished pull request touched configuration, a Supabase Auth Admin adapter, a database migration, persistence locking, lifecycle guards, administrative deletion services, account-deletion state machinery, credential behavior, tests, and durable engineering guidance.
Among other things it introduced:
- a backend-only Supabase secret-key boundary;
- explicit classification of successful, failed, and uncertain Auth Admin outcomes;
- durable deletion-attempt state;
- account-wide mutation barriers;
FOR UPDATElocking for affected roots and Connections;- database-clock leases;
- reconcile-before-replay behavior;
- compare-and-swap transitions tied to exact attempt IDs;
- credential revocation before external deletion;
- failure-safe retries;
- defense against stale JWT restoration;
- secret-redaction requirements;
- tests for races that should never be possible in production.
The deterministic test suite at the end of the task reported:
1063 passed
143 integration tests deselected
ruff clean
pyright: 0 errors
There were additional real-Postgres integration checks for the things mocks are particularly bad at proving: composite constraints, locking behavior, serialization, attempt-state persistence, and the Auth-root cascade.
This was not 100 million tokens spent teaching an agent how to issue a DELETE.
Where did all those tokens go?
Part of the answer is simply that coding agents repeatedly consume context.
A long-running task rereads code, tests, instructions, plans, tool output, review findings, previous changes, and the growing conversation.
A 500,000-token working context used repeatedly across many turns can produce cumulative token consumption that looks absurd very quickly.
But there was another factor.
This task went through the Producer-Reviewer workflow behind OpenOrc.
The Producer would propose a design.
The Reviewer would find a failure mode.
The Producer would revise it.
The revision would expose another concurrency or recovery question.
Implementation would begin.
Tests or review would reveal another invariant.
The Producer would remediate it.
Then the result would be reviewed again.
That loop is intentionally adversarial.
Its purpose is not to minimize turns.
Its purpose is to make incorrect code harder to ship.
The cost is that difficult tasks can become very large conversations.
A hundred million tokens did not cost what you might think
There is another reason the token count needs context.
At the time of this task, I was running GLM-5.3 Flash through OpenRouter at a blended cost of roughly $0.04 per million tokens.
At that rate:
100 million tokens
× $0.04 / million
≈ $4
That is the approximate marginal model cost of the Producer, not the total cost of the workflow.
The Reviewer was GPT-5.6 Sol in ChatGPT. I was already paying for ChatGPT as a subscription, so those review turns did not add a metered per-token API bill.
This makes the spectacular-looking token count much less spectacular in dollar terms.
It also exposes another architectural lesson.
A tempting approach to coding agents is:
Just use the strongest model available for everything.
Producer? Strongest model.
Reviewer? Strongest model.
Every revision, repository read, test failure, tool result, and replay of a very large context? Strongest model again.
Now imagine this same task running entirely through a premium metered API.
The engineering problem has not changed.
The number of revision cycles has not changed.
The context still grows.
But 100 million tokens can suddenly represent a very different bill.
Coding-agent economics are particularly sensitive to this because a large context is not paid for once. Parts of it may be processed repeatedly across a long sequence of turns.
A 500,000-token working context inside a task that runs through many planning, implementation, testing, and remediation cycles can therefore become tens or hundreds of millions of aggregate tokens surprisingly quickly.
Using the most expensive model for every one of those tokens is not automatically a sign of higher engineering quality.
In my workflow, a very inexpensive Producer paired with an independent, stronger Reviewer has worked better than simply putting the strongest available model into both roles.
That gives the Producer-Reviewer split a second benefit beyond independent reasoning.
It lets us allocate inference differently by role.
not:
minimum tokens
and not:
strongest model everywhere
but:
right model × right role × right amount of inference
The Producer may consume enormous numbers of tokens while exploring a repository, editing code, running tools, reacting to failures, and iterating on implementation. That role can benefit enormously from a capable model whose token economics make sustained autonomous work practical.
The Reviewer has a different workload. It receives comparatively discrete artifacts and is asked to reason critically about them. Spending more capability there can make sense even when using that same model for every Producer token would not.
This is not an argument that cheap models are always better.
It is an argument that model allocation is part of systems engineering.
Once agents start consuming tens or hundreds of millions of tokens on real tasks, choosing which model performs which role can matter as much as optimizing the code that orchestrates them.
100 million is not a success metric
I do not consider the token count a badge of honor.
If we can produce the same engineering quality with 20 million tokens, I would strongly prefer 20 million.
If an agent is repeatedly rediscovering facts it should have been given once, rereading irrelevant context, generating redundant tests, or wandering through unconstrained revisions, that is waste.
And this task helped expose places where our own development process could improve.
By the end of Phase 2A, OpenOrc had more than a thousand deterministic tests, plus a growing real-Postgres integration suite.
Running the entire database test battery for every relevant change had become slow enough that we changed the workflow: the Producer should choose and run the smallest meaningful real-database validation set while it still has the context needed to know which tests matter.
That is a process improvement caused by observing the agent workflow itself.
The goal is not maximum token consumption.
The goal is to spend computation where it buys confidence.
But the number does tell us something
Traditional software estimates compress enormous amounts of engineering detail into tiny labels.
“Add account deletion.”
“Implement authentication.”
“Support retries.”
“Add GitHub integration.”
The apparent size of the sentence has almost no relationship to the size of the correctness surface underneath it.
Human engineering teams already know this. We just normally observe it through days, meetings, incidents, design documents, review cycles, and project overruns.
Agentic development gives us another measurement.
Tokens are an imperfect one.
But when a task called “implement administrative deletion and account lifecycle services” crosses 100 million of them, it is difficult to maintain the fiction that the work was ever a small CRUD feature.
The interesting question becomes not:
Why did the AI need so many tokens?
but:
How much engineering reasoning was hiding behind the feature we thought we had named?
For this task, the answer was quite a lot.
And that may be one of the more useful things these systems are beginning to make visible.