# Audit report

> Review only the changed long-review payment route and finite recovery admission. Read docs/review-credit-routing.patch and docs/review-credit-routing.md; full changed adapters and original immutable credit ledgers are mirrored under integration/payment-review/src. Agent is integration/agent-review/network-review.ts; runtime capacity is src/approvals/limits.mjs. Prior shared-input source 071b7c918b8dd64066441c3e8245e1e2c19b2ae5 was reviewed separately; do not repeat its full audit. Check cross-rail idempotency, no reuse of an unresolved effect, correct native model/input/effort, final response validation, original accounting holds retained, no automatic refill, two-USDC caller/global 500-USDC cap and expiry/bounded scope of temporary operator grants. New reviewCredit is nonstreaming upstream with existing downstream keepalives. Existing IDs always use original stored rail and responses. CreditWork reserves independent maximum risk and does not release historical ambiguous calls. Current Mainnet request stays x402 until terminal. No contract code changes, public inference or customer funds. Report concrete correctness/security findings, with severity and locations; distinguish unchanged code from introduced defects. Publication of source alone does not mean activation. Planned recovery grants allow at most one new request per existing reference project, total two, under a dated daily cap10 and unchanged global lifetime32. No counter resets. Operator approval already exists for the bounded Phase3 delivery; do not request owner reconfirmation.

| | |
|---|---|
| Repository | https://github.com/contractclaus/claus-network |
| Commit | `d4af978eb8d43895f7ae448bc1ea4f1fbef9922f` |
| Job | `5e2ee286-9298-4fa2-a0d0-5c4b387c7dc8` |
| Judged | 2026-10-13 15:00 UTC |
| Findings | 1 medium · 6 low · 2 info |

Four agents audited the code as it is at `d4af978`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Medium: Credit-routed review that ends 'uncertain' has no terminal path; one post-dispatch failure wedges the agent's single network-review slot

`integration/agent-review/network-review.ts:143`

```
   }else if(body.status==='failed'&&['not_signed','not_settled','settled'].includes(body.outcome?.payment)){
```

Introduced by routing fresh network-review IDs to the claus_credit_work ledger (integration/payment-review/src/index.js:67-68, :141, :158); the agent status branches and the stores are unchanged. On the credit_work rail every failure after startCredit (provider timeout at the 600 s providerMs, provider non-2xx, unreadable body, result_fenced, cost overrun) goes through the catch at credit-provider.js:159-168 with submitting=true and writes finishCredit(status:'uncertain', amount:null). finishCredit (credit-work-store.js:92-96) only matches rows in 'reserved'/'submitting', so that row is immutable afterwards; expireCredit (credit-work-store.js:41) keeps 'submitting'/'uncertain' as 'uncertain' and only clears the execution slot. Both GET /api/requests/:id (index.js:174) and POST /api/reconcile/:id (index.js:128) return workResult(row) verbatim for such rows, and the mirrored payer has no operator entry point that resolves a claus_credit_work row. The agent's networkReviewStatus acts only on status 'completed' (line 140), status 'failed' with a known payment label (this line) or a 404 (line 146); status 'uncertain' matches nothing, so the claus_network_reviews row stays 'uncertain' forever. cancelNetworkReview (line 157) only flips 'queued' rows. Because claus_network_review_active (line 38) is a unique partial index over state IN ('queued','running','uncertain'), every later submitNetworkReview for any project raises 23505 and throws network_review_capacity until the agent database is edited by hand. On the former x402 rail the same timeout was resolved by reconcilePayment into failed/not_settled, which line 143 accepts. Retaining the 2 USDC hold is correct and by design; the defect is that the new lane has no bounded terminal state for the agent slot, so a single over-long model call (the exact case the patch addresses) blocks both planned recovery decisions. Minimal fix that keeps the hold: in networkReviewStatus, when body.status==='uncertain' and the payer row reports active===false (past active_until), persist state 'failed' with result {error:'inference_uncertain', outcome} so the slot frees while the payer row and its reservation stay untouched; or add an operator action on the payer that resolves a claus_credit_work 'uncertain' row from a request-bound provider billing record (binding.clientRequestId / provider_request_id) into 'completed' or 'failed'.

**Reproduction**

