The fixed-price project that cost me three weeks of unpaid work
Fixed price transfers all estimation risk to you. Price it or change the contract shape.
TL;DR
I quoted £12,000 for a six-week KMP migration. It took nine and a half weeks. I delivered it, I was paid £12,000, and three and a half weeks of my work was unpaid — about £7,000 at my day rate.
The overrun was not incompetence. Two of the three causes were things I could not have known without access I did not have at quoting time: an undocumented native dependency and a CI system only the client could show me. Fixed price meant I had agreed to absorb whatever those turned out to be.
Fixed price is a risk transfer, and the risk being transferred is estimation uncertainty. Either price it explicitly, or change the contract so the uncertainty sits with whoever can resolve it.
The case this post is about
The brief: migrate an Android app's data and domain layers to Kotlin Multiplatform, with iOS consuming the shared module. I had done two similar migrations.
My estimate:
data layer (Room → SQLDelight) 2 weeks
domain + use cases 1 week
networking (Retrofit → Ktor) 1.5 weeks
iOS integration + sample screen 1 week
buffer 0.5 weeks
───────────
6 weeks @ £2,000/week = £12,000
Signed as fixed price, fixed scope, with a 50% deposit. I had seen the repository's README and a screen-share of the app, and not the codebase.
The symptom
Nine and a half weeks:
planned actual reason
data layer 2 weeks 3.5 weeks an undocumented native library in the DAO path
domain 1 week 1 week
networking 1.5 weeks 2 weeks a custom OkHttp interceptor chain for mTLS
iOS integration 1 week 2.5 weeks their CI could not build a framework; I built it
buffer 0.5 weeks 0.5 weeks consumed in week 2
─────────
9.5 weeks
The three causes, and whether I could have known:
cause knowable at quoting time?
an undocumented JNI library used by
four DAOs, no source available NO — needed repository access
a custom mTLS interceptor chain PARTLY — I could have asked "any custom
networking?" and did not
their CI could not produce an iOS
framework; no Mac runners existed NO — needed CI access
One of three was my failure to ask. Two required access I did not have and did not request.
The financial outcome:
quoted £12,000
weeks worked 9.5
effective rate £1,263/week (vs my £2,000 target)
unpaid, at my day rate ~£7,000
And the second-order cost: I turned down a four-week engagement in week 7 because I was still on this one. That was another £8,000 of opportunity, so the real cost of the mis-estimate was closer to £15,000 than £7,000.
Why the obvious thing did not work
"Estimate better." Two of three causes were invisible without access. Better estimation technique cannot resolve information I did not have — and the one cause I could have found by asking is the only part where "estimate better" applies.
"Add a bigger buffer." I had 0.5 weeks, consumed in week 2. Had I quoted 9 weeks, I would probably not have won the work — the client had two other quotes at 6 and 7 weeks. A buffer large enough to cover genuine unknowns prices you out, which is the actual bind.
"Invoke a change order when it ran over." My contract had no change-order clause. "Fixed price, fixed scope" with no mechanism for discovering that the scope was described wrong means the only options were to absorb it or to stop work, and stopping would have cost me the reference.
"Ask for repository access before quoting." This is the right answer and I did not do it, because asking felt like friction on a deal I wanted. That reluctance is the thing worth naming: the cost of asking was social discomfort, and the cost of not asking was £15,000.
The actual mechanism
A contract shape decides who holds which risk, and there are three risks in a project like this:
Estimation risk — the work takes longer than expected. Fixed price puts this entirely on the contractor.
Scope risk — what is wanted changes. Fixed scope nominally puts this on the client and in practice on the contractor, because "that was always implied" is an argument you lose.
Discovery risk — the system is not as described. This one is the trap: the client holds the information and the contractor holds the consequence.
Fixed price is a legitimate shape when estimation risk is small and knowable, which means you have seen the system. When you have not, fixed price is a bet on information you do not have, and the client is not even aware you are making it.
Which makes the contract shape a derivable answer rather than a negotiating instinct — the inputs are all known before you quote:
def contract_shape(*, seen_the_code, test_coverage_known, integrations,
undocumented_integrations, spec_pages, prior_projects_with_client):
blockers = []
# Discovery risk is the trap: the client holds the information, the contractor
# holds the consequence. Not having seen the system is disqualifying on its own.
if not seen_the_code:
blockers.append("have not seen the codebase")
if undocumented_integrations > 0:
blockers.append(f"{undocumented_integrations} integration(s) with no documentation")
if not test_coverage_known:
blockers.append("unknown test coverage — no signal on regression cost")
if blockers:
return ("time-and-materials", blockers,
"Or: a paid discovery phase, fixed price, 2-3 days, deliverable is the estimate.")
# Even with full information, a long project accumulates scope risk that no amount
# of estimation skill addresses. Cap the fixed-price window rather than the price.
if spec_pages > 20 or integrations > 3:
return ("fixed-price per milestone", [],
"Whole-project fixed price over this size is estimation skill vs. compounding scope.")
if prior_projects_with_client == 0:
return ("fixed-price, 50% deposit", [], "No history: the deposit is the scope-change brake.")
return ("fixed-price", [], "")
On the named case — the quote that cost three unpaid weeks:
>>> contract_shape(seen_the_code=False, test_coverage_known=False,
... integrations=4, undocumented_integrations=2,
... spec_pages=6, prior_projects_with_client=0)
('time-and-materials',
['have not seen the codebase',
'2 integration(s) with no documentation',
'unknown test coverage — no signal on regression cost'],
'Or: a paid discovery phase, fixed price, 2-3 days, deliverable is the estimate.')
Three blockers, every one of them knowable the afternoon the quote went out. The six-page spec was
the thing that made it feel well-defined — and spec_pages is deliberately not consulted until
after the discovery checks, because a detailed specification describes what the client wants, not
what the system you have never opened will make it cost.
I have seen the codebase, done this exact migration twice → fixed price is reasonable
I have seen a README and a screen-share → fixed price is a wager

