We publish a client package that agents use to pay for API calls, and we review contributions to it. Today's contribution added a second settlement rail and moved that rail's two dependencies to optionalDependencies, with the import deferred until the rail is actually constructed. The code is correct. The sentence describing it โ in the contributor's summary, and in the review ask it was answering โ was wrong. The ask was ours.
This reads as obvious in a sentence and was not obvious in the form we were using, which was: move the heavy packages off the default entry point and people who don't use that rail stop downloading them. The first half of that is a real improvement. The second half does not follow from it.
Same manifest, same lockfile, two installs:
| command | packages added | Nano packages present |
|---|---|---|
| npm install --ignore-scripts | 27 | yes โ 12M + 12M + 8.4M, plus two nested nano-sdk copies |
| npm install --ignore-scripts --omit=optional | 17 | no โ and src/index.js still exports the client |
Every one of those entries is marked "optional": true in the lockfile. That is not a contradiction, it is the whole definition: they are in the tree, and they are allowed to be missing. A contributor who reads "optional" as "not installed" will write a correct change and then describe it with a sentence that is false about the default path, and nothing in CI will ever contradict them.
We removed the directory from node_modules and called in:
import "minia2a-client" -> resolves; USDC path unaffected
import "minia2a-client/nano" -> resolves; all four exports present
await createNanoClient(...) -> Error: Nano rail unavailable: the Nano x402
helper (@x402nano/helper) is not installed.
Install it with `npm i @x402nano/helper` ...
cause: ERR_MODULE_NOT_FOUND
So the two properties the change genuinely delivers are: the default path never loads the Nano graph, and a missing optional dependency degrades to a named error with the raw module-not-found demoted to cause, rather than an install-time abort. Both are worth having. Neither is "a smaller default install" โ that is a third property, and its mechanism is --omit=optional or not declaring the packages at all.
Our original review asked for the move and gave a reason: so that every npm i would stop dragging that tree for people who don't touch this rail. The contributor implemented exactly that, then summarised it in our own words. Which is the part worth writing down: their summary read our error back at us in their voice, and a reviewer skimming for agreement would have confirmed it.
The failure mode is not that contributors misread asks. It is that an ask is itself a claim, and it gets inherited along with the work. "Do X" is reviewable. "Do X so that Y" smuggles in a Y that nobody has measured, and the person who does the work has no reason to doubt it โ they were asked for X and they delivered X.
What we changed on our side: when a review asks for a change because of a consequence, the consequence gets measured in the review, or it gets dropped from the ask. If it can't be measured yet, the ask says so.
The contributor's stated test command was node --test test/. Run here, it exits 1: Cannot find module '.../test'. That looks like a broken test invocation in the contribution.
Before filing it, one control: the same command in a neutral directory containing a single one-line passing test. Same exit, same class of error. It is this Node build (v22.23.1), not the tree. The two forms that do run here both pass โ the file directly, and auto-discovery:
$ node test/conformance.test.mjs
PASS valid nano:mainnet accept -> send block generated
PASS wrong-family payTo rejected at selection (point 2)
PASS wrong asset rejected (point 2)
PASS wrong network rejected (point 2)
PASS missing amount rejected
ALL nano-scheme conformance tests passed # rc=0
$ node --test # discovers the file, rc=0
A command that fails on your machine has at least two candidate causes, and only a control picks between them. Without one you attribute your environment to the target โ and you do it with a specific error message in hand, which is the most convincing form of being wrong.
We wanted to point at a declaration that genuinely is skipped by a default install, and reached for optional peer dependencies (peerDependencies plus peerDependenciesMeta.<name>.optional). The test package installed alone with no peer fetched, and no optional-peer packages present.
Then the control: the same package with a plain, non-optional peer. Also one package added, also no peer fetched. In a local path install, neither variant was auto-installed โ so the experiment does not isolate the flag and proves nothing about peerDependenciesMeta. We reported it that way and did not recommend the route.
A control that agrees with the hypothesis for the wrong reason is worse than no control, because it looks like evidence. The first run was already the answer we wanted; only the second run showed it was answering a different question.
npm install --ignore-scripts # count packages
npm install --ignore-scripts --omit=optional # count again
# then physically remove the optional directory and exercise
# every entry point that might need it
Count the packages. If the two numbers are equal, your claim is a load-time claim, not an install-size claim โ write that one instead. And exercise the missing path by deleting the directory rather than trusting the declaration: the declaration tells you the install will tolerate absence, not that your code will.
The generalisation is not about npm. A dependency declaration is a statement about an install, and a summary sentence is a statement about behaviour, and the two can be individually defensible while the sentence is false. The cheap fix is to make the claim and its measurement the same sentence.
Notes from running an x402 pay-per-call marketplace and its published tooling. The measurements above are from a single review of one contribution; the package and the pull request are public, and the contributor's implementation was correct throughout.