Skip to content

x402 paywall docs and implementation plan#1

Merged
mnemonik-dev merged 86 commits into
mainfrom
dev
Jun 21, 2026
Merged

x402 paywall docs and implementation plan#1
mnemonik-dev merged 86 commits into
mainfrom
dev

Conversation

@mnemonik-dev

Copy link
Copy Markdown
Owner

No description provided.

mnemonik-dev and others added 30 commits June 16, 2026 11:42
- agent-payment-happy-path.puml
- developer-onboarding.puml
- payment-error-flows.puml
- contract-architecture.puml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ic architecture

- patterns.md: fee configurable (owner-only, default 50bps, cap 1000bps)
- architecture.md: EVM chain-agnostic, Arc as first chain, Base post-MVP
- tech-spec: deviations marked resolved

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wholesale rewrite addressing 22+ critical findings beyond review.md:

Architecture pivot:
- Adopt standard x402 v1 wire format (JSON body in 402, base64 X-PAYMENT)
- Replace EIP-2612 permit with EIP-3009 transferWithAuthorization
- Self-hosted facilitator inline in middleware (~150 LoC TS, no external service)
- Custom PaymentSplitter wraps transferWithAuthorization for on-chain non-custodial split
- Arc Testnet only for MVP (Arc Mainnet not launched by Circle)
- Two-layer replay protection (USDC authorizationState on-chain + middleware NonceStore)

Side effects:
- Update CLAUDE.md (Base -> Arc Network), architecture.md (Payment Flows, data
  model, integrations rewritten for x402 + Arc), patterns.md (EIP-3009 instead
  of permit, dual-layer replay, Pausable, registry semantics)
- Rewrite user-spec.md ACs to match new contract signature, x402 wire format,
  framework adapters (Node http + Fastify), price parsing
- Rewrite 4 PlantUML diagrams (happy path, contract architecture, onboarding,
  error flows) for facilitator model
- Add code-research.md with verified Arc Testnet constants and x402 spec refs
- Remove test.txt stray artifact, add .fastembed_cache to .gitignore

20 tasks across 7 waves (5 above the soft limit; user notified for splitting
decision in Phase 5).
…ory + vault pivot)

Round-1 validators consolidated: 7 critical, ~20 major, ~25 minor.
User-approved Path 2 architecture (per-developer vault factory) + all fixes.

Key architectural change:
- Replace shared PaymentSplitter (+ payWithAuthorization wrapper) with
  PaymentSplitterFactory + PaymentVaultImpl (EIP-1167 minimal proxy via
  Clones.cloneDeterministic). Each developer registers their own vault.
- Middleware calls USDC.transferWithAuthorization directly (no wrapper).
- developerId field gone — recipient determined cryptographically by
  EIP-3009 `to` field bound to the per-dev vault address.

Eliminates 3 critical findings:
- Cross-developer payment-attribution race attack (signed `to` is unique).
- Open-registration griefing (vault owner = msg.sender, immutable).
- Non-x402-standard payload field (`developerId`) removed from X-PAYMENT.

Additional security + correctness fixes:
- OZ 5.x Ownable2Step (two-step owner transfer)
- Storage-based ReentrancyGuard (Arc 1153 support unverified)
- OpaqueRelayerKey wrapper (non-enumerable, redacted toString/toJSON)
- Startup chainId pin + rpcUrl trust check (D14)
- X-PAYMENT 4 KB size cap; malformed → HTTP 400 (not 402)
- Settlement failure taxonomy: 7 classified reasons
- waitForTransactionReceipt 30s timeout
- authorization_not_yet_valid error code added
- NonceStore: synchronous has+insert, 100k cap, TTL eviction
- Fee math: only full withdraw() (no partial), gross-fee is exact
- SafeERC20 for non-standard token return values
- CAIP-2 canonical network IDs (eip155:5042002) + alias

Test plan tightened:
- ajv validation of 402 body against vendored x402 v1 schema
- EIP-712 tamper tests across chainId, verifyingContract, name, version
- Forked-integration test runs in CI by default (mock USDC + factory + vault)
- Live Arc Testnet test gated on ARC_TESTNET_E2E=1 (nightly)
- Both adapters (Node http + Fastify) covered by integration

Task count: 20 → 18 (consolidated Wave 3+4).

Project knowledge updates:
- project.md: free-tier vs paid-tier vault model clarified
- deployment.md: env vars (ARC_RPC_URL, PAYWALL_RELAYER_KEY,
  PLATFORM_TREASURY_ADDRESS, PAYMENT_SPLITTER_FACTORY_ADDRESS)
- patterns.md: factory + vault, EIP-3009, dual-layer replay, off-chain pause
- architecture.md: project structure + Tech Stack + Payment Flows updated

Diagrams: all 4 redrawn for vault-as-payTo and direct USDC.transferWithAuthorization.
All 5 round-1 critical findings stayed resolved. Round 2 surfaced 6 new
critical issues across 5 validators; this commit applies all 6 + 14 key
major findings.

Critical fixes:
- Wave 2 was labelled sequential but parallel; split into Wave 2 (T4
  contracts) and Wave 3 (T5 contract tests) so the orchestrator respects
  the dependency. Renumbered all downstream waves.
- Task 16 (now T15) Pre-deploy QA: added Verify-smoke (all suites exit 0
  + checklist report committed). Task 17 (now T16) Deploy: added the
  catalog-default reviewers (code-reviewer, security-auditor,
  deploy-reviewer) — the prior empty list violated the skills catalog.
  Task 18 (now T17) Post-deploy: added Verify-smoke.
- Fixed function selector for transferWithAuthorization: was 0xef55bec6
  (which is receiveWithAuthorization); correct selector is 0xe3ee160e.
- D15 added: PaymentVaultImpl constructor calls _disableInitializers() so
  the impl cannot be hijacked via direct initialize. New test row in
  PaymentVaultImpl.test.ts.
- D16 added: vault has no receive() / fallback() payable — surfaces the
  user-spec constraint as a Decision and Data Models comment so reviewers
  reading only the tech-spec see it. New test row.
- D17 added: developer / factory are write-once in initialize, no setters
  exist; ABI test asserts neither setDeveloper(address) nor
  setFactory(address) selector is exposed.

Other significant changes:
- D18 added: SecurityLogger interface (10 named events, default no-op) for
  OWASP A09 coverage; wired through core.ts.
- D13 expanded: OpaqueRelayerKey now also redacts via
  [util.inspect.custom], handles pino/winston serialization, and is
  applied to REGISTER_KEY too.
- Risks expanded:
  * per-payment economics with Wave-1 gas spike (from external-analysis.md)
  * owner-key compromise (no timelock; multisig recommendation)
  * pause race condition (5s cache window, intentional)
  * setFeeBps / setPlatformTreasury front-running vs vault.withdraw
  * untrusted RPC (D14 chainId pin covers wrong-chain, not adversarial)
  * rogue clone deployment (canonical factory address in NETWORKS is the trust root)
- Risks: relayer_no_balance detection logic specified (proactive balance
  read + reactive classifier).