Fixed price transfers estimation and discovery risk to you. That is fine when you have the information to price them, and it is a wager when the client holds information you have not asked for.
The fix
Three changes. I have used all three since and the first one does most of the work.
1. A paid discovery phase, which is the whole fix:
## Phase 1 — Discovery (fixed price, 3 days, £1,800)
Deliverables:
- a written migration plan with a module-by-module breakdown
- a list of risks found, each with an impact estimate in days
- a fixed-price quote for Phase 2, valid for 30 days
Access required: read access to the repository, a CI run log, and 90 minutes
with an engineer who knows the networking layer.
If you choose not to proceed to Phase 2, the plan is yours.
Three paid days buys me the information that makes a fixed price honest, and it buys the client a plan they keep either way. In the four engagements since, discovery has cost clients £1,800 and changed the Phase 2 quote by between −15% and +80% — which is the uncertainty becoming visible instead of being absorbed.
2. A change-order clause, so discovery mid-project has a path:
## Changes
Work outside the Scope section, or work required by a condition of the system
not described in the Discovery report, is a Change Order: a written estimate
in days, signed by both parties before the work begins, billed at £400/day.
Either party may decline a Change Order. Declining pauses only the affected
deliverable.
"A condition of the system not described in the Discovery report" is the sentence that would have covered the JNI library and the missing Mac runners.
3. And where discovery is not possible, a different shape:
## Option B — Time and materials with a cap
£400/day, invoiced fortnightly, capped at 30 days without a signed extension.
Weekly written progress with days-used and days-remaining.
The cap is what makes T&M acceptable to a client who fears an open-ended bill — it bounds their exposure while leaving the estimation risk with the party who cannot control it either.
The questions I now ask before quoting anything fixed:
- May I have read access to the repository for two hours?
- Does CI currently build and test every target you need? Can I see a recent run?
- Any native code, JNI, custom networking, or vendor SDKs in the affected layers?
- Who decides "done", and have they written down what that means?
- What has been tried before and why did it stop?
Five questions, about ten minutes. The third one alone would have found two of my three causes.
How this turned out
The four engagements since, with discovery:
project discovery Phase 2 quote actual variance
A £1,800 £14,000 £14,000 0%
B £1,800 £21,600 £23,200 +7% (one change order, signed)
C £1,800 £9,000 £9,000 0% (quote was 80% above my
pre-discovery instinct)
D £1,800 — — client declined Phase 2 after
reading the risk list
Project C is the clearest case for discovery: my instinct before seeing the code was about £5,000, and the code contained a problem worth four extra weeks. Quoting from instinct would have repeated the original mistake almost exactly.
Project D is the other half: the client read the risks and decided not to proceed. I was paid for the discovery, they avoided a project that would have overrun, and neither of us lost anything. A discovery phase that produces a "no" is working.
And the effective rate:
effective weekly rate
the fixed-price job £1,263
A £2,000
B £1,940
C £2,000
One honest limit: a paid discovery phase is an extra step in a sales conversation, and I have lost work because of it — twice, to contractors who quoted a fixed price immediately. I do not know whether those projects went well for them. What I do know is that the three-day phase is the difference between quoting from information and quoting from hope, and I am not willing to go back to the second one to win a deal.
What this generalizes to
A contract shape is a risk allocation, and it is worth naming which risk. Fixed price, time and materials, retainer, milestone-based, value-based. None is correct in general; each puts estimation, scope and discovery risk in different places.
Do not accept a risk you cannot price. If the information needed to estimate is held by the other party, either get the information or decline the shape. Absorbing it silently is a wager nobody told the client you were making.
Charge for the work that makes an estimate honest. Discovery is real work with a real deliverable. Giving it away for free is how you end up quoting from a README — and clients readily pay for a plan when it is framed as one.
The discomfort of asking is cheaper than the overrun. The reluctance to ask for repository access cost me around £15,000. A ten-minute question list, asked before quoting, is the single highest-return habit I have adopted.
Related
- Scope creep is a contract problem, not a communication problem — the change-order clause in practice
- The rate conversation I kept getting wrong — pricing the other way round
- An estimate is a distribution, and you reported the mode — the estimation half of this
Keep reading
This is part of Freelancing & consulting for engineers — the commercial side of the work, written from the mistakes. Follow MobileByteSensei for the rest.
One post a week, no digest
New writing when it lands. No roundups, no course pitch, no "10 tips".