State: payer env NETWORK_REVIEW_FRESH_RAIL=credit_work, SURPLUS_CREDIT_ENABLED=true, SURPLUS_CREDIT_KEY valid, pool funded; agent review nr_X queued. runNetworkReview: GET /api/admission/network-review:nr_X?maximumMicroUsdc=2000000 -> workAdmission {ready:true}; row claimed 'running'; POST /api/completion with Idempotency-Key network-review:nr_X, X-Max-Quote-Micro-USDC 2000000, X-Inference-Purpose research -> index.js:154-158 deliver(reviewCredit). In creditCompletion quote and balance pass, startCredit succeeds (submitting=true), then paidFetch rejects with TimeoutError when providerSignal (600 s) fires (or the provider answers 503 -> 'provider_http_error'). Catch writes finishCredit(status:'uncertain', amount:null): SQL CASE with NULL cost yields status='uncertain', accounted_cost NULL, active=active_until>now(); relay returns 502 {error:'payment_requires_reconciliation'}. Agent line 116 throws -> line 124 sets state 'uncertain'. 800 s later networkReviewStatus GETs /api/requests/network-review:nr_X: workStatus runs expireCredit (active=false, status stays 'uncertain', released 0 so the hold is kept) and returns {status:'uncertain',response:null,outcome:{rail:'surplus_credit',payment:'unknown',...}}. Expected: a bounded path to 'failed' or 'completed' for the agent row while the payer hold is retained (docs/operations.md:134 'Existing admitted requests continue through status reconciliation'). Actual: neither branch at lines 140/143/146 matches; the row stays 'uncertain' on every poll; POST /api/reconcile/network-review:nr_X returns the identical object; cancelNetworkReview('nr_X') returns the same; submitNetworkReview(anyNewInput) -> INSERT conflicts on claus_network_review_active -> Error('network_review_capacity') indefinitely.

### 2. Low: Agent admits a fresh review on whatever rail the payer answers, so a payer without NETWORK_REVIEW_FRESH_RAIL silently re-runs the x402 path the patch is meant to retire

`integration/agent-review/network-review.ts:104`

```
  if(response.ok){const status=await json(response);admitted=status.id===effectId&&status.ready===true&&status.maximumMicroUsdc==='2000000';}}
```

The admission check is unchanged agent code; the new payer route adds a rail field the agent never reads. The review lane exists on the payer only when NETWORK_REVIEW_FRESH_RAIL==='credit_work' (credit-work-admission.js:5). When that variable is unset, or the agent talks to a payer deployment that predates this change, workCandidate() is false for a network-review ID and /api/admission falls through to the x402 budget branch (index.js:120-121), answering {ready:true, rail:'x402'} whenever the lifetime budget has 2 USDC of headroom. The agent checks only id, ready and maximumMicroUsdc, claims the row 'running' and POSTs; index.js:165 then routes the POST to the x402 completion() handler, producing the 120 s signature versus 600 s response mismatch (retained failure SignatureExpired) that docs/review-credit-routing.md says this change removes. The payer README's statement that missing credit configuration blocks admission rather than falling back to x402 holds only once the flag is set; nothing on the agent side verifies the rail, and the planned recovery decisions and the agent's day slot are consumed by the known-failing path. Activation is correctly an operator decision (publication of source is not activation); the defect is that the agent cannot express or verify that a fresh review must use the prepaid rail. Minimal fix: require status.rail==='surplus_credit' in the admitted predicate (optionally only when an agent setting such as NETWORK_REVIEW_FRESH_RAIL==='credit_work' is set) and defer with available_at=now()+60s otherwise.

**Reproduction**

State: agent at this network-review.ts; payer with SURPLUS_CREDIT_* configured but NETWORK_REVIEW_FRESH_RAIL unset; claus_payment_budget remaining >= 2000000; review nr_X queued for the reference project under its dated allocation. GET /api/admission/network-review:nr_X?maximumMicroUsdc=2000000: index.js:111-117 find no saved, prepaid or work row; workCandidate false; creditCandidate false; budget branch returns {id,maximumMicroUsdc:'2000000',ready:true,rail:'x402'}. Expected: the agent defers because the fresh review is not admitted on the prepaid rail. Actual: line 104 sets admitted=true (rail ignored), the row is claimed 'running', POST /api/completion reaches index.js:165 deliver(completion) on x402, the payer signs a 120 s authorization for a ~600 s model response, the request ends failed/uncertain exactly as before, and one of the two planned recovery decisions plus the agent's daily slot are spent.

