Proposal
Extract the patron identity + MCP client stack into a shared @dpyc/mcp-client
package, the way tollbooth-dpyc already owns crypto/vault/pricing for the Python
side. Today it exists as five hand-maintained forks, and the hardening in them was
paid for with real incidents that did not propagate evenly.
This is filed here rather than in a service repo because it is a fleet decision.
Verified duplication
Measured with md5 and diff on 2026-08-30 across the five frontends
(taxsort-mcp is excluded — it predates this stack and has no lib/mcp.ts).
| File |
excalibur |
roastify |
cypher |
goodearth |
optionality |
components/NpubGate.tsx |
323 L |
323 L |
306 L |
323 L |
1028 L |
lib/inlineProof.ts |
30 L |
30 L |
30 L |
30 L |
37 L |
lib/sessionNsec.ts |
65 L |
65 L |
65 L |
65 L |
83 L |
lib/nostrProfile.ts |
77 L |
77 L |
77 L |
77 L |
63 L |
lib/mcp.ts (generic head) |
~645 L |
~645 L |
~645 L |
~645 L |
~645 L |
Differences against the excalibur baseline, in changed lines (diff | grep -cE '^[<>]'):
| File |
vs roastify |
vs cypher |
vs goodearth |
NpubGate.tsx |
4 |
35 |
4 |
inlineProof.ts |
4 |
4 |
4 |
sessionNsec.ts |
2 |
2 |
2 |
nostrProfile.ts |
0 |
0 |
0 |
mcp.ts first 645 L |
23 |
394 |
79 |
And the differences are exactly the trivia the claim predicted:
NpubGate.tsx, excalibur vs roastify — 2 changed lines, both brand strings
(Sign in to eXcalibur / Sign in to Roastify).
sessionNsec.ts — 1 changed line, the localStorage prefix
(excalibur:session_nsec:v1 vs cypher:session_nsec:v1).
inlineProof.ts — comments only (an example tool name in a doc comment).
nostrProfile.ts — byte-identical md5 11d6e5ad3f0541e7d90a44f8b97cc461 in
four of five repos (excalibur, roastify, cypher, goodearth).
Roughly 6,400 lines of duplicated surface across the five repos.
The hardening this would centralize
Each of these was bought with an incident, and each currently lives in 4-5 copies:
- Lowercase
error_code comparison. The wheel emits lowercase snake_case
(proof_required, proof_refresh_needed); an UPPERCASE compare never matched,
so a lapsed proof silently failed every paid call instead of re-presenting the
gate.
- Evicting the recent-login entry alongside the bounced proof token. Otherwise
the returning-user one-tap immediately replays the same dead token and re-bounces.
- A
bestEffort flag so a diagnostic/background call cannot log the user out.
Why this is not hypothetical: the same bug was fixed twice, differently
Item 1 regressed independently in at least two copies, and the two repos resolved
it in incompatible shapes:
- excalibur / roastify / cypher / goodearth — fixed inline in
mcp.ts, with
a comment recording the incident.
- optionality — fixed by extracting
lib/proofExpiry.ts, whose own header
says: "This exact check has regressed as a case-sensitivity bug ... which
silently failed every paid call ... The dedicated test in
verify/proofExpiry.smoke.ts guards against that recurring."
That regression test exists in exactly one of five repos:
excalibur-mcp (none)
roastify-mcp (none)
cypher-mcp (none)
optionality-mcp frontend/verify/proofExpiry.smoke.ts
goodearth-mcp (none)
Item 3 has actually diverged in behavior, not just in spelling. excalibur added
bestEffort (11 call sites) precisely so a background read cannot drop the gate.
optionality has no such flag and deliberately went the opposite way — it
broadcasts PROOF_EXPIRED_EVENT so the gate drops even when the failing call is a
background hydration read. Same concern, two copies, opposite resolutions, neither
aware of the other.
A live example of the cost
While building the sixth copy (goodearth-mcp), CheckPriceResult was found to
declare base_cost / effective_cost / cost — field names that
tollbooth-dpyc's check_price has never returned in its entire history
(the wheel returns base_cost_api_sats / effective_cost_api_sats; verified with
git log --all -S '"effective_cost"' -- '*.py' → no commits). The bad type was
copied into all five frontends and had to be filed five times:
One shared package would have made that one fix in one place. Instead the copies
drifted: optionality's call sites had already grown as unknown casts to reach the
real field names, while the other three carry a type nothing has exercised yet.
Migration cost
- 5 repos, 5 files each:
components/NpubGate.tsx, lib/inlineProof.ts,
lib/sessionNsec.ts, lib/nostrProfile.ts, and the generic head of lib/mcp.ts.
- The per-repo variation is genuinely tiny and already isolated: a
SLUG
constant (used for the storage-key prefix and the MCP tool namespace) and a
brand string for NpubGate. Both fit a single config object passed at init.
lib/mcp.ts splits cleanly at the existing section marker: everything above
// ─── Posts CRUD (paid) ─── (line 644 in excalibur) is generic; below it is
per-service domain tools that stay in the repo.
- optionality-mcp is the outlier (a 1028-line
NpubGate.tsx and a 1227-line
mcp.ts) and should migrate last; its lib/proofExpiry.ts +
verify/proofExpiry.smoke.ts are the best starting point for the shared
package's proof logic and test.
- taxsort-mcp is out of scope — it predates this stack (
hooks/useMCP.ts, no
lib/mcp.ts) and shares only a divergent nostrProfile.ts.
Risk of not doing it
Every hardening fix must be applied five times by hand and silently isn't:
the case-sensitivity regression test is in 1 of 5, bestEffort is in 4 of 5 with
optionality behaving oppositely, and the CheckPriceResult defect needed four
separate issues. A seventh frontend inherits today's fixes and none of tomorrow's.
Filed by Scout. All numbers above were measured, not estimated. No source files
were modified.
Proposal
Extract the patron identity + MCP client stack into a shared
@dpyc/mcp-clientpackage, the way
tollbooth-dpycalready owns crypto/vault/pricing for the Pythonside. Today it exists as five hand-maintained forks, and the hardening in them was
paid for with real incidents that did not propagate evenly.
This is filed here rather than in a service repo because it is a fleet decision.
Verified duplication
Measured with
md5anddiffon 2026-08-30 across the five frontends(taxsort-mcp is excluded — it predates this stack and has no
lib/mcp.ts).components/NpubGate.tsxlib/inlineProof.tslib/sessionNsec.tslib/nostrProfile.tslib/mcp.ts(generic head)Differences against the excalibur baseline, in changed lines (
diff | grep -cE '^[<>]'):NpubGate.tsxinlineProof.tssessionNsec.tsnostrProfile.tsmcp.tsfirst 645 LAnd the differences are exactly the trivia the claim predicted:
NpubGate.tsx, excalibur vs roastify — 2 changed lines, both brand strings(
Sign in to eXcalibur/Sign in to Roastify).sessionNsec.ts— 1 changed line, the localStorage prefix(
excalibur:session_nsec:v1vscypher:session_nsec:v1).inlineProof.ts— comments only (an example tool name in a doc comment).nostrProfile.ts— byte-identical md511d6e5ad3f0541e7d90a44f8b97cc461infour of five repos (excalibur, roastify, cypher, goodearth).
Roughly 6,400 lines of duplicated surface across the five repos.
The hardening this would centralize
Each of these was bought with an incident, and each currently lives in 4-5 copies:
error_codecomparison. The wheel emits lowercase snake_case(
proof_required,proof_refresh_needed); an UPPERCASE compare never matched,so a lapsed proof silently failed every paid call instead of re-presenting the
gate.
the returning-user one-tap immediately replays the same dead token and re-bounces.
bestEffortflag so a diagnostic/background call cannot log the user out.Why this is not hypothetical: the same bug was fixed twice, differently
Item 1 regressed independently in at least two copies, and the two repos resolved
it in incompatible shapes:
mcp.ts, witha comment recording the incident.
lib/proofExpiry.ts, whose own headersays: "This exact check has regressed as a case-sensitivity bug ... which
silently failed every paid call ... The dedicated test in
verify/proofExpiry.smoke.tsguards against that recurring."That regression test exists in exactly one of five repos:
Item 3 has actually diverged in behavior, not just in spelling. excalibur added
bestEffort(11 call sites) precisely so a background read cannot drop the gate.optionality has no such flag and deliberately went the opposite way — it
broadcasts
PROOF_EXPIRED_EVENTso the gate drops even when the failing call is abackground hydration read. Same concern, two copies, opposite resolutions, neither
aware of the other.
A live example of the cost
While building the sixth copy (goodearth-mcp),
CheckPriceResultwas found todeclare
base_cost/effective_cost/cost— field names thattollbooth-dpyc'scheck_pricehas never returned in its entire history(the wheel returns
base_cost_api_sats/effective_cost_api_sats; verified withgit log --all -S '"effective_cost"' -- '*.py'→ no commits). The bad type wascopied into all five frontends and had to be filed five times:
One shared package would have made that one fix in one place. Instead the copies
drifted: optionality's call sites had already grown
as unknowncasts to reach thereal field names, while the other three carry a type nothing has exercised yet.
Migration cost
components/NpubGate.tsx,lib/inlineProof.ts,lib/sessionNsec.ts,lib/nostrProfile.ts, and the generic head oflib/mcp.ts.SLUGconstant (used for the storage-key prefix and the MCP tool namespace) and a
brand string for
NpubGate. Both fit a single config object passed at init.lib/mcp.tssplits cleanly at the existing section marker: everything above// ─── Posts CRUD (paid) ───(line 644 in excalibur) is generic; below it isper-service domain tools that stay in the repo.
NpubGate.tsxand a 1227-linemcp.ts) and should migrate last; itslib/proofExpiry.ts+verify/proofExpiry.smoke.tsare the best starting point for the sharedpackage's proof logic and test.
hooks/useMCP.ts, nolib/mcp.ts) and shares only a divergentnostrProfile.ts.Risk of not doing it
Every hardening fix must be applied five times by hand and silently isn't:
the case-sensitivity regression test is in 1 of 5,
bestEffortis in 4 of 5 withoptionality behaving oppositely, and the
CheckPriceResultdefect needed fourseparate issues. A seventh frontend inherits today's fixes and none of tomorrow's.
Filed by Scout. All numbers above were measured, not estimated. No source files
were modified.