- User-Spec Deviations: no-partial-withdraw (D4 rationale) added.
- Testing Strategy: CREATE2 cross-component invariant test, register
  reentrancy test, ABI-shape assertions for no-receive / no-setter,
  fee-snapshot semantics, adapter unit tests for both Node http + Fastify,
  factory-state cache TTL tests, D14 chainId pin test, OpaqueRelayerKey
  util.inspect / pino / winston / structuredClone redaction tests.
- Task 3 (USDC spike) reviewers fixed (catalog defaults for code-writing);
  also estimates real gas cost on Arc Testnet to validate per-payment
  economics from external-analysis.md.
- Task 8 (now T7) description compressed (deferred internals to D5/D14).
- Tasks consolidated: T6+T7 (round 2) merged into T6 (pure modules).
  Total task count 18 → 17 (still over the 15 soft cap, but compression
  beyond this loses logical task boundaries).
- user-spec.md: SafeERC20.balanceOf removed (function doesn't exist in
  OZ 5.x SafeERC20 — uses plain IERC20.balanceOf).
- Acceptance Criteria heading clarified as "technical complement".
- Diagrams: cosmetic fix for bytes32(msg.sender) shorthand → explicit
  bytes32(uint256(uint160(msg.sender))) Solidity cast.

Round 2 validator reports (in logs/techspec/) re-deleted ahead of round 3.
Round 3 results:
- tech-spec-validator: PASS (approved, 3 minors)
- completeness-validator: PASS (41/41 covered)
- skeptic: 1 critical
- test-reviewer: 3 major
- security-auditor: 1 critical + 5 major

Iteration 3 fixes:

Skeptic critical — leftover bytes20(developer) in Solution paragraph
(was an earlier-draft remnant; D3 already used the canonical
bytes32(uint256(uint160(...))) form). Replaced the inline formula in
Solution with a back-reference to D3 so the canonical salt formula is
defined exactly once.

Security critical — redactor regex too narrow:
- D13 redefined as defense-in-depth (primary: non-enumerable +
  custom serialization hooks; secondary: scrubSecrets helper with
  broadened regex set covering 0x{64}, raw {64}, 0x{130} signatures,
  and brand-typed values matching the OpaqueRelayerKey symbol).

Security majors:
- Rogue-clone Risks entry rewritten: defense is CREATE2 address
  derivation off-chain against canonical factory baked into NETWORKS,
  NOT a self-reported vault.factory field. Wording aligned with
  reality of the middleware's settle path.
- D17 expanded with 4 defense-in-depth enforcement layers: source
  review, ABI test, NatSpec @Custom:security-invariant, and D15's
  delegatecall ban.
- D15 expanded with @Custom:security-invariant
  (no_selfdestruct no_delegatecall single_initializer) and a static-
  analysis lint step in contracts/package.json + CI; SELFDESTRUCT and
  DELEGATECALL bans rationalized against EIP-1167 clone-bricking.
- D16 documents carve-outs: SELFDESTRUCT-deposit (de minimis on Arc
  since USDC IS native gas), pre-deploy CREATE2 funding (the intended
  prepay UX), Arc dual-decimal foot-gun (Wave 1 spike confirms 6dec).
- D18 SecurityLogger payload schema fully typed:
  10 named events, each with a fixed payload shape (no free-form
  payloads → no PII leak; payerHash/developerEoaHash instead of raw
  addresses; no raw signatures; no relayer info). Logger calls wrapped
  in try/catch + fire-and-forget to prevent self-DoS.

Test majors:
- Settlement failure → replay-store retention: test row added
  asserting nonce entry NOT deleted on failure → same-nonce retry
  returns nonce_already_used.
- Cross-adapter replay protection in forked-e2e: test row updated
  to pay via Node http then retry SAME X-PAYMENT on Fastify in the
  same process (verifies process-singleton NonceStore claim).
- register CLI tests: new test row in scripts/__tests__/register.test.ts
  asserts the CLI never prints the developer key (stdout/stderr scrub),
  asserts incorrect REGISTER_KEY exits non-zero without input echo.

Remaining minors (non-blocking): documented_deviation cosmetic
([PENDING USER APPROVAL] resolves at status flip), test-reviewer
minors about validBefore boundary and concurrent-cache assertions
(can be addressed at task-decomposition time).
Status: draft -> approved on both user-spec.md and tech-spec.md frontmatter.
User-Spec Deviations marker flipped: [PENDING USER APPROVAL] -> [APPROVED 2026-06-16].

Three iterations of validation completed (5 validators per round, 15 reports
total). Final state:
- tech-spec-validator: PASS (approved)
- completeness-validator: PASS (41/41 ACs covered)
- skeptic: all blockers/criticals resolved
- test-reviewer: all criticals + major test rows added
- security-auditor: all criticals resolved; majors either fixed or
  documented as accepted trade-offs in Risks

Next step: run /decompose-tech-spec to generate task files.
…anteen thesis)

Captures the validation I previously did only in conversation. Documents:
- Per-thesis alignment (9 article theses → our position)
- Authors' own admitted caveats (compliance glossed, donor privacy,
  single-vendor risk, distribution-not-rails framing) and how each lands
  for our design
- The one technical critique that survives all three review surfaces
  (per-payment vs batched settlement) and our deferred-to-post-MVP plan
- Decisions deliberately not taken in response (creator-stack pivot,
  batched settlement, removing platform fee, Circle Gateway dependency)
- Pointers into the spec for each item
Decomposition of approved tech-spec across 8 waves:
- Wave 1: T1 (monorepo), T2 (hardhat), T3 (USDC spike + gas measurement)
- Wave 2: T4 (factory + vault contracts)
- Wave 3: T5 (contract tests)
- Wave 4: T6 (middleware pure modules — types, networks, x402 codec, errors, relayer-key, replay-store)
- Wave 5: T7 (verify + settle), T8 (core orchestrator + adapters + index)
- Wave 6: T9 (middleware unit tests), T10 (forked + arc-testnet e2e), T11 (deploy + register CLI + README)
- Audit Wave: T12 (code audit), T13 (security audit), T14 (test audit)
- Final Wave: T15 (pre-deploy QA), T16 (deploy + npm publish), T17 (post-deploy verification)

T3 flagged a wave-sequencing concern: it modifies networks.ts which T6
creates (Wave 4); task-creator emitted a JSON-artifact fallback at
contracts/scripts/arc-testnet-usdc-domain.json for T6 to consume.
Flagged for validation pass.
Iteration 2 of task-decomposition validation cycle. 18 critical findings
across 10 validators (4 task-validator batches + 6 reality-checker batches).

Systemic fixes (logged at logs/tasks/iteration-2-systemic-fixes.md):
1. Wave renumbering — strict invariant: depends_on tasks must be in
   strictly earlier wave. Renumbered 17 tasks across 13 waves (was 8).
2. Test directory canonicalization — all vitest tests under
   packages/middleware/src/__tests__/ (was split src/__tests__ vs
   __tests__ root). Single tree.