### 3. Low: A definitive pre-reservation 503/409 from the payer after a ready admission is treated as 'uncertain' and becomes a counted failed review after 800 s

`integration/agent-review/network-review.ts:116`

```
  if(!response.ok)throw Error('network_review_inference_uncertain');
```

Unchanged agent line, newly and commonly reachable because the review lane shares Builder's single execution slot. workAdmission is evaluated twice: once in the agent's read-only GET /api/admission and again inside POST /api/completion (index.js:156). The credit_work lane has one execution slot (claus_credit_work_one_active, credit-work-store.js:21; reserveCredit's NOT EXISTS at :62) that reviews now share with Builder's independentCredit, plus env gates that can flip between the two calls. When the POST's workAdmission returns ready:false the relay answers a plain JSON 503 {error:'credit_busy'|'credit_exhausted'|..., payment:'not_signed'} (index.js:157) before any reservation exists; when the slot is taken between that check and reserveCredit, creditCompletion answers 409 {error:'request_already_reserved'} (credit-provider.js:67), also with no row in claus_credit_work and no claus_payment_ids claim. The agent maps every non-OK response to 'uncertain' and will not retry the POST; after 800 s networkReviewStatus gets 404 not_found (confirmAbsent fences the ID, index.js:179) and writes state 'failed' {error:'inference_not_reserved'}. The review ID is derived from the proposal hash, so that proposal can never be reviewed again; its network-side decision count (reserved at proposal creation, docs/operations.md:126) and the agent's daily count are consumed without any model call, and during the planned Phase 3 recovery that is one of the two allocated attempts. On x402 the same shape existed only for payment_budget_unavailable; the shared slot makes it reachable whenever Builder is active. Minimal fix: when response.status is 503 or 409 and the JSON body carries payment==='not_signed' or error==='request_already_reserved' (no streamed envelope, nothing reserved), set state back to 'queued' with available_at=now()+60s instead of 'uncertain'; keep 'uncertain' for timeouts, 502 envelopes and streamed errors.

**Reproduction**

State: NETWORK_REVIEW_FRESH_RAIL=credit_work, SURPLUS_CREDIT_SCOPE=builder, BUILDER_FRESH_RAIL=credit_work, SURPLUS_CREDIT_ENABLED=true, pool funded, budget remaining >= 4000000. t0: agent GET /api/admission/network-review:nr_X?maximumMicroUsdc=2000000 -> {ready:true} (no active claus_credit_work row). t0+1s: Builder POST /api/completion Idempotency-Key build_<24hex>:model:1 -> reserveCredit inserts an active row. t0+2s: agent POST /api/completion Idempotency-Key network-review:nr_X -> index.js:156 workAdmission sees state.busy -> HTTP 503 {error:'credit_busy',payment:'not_signed'}; no row in claus_inference, claus_credit_requests or claus_credit_work. Expected: the review returns to 'queued' with available_at=now()+60s, as the agent already does when admission is not ready, because the payer stated nothing was signed or reserved. Actual: line 116 throws, line 124 sets state 'uncertain' and holds the single active slot; at t0+800s GET /api/requests/network-review:nr_X -> legacy absent -> confirmAbsent -> 404 {error:'not_found'} -> state 'failed' {error:'inference_not_reserved'}; a later POST with the same Idempotency-Key fails in reserveCredit's claimed CTE on claus_payment_ids, so nr_X is unrecoverable and the proposal's decision attempt is spent.

### 4. Low: Review admission omits the quote-ceiling check the POST applies, so a MAX_INFERENCE_QUOTE_MICRO_USDC below 2 USDC admits every review and then refuses it

`integration/payment-review/src/credit-work-admission.js:14`

```
 if(maximumMicroUsdc!=='2000000')return waiting('credit_ceiling_mismatch');
```

Boundary gap between the admission predicate (partly introduced: reviewCreditCandidate) and the unchanged handler predicate. workAdmission validates key, pool binding, lifetime budget, the busy slot and that the caller ceiling is exactly 2000000, but never reads env.MAX_INFERENCE_QUOTE_MICRO_USDC, which configured() (index.js:38-39) accepts anywhere in 1..3000000. createCreditCompletion computes budget=requestBudget(request, min(configured,3000000), header) (credit-provider.js:40-42) and then refuses with 503 credit_exposure_exceeds_request when the fixed work exposure (pool.remaining is always '2000000', credit-work-store.js:29) exceeds callerMaximum (credit-provider.js:60), or with 400 invalid_request_budget if requestBudget rejects the header. Both refusals happen before reserveCredit, so no ledger row exists. For the review lane an admission answer is the trigger for a counted attempt on both sides (agent row claimed 'running'; network decision reserved at proposal creation), and reviews, unlike Builder calls, have no retry identity. With the ceiling configured below 2000000 every review is admitted by GET /api/admission and refused by POST /api/completion, then follows the 'uncertain' -> 404 -> failed/inference_not_reserved path described in the previous finding, deterministically, for every proposal. The admission endpoint exists to prevent exactly this class of pre-reservation refusal (compare credit_ceiling_mismatch, credit_exhausted). Minimal fix: after this line return waiting('credit_quote_ceiling_unavailable') unless /^\d{1,7}$/.test(env.MAX_INFERENCE_QUOTE_MICRO_USDC??'') and BigInt(env.MAX_INFERENCE_QUOTE_MICRO_USDC)>=2000000n, mirroring the handler. The vendor requestBudget is not mirrored; both of its possible outcomes (throw or cap at the ceiling) lead to the same pre-reservation refusal.

**Reproduction**

State: payer env MAX_INFERENCE_QUOTE_MICRO_USDC='1500000' (passes configured()), NETWORK_REVIEW_FRESH_RAIL=credit_work, SURPLUS_CREDIT_ENABLED=true, SURPLUS_CREDIT_KEY valid, pool funded, budget remaining >= 2000000, no active work row. GET /api/admission/network-review:nr_<64hex>?maximumMicroUsdc=2000000 -> workAdmission passes every check including this line -> {ready:true, rail:'surplus_credit', maximumMicroUsdc:'2000000'}; the agent claims the row 'running'. POST /api/completion with the same ID, X-Max-Quote-Micro-USDC: 2000000, X-Inference-Purpose: research -> workAdmission ready -> deliver(reviewCredit) -> configured=1500000n, requestBudget(..., 1500000n, '2000000') either throws (400 invalid_request_budget) or yields callerMaximum=1500000n; maximum=2000000n > callerMaximum -> 503 {error:'credit_exposure_exceeds_request',payment:'not_signed'} before reserveCredit. Expected: admission answers ready:false with a reason so the agent keeps the review 'queued' with retryAt. Actual: admitted then refused; agent state 'uncertain'; after 800 s GET /api/requests/... -> 404 not_found -> state 'failed' {error:'inference_not_reserved'}; the same happens to every subsequent review while the setting is below 2000000.

### 5. Low: Agent-side recovery limit raises the daily cap for every project on the recovery day, while the routing note says other projects keep the ordinary ceiling

`integration/agent-review/network-review.ts:65`

```
    WHERE (SELECT count(*) FROM claus_network_reviews WHERE created_at>=date_trunc('day',now() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC')<${networkReviewDailyLimit()}
```

Introduced code. docs/review-credit-routing.md:5 states the recovery is project-specific ('Other projects keep the ordinary global-day ceiling') and calls NETWORK_REVIEW_RECOVERY_DAY/NETWORK_REVIEW_RECOVERY_LIMIT the agent's 'matching' settings. The network side is project-bound (src/approvals/limits.mjs:17 allocation.projectId===projectId), but networkReviewDailyLimit() (lines 48-52) has no project term and is compared here against count(*) of all claus_network_reviews rows created today, whatever their input.projectId. Public projects are still stopped at the network by the unchanged global-day counter, so for them the agent raise is harmless. Pilot (non-public) projects, however, never reach the public counters (limits.mjs:8 returns false before any count) and are bounded only by this agent cap; on the recovery day they can consume the 9th..16th agent slots the dated raise was issued for, and the allocated project's planned request then fails with network_review_capacity until the allocation expires. The raise is correctly bounded in time (one UTC day) and value (8..16) and each extra review stays under the 2 USDC per-call and lifetime payer caps, so money caps are unaffected; the defect is scope: the documented project binding does not exist on the agent side, and the planned two recovery requests can be starved. Fix: either bind the agent raise to the allocated project IDs (e.g. NETWORK_REVIEW_RECOVERY_PROJECTS compared with input.projectId, 8 for all others) or correct review-credit-routing.md to state that the agent raise is global for that day and the network allocation is the only project-bound gate.

**Reproduction**

State: agent env NETWORK_REVIEW_RECOVERY_DAY=2026-10-14, NETWORK_REVIEW_RECOVERY_LIMIT=10; on 2026-10-14 UTC eight claus_network_reviews rows already exist from other projects (count=8). networkReviewDailyLimit() evaluated with that env and now=2026-10-14T12:00Z returns 10 (verified by evaluating the function body with node). A pilot project (review-pilots enabled, not in public-chain-projects, so publicDecisionCapacity returns false without reserving) submits a proposal; submitNetworkReview: count=8 < 10 -> row inserted. A second pilot proposal: count=9 < 10 -> inserted. The reference project's planned recovery request, admitted on the network under {maximumGlobalDay:10}, reaches submitNetworkReview: count=10 < 10 is false -> no row -> Error('network_review_capacity'). Expected per review-credit-routing.md: on 2026-10-14 non-allocated projects stop at 8 and only the allocated project may use slots 9 and 10. Actual: any project fills the raised cap at the agent.

### 6. Low: Published agent source binding still records the pre-change network-review.ts hash while claiming the mirror matches the verified deployment

`docs/agent-review-source.json:4`

```
    "integration/agent-review/network-review.ts": "068d26646ce48313bd82d6586b7051115c8c5a1fa3a6a4ba5d6c79733562d8b4",
```

Introduced inconsistency. Commit e5df7c2 published this manifest with scope 'These three agent source mirrors match the verified deployed agent' for deployment dpl_HNEkqeYJ63VQUoZZQ3i9bcSaXzRi at sourceCommit 071b7c9. Commit d4af978 then changed integration/agent-review/network-review.ts (networkReviewDailyLimit and the parameterised cap at line 65) without updating the manifest, so the recorded value is the hash of the previous mirror; the schema and signal mirrors still match. docs/operations.md:150 calls this file 'the exact review-source candidate hashes' and integration/agent-review/README.md says the deployment verifies these hashes against the agent release. A reader therefore cannot tell whether the bound deployment enforces the fixed 8-per-day cap or the dated 8..16 raise that the planned Phase 3 delivery (dated ceiling 10) relies on; if the deployed agent is still the 071b7c9 build, a recovery submission beyond eight agent reviews that day fails with network_review_capacity after the network has already reserved the project's decision. Publication of source does not mean activation; the defect is the stale verification claim, not activation itself. Fix: after the agent release containing networkReviewDailyLimit is verified, update sha256, sourceCommit, deployment and verifiedAt together; until then mark network-review.ts as a candidate ahead of the bound deployment and state in review-credit-routing.md that the agent-side recovery settings are not active in the bound deployment.

**Reproduction**

Run: sha256sum integration/agent-review/network-review.ts at HEAD -> abbe3192a0ea5e8caedf4b49ded8e0086c79cf7b0edf88181a0e3838920fd606. Run: git show 071b7c9:integration/agent-review/network-review.ts | sha256sum and git show HEAD~1:integration/agent-review/network-review.ts | sha256sum -> both 068d26646ce48313bd82d6586b7051115c8c5a1fa3a6a4ba5d6c79733562d8b4, the value on this line. The other two recorded hashes match their files at HEAD. Expected: the recorded hash equals the mirrored file, or the record states that the mirror is ahead of the verified deployment. Actual: the record asserts a match for a file whose content differs.

### 7. Low: operations.md still states the eight-attempt global daily cap always applies, contradicting the new maximumGlobalDay allocation field

`docs/operations.md:128`

```
The eight-attempt global daily cap, 32-attempt global lifetime cap, separate model budget and 2-USDC request ceiling still apply.
```

Introduced inconsistency. src/approvals/limits.mjs:15-30 now lets a per-project allocation carry maximumGlobalDay (8..16) that replaces the global-day ceiling for that project's checks on that day. The runbook section 'Bounded decision recovery allocation' is the only place the public-decision-allocations record shape is documented for operators; it lists maximumProjectDay and maximumProjectLifetime, says the eight-attempt global daily cap 'still appl[ies]' and never mentions maximumGlobalDay. Only docs/review-credit-routing.md:5 mentions the field. An operator following operations.md will neither know the field exists nor that an allocation can admit a 9th..16th global decision on the day, and the planned Phase 3 delivery (dated ceiling 10) depends on writing it. The code behaviour is covered by test/public-decision-admission.test.mjs and matches review-credit-routing.md; the runbook is what is wrong. Fix: document maximumGlobalDay (optional integer, 8 through 16, invalid values invalidate the allocation, expires with the allocation, project-bound) in this section and qualify the sentence about the eight-attempt cap.

**Reproduction**

State: day D with public-decision-admission {scope:'day',subject:'global',period:D}.count = 8. Write public-decision-allocations[digest({projectId:'recovery',day:D})] = {projectId:'recovery',day:D,expiresAt:now+10000,maximumProjectDay:2,maximumGlobalDay:10}. Call publicDecisionCapacity(store,'recovery',now,true). Per this line the expected result is decision_capacity_reached. Actual: the call succeeds and the global day counter becomes 9; this is exactly what the test 'global recovery headroom is project-bound, finite and never resets historical counters' asserts, and `node --test test/public-decision-admission.test.mjs` passes all 7 tests at HEAD.

### 8. Info: NETWORK_REVIEW_RECOVERY_LIMIT accepts hexadecimal, binary, exponent and padded encodings instead of treating them as invalid

`integration/agent-review/network-review.ts:49`

```
 const n=Number(env.NETWORK_REVIEW_RECOVERY_LIMIT);
```

Introduced code. docs/review-credit-routing.md:5 says the agent settings accept '8 through 16, otherwise 8' and that invalid values invalidate the allocation, mirroring the network side where any non-canonical value (for example the string '10', test case {maximumGlobalDay:'10'}) disables the whole allocation. The agent uses Number(), which parses '0x10' as 16, '0b1010' as 10, '1e1' as 10, ' 12 ' as 12 and '16.0' as 16. The result is always clamped to 8..16 and to the configured UTC day, so the bound itself holds; the deviation is only that a mistyped or non-decimal value is silently accepted as a grant rather than rejected. Fix: require /^(8|9|1[0-6])$/.test(env.NETWORK_REVIEW_RECOVERY_LIMIT??'') before Number().

**Reproduction**

Evaluated the function body with node: env NETWORK_REVIEW_RECOVERY_DAY='2026-10-13', now=2026-10-13T12:00:00Z; NETWORK_REVIEW_RECOVERY_LIMIT='0x10' -> 16, '0b1010' -> 10, '1e1' -> 10, ' 12 ' -> 12, '16.0' -> 16, while '17', '7', 'abc' and '' -> 8 and any value on another day -> 8. Expected per the documented rule: 8 for every non-canonical encoding. Actual: submitNetworkReview admits up to 16 rows that day with '0x10'.

### 9. Info: Admission 'waiting' responses label review candidates as rail x402 when the relay is disabled or the lookup fails

`integration/payment-review/src/index.js:108`

```
    const waiting=reason=>({id,maximumMicroUsdc,ready:false,rail:creditCandidate(env,id)?'surplus_credit':'x402',reason,retryAfterSeconds:60});
```

Unchanged helper, now reached by a new class of IDs. The waiting() label only consults creditCandidate (original ledger pilot), not workCandidate, so for a network-review:nr_... ID with NETWORK_REVIEW_FRESH_RAIL=credit_work the payment_unavailable (line 109) and admission_unavailable (line 122) responses advertise rail 'x402' although such an ID can only ever be admitted on 'surplus_credit' (workAdmission's own waiting() at credit-work-admission.js:8 correctly says surplus_credit). The agent ignores the rail field, so there is no behavioural impact; operators reading admission output get a misleading rail, and the inconsistency would mask the rail assertion proposed for the agent. Fix: use creditCandidate(env,id)||workCandidate(env,id) in the label.

**Reproduction**

Env: NETWORK_REVIEW_FRESH_RAIL=credit_work, PAYMENT_RELAY_ENABLED unset (enabled() false). GET /api/admission/network-review:nr_<64hex>?maximumMicroUsdc=2000000 -> line 109 returns waiting('payment_unavailable'). Expected: {ready:false,rail:'surplus_credit',reason:'payment_unavailable'}. Actual: {ready:false,rail:'x402',reason:'payment_unavailable'}.

---

Judge's submission `180365c79d795f7db477cce0a02fdbf845cfa3f06185fcc01af27413e70a028f`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