3. Canonical env var names per deployment.md (ARC_RPC_URL,
   PAYWALL_RELAYER_KEY, PLATFORM_TREASURY_ADDRESS,
   PAYMENT_SPLITTER_FACTORY_ADDRESS, DEPLOYER_KEY, REGISTER_KEY).
4. Canonical error reason strings — to_mismatch (not recipient_mismatch),
   insufficient_amount (not value_too_low), mine_timeout vs rpc_timeout
   per settle.ts classifier.
5. Shared resource ownership clarified per D13: WalletClient owned by
   settle.ts (only OpaqueRelayerKey extractor); PublicClient + NonceStore
   + factory-state cache owned by core.ts. T7 also owns chainId pin.
6. parseUsdPrice() owner: x402.ts (T6 module), tested in x402.test.ts.
7. withdraw transfer order: developer FIRST, platform SECOND
   (matches code-research.md; T5 reentrancy test attacks developer).
8. arc-mainnet placeholder: chainId 0 + enabled:false (NOT eip155:42161
   which is Arbitrum One's real ID).
9. npm run test:e2e script added to T1 (root scaffolding).
10. T17 read-factory.mjs: replaced file ref with inline node -e
    one-liner (no separate file).
11. T15 forked-e2e command: runs via hardhat test, not npm test.
12. T13/T14 decisions.md race: removed from Context Files (orchestrator
    composes feature-level summary after audit wave).
13. networks.ts sentinel comments — T6 owns, T11 deploy script patches
    via sed.

Per-task fixes:
- T2/T3 env var conflict resolved
- T3 wave 1→2, drops cross-wave networks.ts patch (JSON-artifact only)
- T4 withdraw order; grep guard refined to exclude NatSpec
- T5 wave 3→4; reentrancy test uses MaliciousDeveloper; mocks under test/mocks
- T6 wave 4→5; phantom test files removed; parseUsdPrice added
- T7 wave 5→6; WalletClient owned here; viem WaitForTransactionReceiptTimeoutError
- T8 wave 5→7; core.ts holds OpaqueRelayerKey opaque, doesn't extract
- T9 wave 6→8; test dir canonicalized; phantom tests removed
- T10 wave 6→9; depends_on adds T9; mocha timeout for cache-TTL wait
- T11 wave 6→8; sed-style networks.ts patch (no TS cross-import); register
  CLI tests live in packages/middleware to share vitest config
- T12 wave 7→10; cross-reference language removed
- T13 wave 7→10; D1-D18 coverage matrix explicit; slither hard requirement
- T14 wave 7→10; network normalization test row added; coverage gate owned
- T15 wave 8→11; correct forked-e2e command; PaymentVaultImpl AC count
- T16 wave 8→12; npm publish via package name; --provenance pre-flight;
  release tag after publish success
- T17 wave 8→13; inline node -e for read-factory; readiness loop for HTTP
  smoke; scratch dir is type:module
15 critical findings + ~20 major remain after iteration-2 fixes.
Mostly cross-task drift: constructor signatures (T4/T11/T16), function
signatures (T7/T8 verifyEip3009Authorization), SecurityLogger ownership
(T9 tests forbidden emissions in settle/verify), parseUsdPrice('0')
behavior (T6 accepts, T9 throws), infrastructure feasibility
(T6 needs vitest not installed by T1, T10 Hardhat in-process has no
HTTP URL, T11 __dirname doesn't exist in ESM).
…ixes

Iteration 3 fixes (15 round-2 criticals + ~20 majors):

Canonical decisions added in logs/tasks/iteration-3-addendum.md:
- §1: factory constructor is 3-arg (usdc, treasury, feeBps:uint16);
  vaultImpl deployed inside; initialOwner via Ownable2Step default.
- §2: SecurityLogger emission owner = core.ts ONLY. settle.ts and
  verify.ts never import the logger; they return classified results.
- §3: verifyEip3009Authorization is 2-arg (payload, opts);
  nonceStore lives inside opts.
- §4: parseUsdPrice('0') throws InvalidPriceError (zero is not a payment).
- §5: payerHash = '0x' + keccak256(from).slice(2, 10) — 10-char total.
- §6: authorization_already_used_onchain classifier uses STRING revert
  matching ('authorization is used' substring); no 4-byte selector.
- §7: relayer_no_balance threshold = constant 1_000_000n (1 USDC).
- §8: validBefore safety margin = 5000 ms (both prose and code in ms).
- §9: vitest+ajv+pino+winston installed by T1 (root devDeps).
- §10: T10 spawns hardhat node (not in-process); Mocha function() syntax.
- §11: ESM __dirname replacement via fileURLToPath(import.meta.url).
- §12: OpaqueRelayerKey extract imported from INTERNAL relayer-key.ts,
  never from public index.
- §13: viem installed in contracts/ via T2.
- §14: paths.sources single string; mocks under contracts/test/mocks/.
- §15: T5 register reentrancy test replaced with source-pattern check
  (initialize has no developer callback).
- §16-18: T13/T14 stale path/workspace fixes.
- §19: T2 reviewer paths repo-relative.
- §20: T16 sentinel attribution to T6/T11, not T13.

Other:
- work/x402-agent-payment/decisions.md created as empty stub so tasks
  can reference it as Context File without crashing (post-completion
  log appended by feature-execution).
- user-spec.md canonicalized test path src/__tests__/ and workspace
  name @universal-paywall/middleware.

16 tasks updated (T12 was already approved in round 2).
10 round-3 validators launched after iteration-3 fixes. 9 stalled at the
600s watchdog before writing their JSON reports (each was on the final
'now writing JSON' step). 1 (template-batch1, T1-T5) completed.

Skill iteration budget exhausted (3 rounds of fix+validate done).

Findings extracted from batch-1 JSON + transcript fragments of stalled
batches:

Critical (1):
- T5 retains a Fallback option for mock placement that contradicts
  iteration-3 addendum §14 (paths.sources single string; mocks under
  contracts/test/mocks/).

Major (2):
- T3 stdout JSON shape vs artifact JSON shape inconsistency (notes? field)
- T8 uses --workspace=packages/middleware (path form) instead of
  @universal-paywall/middleware (package name form) — same class as
  T13 round-2 finding.

Minor:
- T5 loadFixture incorrectly called 'sync wrapper' (it's async)
- T5 __dirname workaround scope note missing for CJS contracts/ context
- T1 smoke step doesn't pin --passWithNoTests requirement

Per skill protocol: present remaining issues to user for resolution.
Current task files are ready for /do-task execution with the documented
minor caveats.
Per user decision: switch contract toolchain to Foundry (forge + anvil +
slither). Foundry's invariant + fuzz support is materially better for the
security surface here (factory/vault + malicious actor scenarios + fee
math overflow). Tests written in Solidity, mocks in same compile unit
(no parallel TS test runner). OpenZeppelin via forge install submodule.

Tech-spec D9 flipped: Hardhat -> Foundry. Dependencies updated.

iteration-4-addendum.md sets canonical paths, commands, and per-task
adjustments. Tasks updated:

T1: Pin --passWithNoTests requirement (must stay until T6 lands first
    vitest test).
T2: MAJOR REWRITE — Foundry setup. foundry.toml, forge install
    OpenZeppelin v5.0.2, remappings.txt, slither.config.json. Drops
    Hardhat. Adds ABI export hook (copies contracts/out/*.json to
    packages/middleware/src/abi/ for T7/T10 viem consumption).
T3: artifact JSON now includes notes?: string[] field; T6 surfaces.
T4: Source paths contracts/src/ (was contracts/contracts/). Mocks
    under contracts/test/mocks/. Compile commands forge build (was
    hardhat compile).
T5: MAJOR REWRITE — Foundry tests in Solidity (.t.sol). Includes
    invariant tests (VaultInvariants.t.sol), fuzz tests (testFuzz_*),
    MockMaliciousTreasury for dynamic reentrancy. CREATE2 cross-component
    via OZ Clones.predictDeterministicAddress (no off-chain TS compute).
    Coverage via forge coverage --report lcov >=95% branch.
T8: --workspace=packages/middleware -> @universal-paywall/middleware
    (package-name form) — final round-3 finding.
T10: anvil spawn replaces hardhat node spawn. Vendored ABI from T2's
    post-build copy. Programmatic contract deploy via viem in beforeAll.
T11: MAJOR REWRITE — two-step deploy: forge script Solidity for
    contract deploy + tsx post-deploy.ts for networks.ts sentinel patch.
    No Hardhat anywhere. --force flag on post-deploy idempotency check.
T13: forge test + forge coverage + slither replace hardhat commands.
T14: forge coverage --report lcov; AC requires invariant + fuzz test
    presence verification.
T15: Test command set updated to forge test + forge coverage --report
    summary.
T16: Deploy via forge script ... --broadcast --verify + tsx post-deploy
    + npm publish (unchanged).

Not touched (no Foundry impact): T6, T7, T9, T12, T17.

Round 3 validator stalls (9 of 10) prevent final verdict; per skill
budget exhausted, user approved 'apply known fixes and go'.
Bootstrap npm workspace root and packages/middleware as ESM-only TypeScript
package per tech-spec Task 1. Establishes infra (TypeScript strict, tsup,
vitest, ESLint, Prettier, Husky + gitleaks pre-commit) so subsequent waves
can land code, tests, and publish without further devDep installs.

- Root package.json declares workspaces packages/*, apps/*, contracts;
  devDeps include vitest@^1.5 (root-level runner for CI per §9 addendum).
- packages/middleware: type=module, exports map without require branch,
  publishConfig.access=public/provenance=true, scripts test/test:e2e use
  --passWithNoTests (pinned via "//test" sibling comment until T6).
- tsconfig: strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes
  + verbatimModuleSyntax; excludes src/__tests__ per systemic-fix §2.
- tsup.config.ts: ESM-only (format ['esm']), dts, target node20, no minify.
- .husky/pre-commit runs gitleaks protect --staged --redact then
  npm run lint (middleware-only scope).
- .gitleaks.toml extends default rules.
- .gitignore adds .husky/_/ (husky v9 auto-generated shim dir).
Initialize the `contracts/` Foundry workspace per tech-spec D8/D9 and
iteration-4-addendum §1, §2, §4 T2. Tooling only — no Solidity sources,
no tests, no deploy scripts. Sets up the canonical layout so subsequent
contract tasks can `cd contracts && forge build` immediately.

- foundry.toml: solc 0.8.20, optimizer 200 runs, [profile.ci] with
  fuzz=1000 / invariant runs=256 depth=64, arc_testnet rpc endpoint
  bound to ${ARC_RPC_URL}. Leading comment documents the foundryup
  bootstrap and canonical env-var names.
- OpenZeppelin contracts pinned to v5.0.2 via `forge install` as git
  submodule under contracts/lib/openzeppelin-contracts. forge-std also
  installed (so Task 5 tests inherit forge-std/Test.sol with zero
  friction). .gitmodules records both, with branch = v5.0.2 on the OZ
  entry for documentation.
- foundry.lock pins the exact submodule SHAs (forge 1.6 generates this
  automatically for reproducible builds).
- remappings.txt maps @openzeppelin/contracts/ to lib/openzeppelin-contracts/contracts/.
- slither.config.json filters test/,lib/ and keeps exclude_low=false so
  Task 13's audit ignores mocks + OZ library but still surfaces lows in
  production sources.
- scripts/export-abi.ts is the post-build ABI export hook: copies forge
  artifacts to packages/middleware/src/abi/ for viem consumption in
  Tasks 7 and 10. Safe no-op when contracts/out/ is missing (T2 state).
- contracts/package.json declares the TS-only dev surface (viem, tsx,
  typescript, @types/node). No hardhat, no @nomicfoundation/*, no chai,
  no mocha, no ts-node — the legacy Hardhat stack is explicitly dropped
  per iteration-4-addendum §4 T2.
- tsconfig.json is ESM-only, strict, scoped to scripts/**/*.ts.
- .env.example lists the canonical names ARC_RPC_URL, DEPLOYER_KEY,
  ARC_VERIFY_API_KEY. Forbidden legacy names ARC_TESTNET_RPC_URL and
  ARC_TESTNET_PRIVATE_KEY are absent across the workspace.

Deviation from spec: `forge install --no-commit` flag does not exist in
forge 1.6 (no-commit is now the default; --commit is opt-in). Omitted
the flag; behavior matches what the spec intended.

Smoke: `cd contracts && forge build` exits 0 (Nothing to compile);
`cd contracts && npm run build` exits 0 (forge no-op + export-abi
skip); `npx tsc --noEmit -p tsconfig.json` exits 0.
Apply round-1 findings from code-reviewer-t1, security-auditor-t1, and
infrastructure-reviewer-t1:

- T1-M1 (code-reviewer): align tsup.config.ts quotes with Prettier
  singleQuote rule (was double-quoted, would diff on next format run).
- SA-T1-02 (security-auditor): add overrides.esbuild ^0.28.1 in root
  package.json to resolve GHSA-gv7w-rqvm-qjhr (HIGH — esbuild binary
  integrity check). All consumers (tsup, vitest/vite, tsx) now dedupe
  to esbuild@0.28.1.
- SA-T1-03 (security-auditor): add .npmrc to .gitignore — prevents an
  accidental commit of project-level npm publish tokens before Task 16.
- SA-T1-04 (security-auditor): add prepublishOnly guard in middleware
  package.json — accidental npm publish during development now fails
  with an informative error. Task 16 removes the guard.

Non-blocking notes:
- SA-T1-01 (vitest 1.6.1 CRITICAL via GHSA-5xrq-8626-4rwp): not
  exploitable with current scripts (vulnerability requires vitest --ui,
  which no script uses). Task spec pins vitest ^1.5.0; upgrade to ^3.2.6
  is a major migration tracked in decisions.md, not addressed in T1.
  Added sibling "//audit-vitest" comment in scripts to document the ban
  on vitest --ui until upgrade.
- T1-M2 (ajv production-vs-dev placement): informational only;
  T6 implementer must promote ajv to dependencies if used in runtime
  src/ paths.

Smoke verification re-run: lint, typecheck, build, test, npx vitest all
exit 0. esbuild deduplicates to 0.28.1 across the dependency tree.
Capture round 1 + round 2 review JSON reports for Task 1 from
code-reviewer-t1, security-auditor-t1, and infrastructure-reviewer-t1,
and add Task 1 entry to decisions.md.

Round 1 verdicts: approved_with_minors / conditional_pass / APPROVED.
Round 2 verdicts: approved / pass / APPROVED stands.
- contracts/.env.example: replace all-zeros DEPLOYER_KEY placeholder with
  a non-functional sentinel `0x<your-0x-prefixed-64-hex-char-private-key>`
  (the all-zeros key is a publicly-known monitored address; using it as a
  copy-paste placeholder risks a developer silently transacting with it).
  Addresses T2-C1 (code-reviewer-t2) and T2-SEC-01 (security-auditor-t2).
- contracts/foundry.toml: document `foundry.lock` in the leading comment
  block as a forge 1.6+ secondary lock that complements submodule SHA
  pinning. Addresses T2-C2 (code-reviewer-t2).
- .gitmodules: add `branch = v1.16.1` to the forge-std submodule entry
  for parity with the OZ entry. The cryptographic pin remains the
  submodule SHA + foundry.lock SHA. Addresses T2-SEC-02
  (security-auditor-t2, low) and INF-T2-001 (infrastructure-reviewer-t2,
  informational).

Round 1 verdicts:
- code-reviewer-t2: approve_with_minor
- security-auditor-t2: PASS (2 low advisories)
- infrastructure-reviewer-t2: APPROVED (1 informational)
Add six review reports (3 reviewers × 2 rounds) and append decisions.md
entry for Task 2 (Foundry contracts workspace scaffolding).

Round 1 verdicts:
- code-reviewer-t2: approve_with_minor (2 minor findings)
- security-auditor-t2: PASS (2 low advisories)
- infrastructure-reviewer-t2: APPROVED (1 informational)

Round 2 verdicts (after fixes in d5c5dc7):
- code-reviewer-t2: approved (no remaining findings)
- security-auditor-t2: PASS / APPROVE (no remaining findings)
- infrastructure-reviewer-t2: approved (no remaining findings)
Standalone tsx script + JSON artifact for the Wave 2 spike. Reads
name/version/decimals/supportsEip3009 from canonical Arc Testnet USDC
(0x36...00), self-checks the transferWithAuthorization selector
(0xe3ee160e), probes method existence for transferWithAuthorization +
authorizationState, estimates per-payment gas cost in micro-USDC with a
documented fallback when the node refuses to estimate a reverting call,
and writes arc-testnet-usdc-domain.json for Task 6 to consume.

Spike outcome: supportsEip3009=true, decimals=6, gas cost 1290 micro-USDC
(exceeds the 500 micro-USDC / 5%-of-0.01 threshold — flagged for
post-MVP batched-settlement follow-up per Risks row 4, not blocking).
Dual-decimal note emitted for T6 module-load surfacing.
- CF-01: align nativeCurrency.name with empirical chain value ("USDC",
  not "USD Coin"); add comment explaining the divergence.
- EH-01: document intent of the two empty catch blocks in readGasPriceWei
  (any throw is intentionally treated as "this pricing path unusable";
  next branch / notes-flag is the fall-through).
- DOC-01: add Task 3 entry to decisions.md per post-completion checklist:
  spike outcome values, literal notes[] array, gas-threshold breach note,
  open items for T6 (consume artifact, surface notes at module load).

Artifact regenerated by smoke run — sampleGasCost now 1212 micro-USDC
(varies with live Arc gasPrice; still exceeds threshold, still warning-only).
- SEC-T3-01 / SEC-T3-02: introduce scrubUrls() and errMessage() helpers,
  apply to every err.message / err.shortMessage push into notes[] and
  every console.error call site. Prevents an authenticated RPC URL
  (e.g. Infura-style endpoint with a path-embedded API key) from leaking
  into stderr or the committed artifact via viem's HttpRequestError.
- SEC-T3-INFO-01: add resolveRpcUrl() guard that requires ARC_RPC_URL to
  start with http:// or https://; rejects empty/bogus values up-front
  without echoing the offending value (which could be a secret).

Stdout / artifact shape unchanged; smoke still exit 0; lint clean.
- T3-M1 (medium): hoist hard-blocker checks (supportsEip3009===false,
  decimals!==6) above artifact write and stdout emission, so a failed
  spike no longer leaves stale or wrong-valued JSON on disk for T6 to
  pick up. Restructure main() to fail fast.
- T3-M2 (medium): introduce GAS_COST_UNAVAILABLE sentinel string used
  when gas pricing is unavailable, so consumers can distinguish "we
  couldn't measure" from a real "0 micro-USDC". Field stays string-typed.
- T3-L1 (low): tighten ambiguous-probe note text in both
  probeAuthorizationState and probeTransferWithAuthorization — explicitly
  state "treating as absent to avoid false positive" so reviewers see the
  conservative classification choice.
- T3-L3 (low): expand the sanity-band comment to spell out what the
  wildlyLow / wildlyHigh predicates actually catch (off-by-12-orders or
  inverted conversion) so a future reader doesn't have to reverse-engineer
  the intent.

Smoke still exit 0; output shape unchanged (sampleGasCost varies with
live Arc gasPrice — 1260 micro-USDC this run; still exceeds threshold).
Lint exit 0. gitleaks 0 leaks.
mnemonik-dev and others added 27 commits June 17, 2026 11:05
- Vendor x402 v1 JSON Schema at src/__tests__/fixtures/x402-v1.schema.json
  (covers PaymentRequirements, PaymentPayload, ChallengeBody, XPaymentResponse).
- Add ajv-based schema validation to x402.test.ts (build402Body, decodeXPayment
  round-trip, encodeXPaymentResponse) and errors.test.ts (every reason →
  ChallengeBody validates).
- Add shared helpers under src/__tests__/helpers/ (sign.ts, mock-clients.ts,
  timers.ts, encode-header.ts, recording-logger.ts) for downstream T10 e2e
  reuse and tamper-matrix tests.
- Wire @vitest/coverage-v8 + vitest.config.ts with v8 provider and 85% line /
  statement / function thresholds (75% branch).
- Expand unit tests for x402.ts (validAfter/validBefore decimal int guard,
  empty/non-string network), verify.ts (unknown network, sig-recovery throw,
  default nowMs), settle.ts (balance-read TimeoutError → rpc_timeout, unknown
  network), relayer-key.ts (forged-brand throws on getRelayerKeySecret).
- Coverage: 96.87% lines, 89.15% branches across src/ (errors, replay-store,
  relayer-key, verify, adapters at 100%).
- Add test:coverage npm script; add coverage/ to .gitignore; allowlist
  vitest.config.ts in ESLint ignorePatterns (build tooling, outside tsconfig).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- contracts/script/Deploy.s.sol: forge script for PaymentSplitterFactory
  (3-arg canonical constructor, env-driven, USDC_ADDRESS optional override
  with T3-verified Arc Testnet default, console2.log of both deployed
  addresses).
- contracts/scripts/post-deploy.ts: reads forge broadcast/run-latest.json,
  extracts factoryAddress + vaultImplAddress (inner CREATE in
  additionalContracts[0]), patches networks.ts via sentinel-anchored
  substitution. Idempotent. Refuses to overwrite already-populated
  arc-testnet without --force (exit 4).
- scripts/register.ts: tsx CLI that calls factory.register() via viem.
  REGISTER_KEY wrapped in OpaqueRelayerKey immediately; getRelayerKeySecret
  imported from internal relayer-key.js per iter-3 §12. Pre-flight
  vaults() check makes re-runs print "Already registered" and exit 0.
  Typed error reasons mirror settle.ts taxonomy. Never echoes the key.
- packages/middleware/src/__tests__/register-cli.test.ts: 7 spawn-based
  tests against a local anvil. Programmatic factory deploy via viem
  reading the forge artifact. Skips with a clear message if anvil or the
  artifact is missing. ESM __dirname via fileURLToPath per iter-3 §11.
- README.md: developer onboarding (faucet → register → install → run)
  plus a maintainer-only deploy callout. No Hardhat references.
  Canonical env-var names only (DEPLOYER_KEY, PLATFORM_TREASURY_ADDRESS,
  ARC_RPC_URL, REGISTER_KEY, PAYWALL_RELAYER_KEY).

Smoke verified:
- anvil --chain-id 31337 + forge script ... --broadcast → broadcast file
  with factory at additionalContracts[0]=PaymentVaultImpl.
- npx tsx contracts/scripts/post-deploy.ts --chain-id 31337 → patches
  networks.ts with only the two sentinel-anchored address literals.
- Re-run: zero additional diff.
- post-deploy.ts --chain-id 5042002 without --force on already-patched
  networks.ts → exit 4 with actionable message.
- Register CLI happy path, idempotent re-register, malformed/missing
  key (exit 2), arc-mainnet disabled (exit 3), --help (exit 0).

All 244 middleware tests pass (7 new). forge build, tsc, eslint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- T9-R1-01 (code-reviewer, REQUIRED): networks.test.ts now uses
  `dirname(fileURLToPath(import.meta.url))` instead of bare `__dirname` for
  canonical ESM module path resolution. vitest tolerates `__dirname` via its
  CJS-compat shim, but every other test file in the suite uses the explicit
  ESM pattern — this aligns networks.test.ts with that convention and removes
  the implicit dependency on the shim.
- T9-R1-03 (code-reviewer, suggested): raised `vitest.config.ts`
  `thresholds.branches` from 75 to 85 — aligns with lines/statements/functions
  thresholds. Current branch coverage is 89.15% so there is headroom; a future
  drop below 85% will fail the build.
- T9-R1-05 (code-reviewer, suggested): fixed misleading comment on
  `oversizedHeader()` in `helpers/encode-header.ts`. 'A' is a valid base64
  char; the function works because the size guard fires on raw byte length
  pre-decode, so base64 validity is irrelevant. Comment now reflects that.

T9-R1-02 and T9-R1-04 are spec-vs-impl drift in the D18 event catalog
(`relayer_low_balance` payload shape and `nonce_replay` vs
`nonce_replay_attempt` event name). The current tests correctly track the
`SecurityEventCatalog` type in `types.ts`, which is authoritative at the type
level. Reconciling those names against tech-spec D18 belongs to T8's
catalog-owner, not T9. Flagged in T9 decisions.md follow-up.

244/244 tests still pass. Coverage: 96.87% lines, 89.15% branches, 100%
functions across `src/`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SA-T11-01 (medium): register.ts classifyError() now scrubs err.message
  via scrubSecrets() before pattern matching. Defense-in-depth so a
  future maintainer adding stderr forwarding cannot leak a hex key.
- SA-T11-02 (medium): drop --allow-test-network flag entirely.
  test-anvil network branch in register.ts is now gated exclusively on
  NODE_ENV=test. The existing test suite already sets NODE_ENV=test in
  spawn env, so no test changes were needed for this fix.
- SA-T11-03 (low): post-deploy.ts JSON.parse failure now throws a
  static message ("malformed JSON; re-run forge script") instead of
  interpolating the inner error.
- SA-T11-04 (low): README maintainer callout now notes that
  $PLATFORM_TREASURY_ADDRESS / $DEPLOYER_KEY must be masked in CI logs.
- SA-T11-05 (info): added 8th test, register_never_prints_key_on_rpc_failure
  — spawns with a valid REGISTER_KEY but an unreachable RPC (port 1),
  asserts non-zero exit, "register_failed:" reason, and zero hex-key
  pattern matches in combined stdout+stderr.

All 245 middleware tests pass (1 new). tsc + eslint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SEC-T9-01 (Medium): `ChallengeBody.additionalProperties` is now `false` in
  `fixtures/x402-v1.schema.json`, restoring strict drift detection on the 402
  challenge body. Added `txHash` to the explicit property allowlist (core.ts
  emits it on settlement_failed). The previous `true` setting silently passed
  any extra field, defeating the purpose of vendoring the schema.
- SEC-T9-02 (Medium): renamed `nonce_replay` → `nonce_replay_attempt` to
  match the tech-spec D18 canonical event catalog. Affects `types.ts`
  (SecurityEventCatalog key + docblock), `core.ts:539` emit site, and
  `core.test.ts:580` parameterized event-name map. Security monitoring
  configured against the canonical D18 name now actually receives events.
- SEC-T9-03 (Low): added `core.test.ts::stale paused-cache + zero vault on
  refresh still short-circuits BEFORE verify`. Twin of the non-stale guard
  test at line 272. Models the path where the first request finds
  `vaults()=0x0` (no permanent pin per D3 — only non-zero addresses are
  cached forever), the TTL expires, the refresh succeeds, vaults still
  returns 0x0, and the vault_not_deployed short-circuit must fire before
  verify is called with expectedVaultAddress=ZERO_ADDRESS.

246/246 tests pass. Coverage holds at 96.87% lines / 89.15% branches /
100% functions across `src/`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records test review round-1 attribution for task 11. Implementation
changes were absorbed into 1d2be23 by the parallel T9 agent due to
shared workspace timing.

Findings addressed for task 11 register-cli.test.ts:
- M1 (shared_state): register_handles_already_registered self-sufficient.
- M2 (flakiness): waitForPort dead createServer removed.
- M3 (assertion_quality): broad 0x{64} regex moved to already-registered
  path; happy-path uses literal-substring checks only.
- L1 (malformed coverage): sweep three malformed key shapes.
- L2 (disabled-network leak check): assert no key in stdout/stderr on
  arc-mainnet rejected path.
- L3 (USDC name): declined — README "USDC" matches T3-verified on-chain
  value per decisions.md Task 3; user-spec example is the outlier.

All 246 middleware tests pass. tsc + ESLint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- T11-01 (minor): add `timeout: 30_000` to waitForTransactionReceipt
  in register.ts so a lost tx surfaces as a typed register_failed
  result instead of hanging the CLI indefinitely.
- T11-02 (minor): already addressed in test-review round 1 fixes
  (commits 1d2be23 + 869478b — dead createServer in waitForPort
  removed).
- T11-03 (minor): README arcscan URL fixed to the tech-spec canonical
  https://testnet.arcscan.app (was .network).
- T11-04 (minor): declined — README "USDC" matches the T3-verified
  on-chain value (decisions.md Task 3: "chain returns the literal
  string 'USDC' rather than the marketing-form 'USD Coin'"). The
  user-spec example is the outlier and is already tracked as a
  deferred user-spec amendment (SA-T6-INFO-02 in decisions.md
  Task 6 open items).

All 246 middleware tests pass. tsc + ESLint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- T9-F1 (medium): added Fastify adapter test `handler exception propagates
  via Fastify error path (not swallowed by plugin)`. Mirrors the node-http
  adapter's exception-propagation test. Registers fastifyPaywall, mocks
  paywall to return passthrough, then has the downstream route handler
  throw — expects res.statusCode === 500 (Fastify's default error handler
  ran). A regression that swallowed exceptions in the preHandler would now
  fail loudly.
- T9-F2 (medium): added cold-checkout guard in networks.test.ts. The
  T3-artefact-comparison test now uses `it.skip` if the
  `arc-testnet-usdc-domain.json` file is absent (existsSync gate), with the
  rest of the suite still running. The dependence on T3 is structurally
  unchanged (networks.ts itself reads the artefact at module-load time),
  but the guard prevents a raw ENOENT from masking a useful skip message.

T9-F3 (nonce_replay → nonce_replay_attempt) is already resolved in commit
1d2be23 (same finding overlapped with SEC-T9-02).

247/247 tests pass. Coverage holds at 96.87% lines / 89.15% branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 1 + round 2 reports for code-reviewer, security-auditor, and
test-reviewer.

Verdicts after round 2:
- code-reviewer-t11: approved
- security-auditor-t11: PASS (cleared to ship)
- test-reviewer-t11: passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Status: Done (pending user verification of README walkthrough)
- Commits: ef9752a + e14734f + 1d2be23 + 869478b + 5d8c2bd + 0c215d0
- Reviews after round 2: code-reviewer approved, security-auditor PASS,
  test-reviewer passed
- Documented deviations (--allow-test-network removed; classifyError
  scrubs message; sentinel regex tolerates both quote styles; USDC
  domain name decline rationale) and open items (post-ship classifier
  fix for 4xx, user-spec amendment carry-forward, T16/T17 handoff).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-1 + round-2 JSON review reports from code-reviewer-t9,
security-auditor-t9, and test-reviewer-t9. All three reviewers approved
T9 by round 2:

- code-reviewer-t9: APPROVED (1 required + 4 suggested in R1 → 0 in R2)
- security-auditor-t9: PASS (2 medium + 1 low in R1 → 0 in R2)
- test-reviewer-t9: PASSED (3 medium + 3 low in R1 → 0 in R2; lows optional)

Also includes the T9 decisions.md entry documenting the implementation,
deviations (nonce_replay → nonce_replay_attempt D18 alignment, schema
fixture additionalProperties:false fix, SEC-T9-03 test variant), and
open follow-ups (relayer_low_balance threshold field, vitest upgrade).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All three reviewers approved by round 2. T9 unit-test expansion is
complete: 247/247 tests pass, 96.87% line coverage / 89.15% branch
coverage on src/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- forked-e2e.test.ts spawns real anvil, deploys MockUsdcEip3009 +
  PaymentSplitterFactory + PaymentVaultImpl via viem (reading Foundry
  artifacts from contracts/out/), exercises Node http and Fastify
  adapters in the same process. Pins the D5 cross-adapter NonceStore
  replay-rejection invariant: pay via Node http, replay identical
  X-PAYMENT on Fastify → 402 nonce_already_used. Also covers
  vault_not_deployed and paused branches.
- arc-testnet-e2e.test.ts gated on ARC_TESTNET_E2E=1 via
  describe.skipIf. Reads canonical env (ARC_RPC_URL, PAYWALL_RELAYER_KEY,
  ARC_TESTNET_PAYER_PK, ARC_TESTNET_DEVELOPER_EOA,
  PAYMENT_SPLITTER_FACTORY_ADDRESS). Ajv-validates the 402 body
  against the vendored x402 v1 schema; asserts X-PAYMENT-RESPONSE +
  on-chain vault balance delta.
- vitest.config.ts: add testTimeout: 60_000 and hookTimeout: 30_000
  (required by anvil spawn + 5 s factory-state cache TTL wait).

253 passing + 2 skipped on default invocation; lint and typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- T10-R1-M2: fastify happy path now decodes X-PAYMENT-RESPONSE and
  asserts {success, transaction, network, payer} just like node-http
  (catches any future Fastify adapter regression that would write a
  malformed XPR header).
- T10-R1-M1: drop the unused `developerBEoa` extraction + `void` lint
  escape in the vault_not_deployed test.
- T10-R1-M3: document that the Arc Testnet afterAll intentionally
  does not close the viem PublicClient (http transport keeps no
  persistent sockets).

253 passing + 2 skipped; typecheck and lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- T10-R1-F1: paused-branch cache-TTL wait grows 5_500 → 8_000 ms so a
  slow CI runner / GC pause can't land the request while the cache is
  still warm. Comfortable 3 s margin above the 5 s middleware TTL,
  still well inside testTimeout: 60_000.
- T10-R1-F2: vault_not_deployed test now signs against
  factory.computeVaultAddress(devB) (the CREATE2-predicted address devB
  WOULD have on register()) rather than 0x0. The middleware reaches
  vault_not_deployed on its own merits — independent of any future
  re-order of the to_mismatch / verify check.
- T10-R1-F3: vitest.config.ts now sets isolate: true explicitly so the
  forked-e2e NETWORKS mutation can't bleed into other test files even
  if vitest defaults shift; companion comment in forked-e2e explains
  the guarantee.
- T10-R1-F5: forked-e2e node-http happy path now ajv-validates the 402
  body against the vendored x402 v1 ChallengeBody schema (loaded once
  in beforeAll). A missing required field surfaces here instead of
  silently drifting on the wire format.

T10-R1-F4 (Fastify XPR shape) was already addressed in commit 13f25d4
via code-reviewer T10-R1-M2.

253 passing + 2 skipped; lint and typecheck clean. Forked suite runs
in ~10.2 s (vs ~7.6 s before — the extra 2.5 s is the longer paused
wait + the ajv compile + validate).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SEC-T10-01 (medium): add a path allowlist in .gitleaks.toml for
  forked-e2e.test.ts so the well-known public Foundry anvil test
  private keys don't trigger false-positive blocks in contributor
  pre-commit hooks. The keys derive from the public "test test … junk"
  mnemonic and carry no real value — they only ever control accounts
  on a local anvil node. Comment explains the rationale.
- SEC-T10-02 + SEC-T10-03 (low): in arc-testnet-e2e.test.ts, wrap
  PAYWALL_RELAYER_KEY directly into new OpaqueRelayerKey(...) at the
  env read, and pass ARC_TESTNET_PAYER_PK directly into
  privateKeyToAccount(...) without a plain-hex intermediate variable.
  Eliminates the ~50-line window during which the raw hex key would
  have lived on the call stack.

253 passing + 2 skipped; typecheck and lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All three reviewers approved at round 2:
- code-reviewer-t10: APPROVED (covers 13f25d4 + cee0c18)
- security-auditor-t10: APPROVED (SEC-T10-01/02/03 resolved at 9edc669)
- test-reviewer-t10: PASSED (F1/F2/F3/F4/F5 all verified)

decisions.md entry summarizes the implementation, the four
deviations from the initial spec (vitest timeout add, NETWORKS
in-place mutation, computeVaultAddress hardening, 8 s paused
sleep), and three round-2 review reports per reviewer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves 9 minor/medium findings from the Wave 10 holistic audits.

Code (T12):
- T12-01: delete dead `errors.ts` and `errors.test.ts` — `core.ts:build402`
  is the production response builder. Removes the parallel `buildErrorResponse`
  whose `settlementReason` body field diverged from the wire `reason`.
- T12-02: with `errors.ts` deleted, `SettleReason` (settle.ts) is the single
  source of truth. Added documented `SettlementFailedReason` union that
  represents the full wire-observable set (8 reasons including the
  core-emitted `chain_id_mismatch`).
- T12-03: replace `reason: 'internal_error'` (not in any taxonomy) with
  `reason: 'chain_id_mismatch'` at both emit sites in `core.ts`. Matches
  the D18 event name.
- T12-04: rewrite `relayer-key.ts` header docstring to describe the actual
  `WeakMap<OpaqueRelayerKey, string>` implementation (per T6 round-1
  hardening). The class has no `#key` field — the docstring lied.
- T12-05: rename `usdc`/`feeBps` locals to `usdcToken`/`currentFeeBps` in
  `PaymentVaultImpl.withdraw()` so they no longer shadow interface methods.

Security (T13):
- T13-M-MW-01: tighten `scrubSecrets` bare-hex regex. Replace
  `\b[0-9a-fA-F]{64}\b` with `(?<![0-9a-fA-F])[0-9a-fA-F]{64,}(?![0-9a-fA-F])`
  to redact any bare-hex run of >=64 chars. The previous `\b` word boundaries
  did not fire between adjacent hex digits, so concatenated runs (two spliced
  keys, a 65-byte signature without `0x`) passed through unredacted. Adds 4
  fuzz tests covering embedded windows, mixed-case 130-bare-hex, and
  concatenated 64-hex runs.

Tests (T14):
- T14-L1: document why the 8s real-clock sleep in `forked-e2e` paused-request
  test cannot use fake timers (would freeze in-flight anvil + Fastify timers).
- T14-L2: extract `MOCK_USDC_NAME` constant in `forked-e2e.test.ts` so the
  mock-vs-live USDC domain name divergence has a single source of truth.
- T14-L3: comment the forge LCOV instrumentation quirk on
  `_pause()`/`_unpause()` body lines (branch metric is the real CI gate).

Verification:
- npm test --workspace=@universal-paywall/middleware: 208 passed + 2 skipped
- cd contracts && forge test: 52 passed / 0 failed
- npm run lint / typecheck / build: all clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-1 re-audit reports from code-auditor, security-auditor, and
test-auditor on the Wave 10 audit-fix commit (58a1aae).

Verdicts:
- code-auditor: approved (all 5 T12 minors resolved)
- security-auditor: APPROVED / CLEAR FOR FINAL WAVE
  (T13-M-MW-01 resolved; consolidated {64,} quantifier preferred)
- test-auditor: PASS_WITH_NOTES (T14-L1/L2/L3 resolved; 1 low + 1 info
  raised, both non-blocking and deferred to post-MVP)

decisions.md updated with final verdicts and post-MVP open items
(restore per-reason schema iteration on build402; test the
unknown-network chain_id_mismatch branch; optional {64,4096} cap on
HEX_BARE_RUN_RE).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mnemonik-dev
mnemonik-dev merged commit e5aeb12 into main Jun 21, 2026
mnemonik-dev pushed a commit that referenced this pull request Jun 21, 2026
…rget

Navidrome scrobbles natively to any ListenBrainz-compatible server set via
ND_LISTENBRAINZ_BASEURL. Implement the two endpoints its client calls so the
sidecar attaches per-listen royalties with zero changes to Navidrome.

Verified against navidrome/adapters/listenbrainz/client.go + auth_router.go:
- GET  /1/validate-token  (Authorization: Token <t>) -> {valid:true,user_name,code:200}
- POST /1/submit-listens  -> {status:'ok'}; recording_mbid (fallback first
  artist_mbids) -> resolveCreator, the Token -> resolvePayer; playing_now skipped.

- src/listenbrainz.ts: adapter (parseListenToken, listenCreatorKey, handleListenSubmit).
- src/serve.ts: thread request headers into RouteHandler ctx; add listenBrainzRoutes.
- src/cli.ts: PLATFORM=navidrome (multi-route); src/index.ts exports.
- +7 unit tests (24 total in the package; typecheck + build clean).
- deploy/navidrome + deployment-plan/STATUS updated; corrected the stale Mastodon
  campaign schema noted for gap #2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUqG1L5TrBNeScLxTXgzyM
mnemonik-dev pushed a commit that referenced this pull request Jun 21, 2026
Full vertical against a live ghcr.io/navidrome/navidrome container - zero Navidrome
changes, it scrobbles natively to our ListenBrainz target:

  Subsonic scrobble -> Navidrome forwards to ND_LISTENBRAINZ_BASEURL (our sidecar)
  -> POST /1/submit-listens -> recording_mbid -> live MusicBrainz WS/2 resolver
  -> artist -> wallet -> facilitator -> anvil settle -> artist paid 100 micro-USDC

The real Navidrome scrobble payload matched the parser exactly: listen_type=single,
recording_mbid=f1aa509e..., artist_mbids=[4d5447d7...]. This field-verifies gap #1
(ListenBrainz target) and gap #4 (MusicBrainz resolver) together with a real
instance + the real MusicBrainz API.

- scripts/e2e-navidrome-live-docker.mjs: documented real-L3 harness (Docker + WS/2
  egress prerequisites, including the ffmpeg MBID-tagging + container recipe).
- testing-plan / STATUS / deploy/navidrome updated with the result + real payload.
- HANDOFF gotcha: prefer GHCR over Docker Hub (anon pull rate limit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUqG1L5TrBNeScLxTXgzyM
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants