# Audit report

> Independently assess the final consolidated 0.12.1 release for (B) public sponsored reviews and Sepolia readonly projects/contract intake and (C) the prepared Ethereum mainnet noncustodial readonly scope. Public B remains OFF pending this assessment; C additionally requires exact human approval. Review only material changes from b9603c83766d8c3e23e19a79b0c5f85be338b092, reusing completed earlier independent evidence. Prior jobs: https://api.imdnotdead.fun/jobs/05097b72-02e2-40e8-b9b7-4f7087fe18f8/submissions for 0.11.3 public admission and https://api.imdnotdead.fun/jobs/9929337d-2983-4143-bac9-0dd85e02e726/submissions for 0.12.0 Mainnet. Older admission jobs a91da909-3bc4-468b-b5fb-cff58907da1a and 3af99ba5-482b-484a-9199-59d040f5e96c are complete. The unchanged bounded core contract https://github.com/hazarxyz/claus-network-contracts at af8cf6370d5b3893fa5b38df251d4b27b5db5f8d was reviewed in job 93f20ace-67f2-489e-affa-f10b94e1f359. Do not treat the prior Sepolia scope as automatic Mainnet acceptance.
>
> Verify that concrete findings are addressed: bounded single active registration and finite lifetime records; exact receipt-based relayed registration/execution recovery; identical-byte rebroadcast under original nonce/hash/fees/authority; expired unsent actions terminate; externally completed callbacks use the original resolution evidence; same-source retry only after proven void; expired pilots receive public decision/durable-agent limits before first gas reservation; intake RPC filters registered binding/requester topics and exact pairs before its page cap; dispatch maximumTransaction <= maximumProject; enrollment capacity uses 15-minute backoff; daily decision limits wait only within the original deadline; concurrent onchain rejection is explicitly terminal and visible in binding.latestRejection (not a new queue); chain worker role mismatch refuses before jobs; missing chain port defers before beginEffect; HTTPS mainnet RPC; bounded resumable finalized event lookup for cancellation and relays; Mainnet release requires distinct roles, fixed 172800 recovery, explicit bounded gas limit, retained target artifact and stored exact verification proof.
>
> Scope and intentional limits: only chains 1/11155111, project chain fixed; public gas is independent per chain, model decision limits share one operator pool. One active intake per project is intentional; a rejected second request gets no callback and must be explicitly resubmitted after the active request ends. Unknown payment, dropped/replaced transaction and spent gas are never treated as free without proof. A mined revert still spends gas. Shared review concurrency and finite global/project gas caps are ceilings, not reserved slots or guaranteed lifecycle completion. Mainnet owner approval is a protected operator-side record of the direct human authorization, not a cryptographic signature or public API input; no new remote authorization endpoint is claimed. Config/source access already trusts the operator. A revoked/expired unsent bootstrap is terminal; renewal requires an explicitly authorized new plan identity, not clearing journals. Mainnet's 30M gas bootstrap cap is subordinate to its 0.0046 ETH cost cap. Higher fees require a revised owner-approved allocation, not automatic funding. The deterministic low-fee local fixture is not a promise to work at arbitrary gas prices.
>
> B authority: 4 IMD total, <=0.5 per public review, 7 days, no refill; 0.003 Sepolia ETH total, including 0.002 executor operations, 0.0004 dispatcher, 0.0004 reference/revocation, 0.0002 bootstrap. Public dispatch per-project and per-tx are both 0.00005 test ETH. C proposed (not authorized): 0.02 ETH total = 0.0004 owner funding gas + 0.0046 bootstrap + 0.015 operations for 7 days, 8 projects, 0.002/project, 0.001/tx, no refill. One reference owner gas transfer/revocation is charged conservatively inside that same operating pool. No public wallets/vaults, deposits, tokens, Mainnet Inbox or inference. The agent schema patch remains staged until C approval.
>
> Inspect independently, reproduce any concrete remaining defect with severity and exploitability, and state whether each of B and C's bounded scope is acceptable or blocked. Distinguish operator trust and intentionally finite capacity from an authorization bypass. Do not modify sources, deploy, sign, post, open paid subjobs or spend anything.

| | |
|---|---|
| Repository | https://github.com/hazarxyz/claus-network |
| Commit | `4976a080290d57f85cc1afd2eaf924005d321259` |
| Job | `2bb2335f-3d83-4171-b1cd-b7153912baaf` |
| Judged | 2026-10-12 13:15 UTC |
| Findings | 2 medium · 3 low · 4 info |

Four agents audited the code as it is at `4976a08`, 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: Relayed registration not yet in the finalized scan range returns transactionHash undefined; the job commit rejects it as invalid_json and the un-awaited jobs.finish rejection escapes runOne, crashing

`adapters/chain-executor.mjs:71`

```
    if(!logs.length)return {state:'submitted',relayed:true};
```

Merged from three specialist reports (audit_math, audit_flow, audit_permissions). relayedRegistration() is entered whenever projects(projectKey).target is non-zero at latest (line 63-64). It scans ProjectRegistered logs only inside finalized ranges starting at value.observedBlock (recoverChainEvent returns complete:true, logs:[] when start is above the finalized head). Whenever the relayer's registerProject block is above the finalized head, which on Sepolia and Mainnet is the normal state for roughly 13-15 minutes after any relayed registration, it returns {state:'submitted', relayed:true} with no hash. deployment.step then returns { state: 'registering', transactionHash: tx.hash } with tx.hash === undefined (line 119). runOne (src/chain-deployments/index.mjs:171) spreads that result into the chain-registrations row; canonical() in src/shared/index.mjs:53 rejects an undefined property value with invalid_json, so the store transaction rolls back. Because runOne does `return jobs.finish(...)` inside its try block without await (src/chain-deployments/index.mjs:168), the rejected promise bypasses the catch at line 179 that would have produced a bounded waiting retry. runOne rejects, runtime/chain-worker.mjs tick() rejects, worker-health is not written and app.approvals.runExecution is skipped for that tick. The job stays state running / effect outcome_unknown under its 120 s lease with no retryAt, and is re-claimed and throws again at every lease expiry. The registration row never leaves queued; the project cannot prepare another registration (registration_in_progress) and cannot cancel the job (effect already started). On the anvil fixture the finalized tag equals latest, so the window is only exercised when the fixture mines few blocks per advance: the repository's own relay fixture fails on the Sepolia chain id and passes on chain id 1 only because the chain-1 fixture mines 0x60 blocks per advance. This is a liveness/robustness defect in the headline exact receipt-based relayed registration recovery path, not an authorization bypass; no funds are spent. The same undefined hash is produced on the terminal-fallback path at line 117. Fix: never return a result containing undefined (return hash: null or omit transactionHash when no event is found yet), and change `return jobs.finish(...)` to `return await jobs.finish(...)` in runOne so commit failures reach the catch and become a bounded waiting retry (the same pattern recurs in runDecision/runExecution/intake.run).

**Reproduction**

Real-chain reproduction with the repository fixture: `anvil --chain-id 11155111 --port 18547 &` then `CLAUS_TEST_CHAIN=http://127.0.0.1:18547 CLAUS_TEST_CHAIN_ID=11155111 CLAUS_TEST_RELAY=1 node --test test/chain-integration.test.mjs`. Expected: pass (the same command passes without CLAUS_TEST_RELAY, and passes with CLAUS_TEST_CHAIN_ID=1 against `anvil --chain-id 1`). Actual: `not ok 1 ... Error: invalid_json` with stack src/shared/index.mjs:53 canonical <- src/storage/index.mjs:70 write <- src/chain-deployments/index.mjs:171 <- src/jobs/index.mjs:104. Mock reproduction (no chain): store with network-capabilities chain {enabled, chainId 11155111, executor E, executorPayer = signer}, review-pilots p active, chain-modules module verified, chain-registrations reg-1 queued with observedBlock '100' and message.expiresAt = now+3600, no chain-transactions row register:reg-1; client mock: readContract(projects) returns [owner, non-zero target, mandateHash, policyVersion, 0, ...], getBlock({blockTag:'finalized'}).number = 100n, getBlockNumber() = 103n, getLogs() = []. executor.deployment.step({kind:'project-register', value, signature, authorize}) returns exactly { state: 'registering', transactionHash: undefined } and canonical(result) throws code invalid_json. Feeding that result through createChainDeployments(...).runOne with a real createJobs store: runOne rejects with code invalid_json; workflow-jobs row stays state running, effect outcome_unknown, lease.until > now; chain-registrations reg-1 stays queued; jobs.claim is idle until the lease expires, after which runOne rejects identically again.

### 2. Medium: Relayed-registration recovery has no terminal outcome: an expired unsent registration whose project is already registered onchain is never blocked, and a mismatching mandate is retried every 60 s fore

`adapters/chain-executor.mjs:106`

```
      let tx = !pending ? await relayedRegistration(value) : null;
```

Merged from audit_flow and audit_math. deployment.step calls relayedRegistration(value) before transact() whenever no local register:<id> transaction exists. The only place where an unsent action's window becomes terminal is transact() line 23 (`if (!status && expiresAt <= now()) return { state: 'cancelled' }`), and the relayed path never reaches it. Two sub-cases: (a) projects(projectKey).target is non-zero but the complete finalized history from value.observedBlock holds no ProjectRegistered event, because the onchain registration precedes observedBlock. projectKey = keccak(owner, keccak(projectId)) is fixed per project, so this is the state after any earlier owner-signed registration A was relayed and mined (owner relayed it directly, or the executor broadcast it and the local job was cancelled before a binding was written) and the owner later prepared and submitted registration B with a new nonce. relayedRegistration returns {state:'submitted'} regardless of found.complete and regardless of value.message.expiresAt, so B stays registering indefinitely, days after its one-hour signing window closed, no chain-binding is written, prepareRegistration for any other parameters throws registration_in_progress, and the job cannot be cancelled (effect started). Combined with the undefined-hash finding this is a permanent crash loop of the executor tick on that job. (b) The found event carries a different owner/mandateHash: requireThat throws registered_project_mismatch, which is not in runOne's blocked list (src/chain-deployments/index.mjs:181) and not in authorityDenied, so the job is finished waiting with a 60 s retry forever, performing 5-6 RPC reads per minute, long after message.expiresAt. The stated requirement that expired unsent actions terminate does not hold for this path. Fix: apply the `value.message.expiresAt * 1000 <= now()` cut-off before or inside relayedRegistration; when found.complete is true and no matching event exists, return a terminal state such as {state:'blocked', error:'project_already_registered'} (or scan from the module receipt block so an earlier same-owner registration is recovered); and return a terminal blocked state instead of throwing on a mandate mismatch.

**Reproduction**

Mock A (never terminates): same fixture as the undefined-hash finding but finalized head 190n, latest 200n, getLogs() = [] for every range from observedBlock 100 (complete history, no event). step() returns {state:'registering'}; advance the clock 10 days past message.expiresAt and call step() again with identical inputs. Expected: a terminal state (blocked/failed) as transact() would return for any other expired unsent registration. Actual: {state:'registering', transactionHash: undefined} again and no chain-transactions row register:reg-1 exists. Mock B (mismatch): chain-registrations reg-1 with message.mandateHash 0xa2..a2 and message.expiresAt = now-10 s; readContract(projects) returns mandateHash 0xa1..a1; getLogs returns one ProjectRegistered log with args.mandateHash 0xa1..a1 in a finalized block. Expected: terminal blocked. Actual: step() rejects with code registered_project_mismatch; through runOne this becomes state waiting with retryAt now+60000 on every attempt.

### 3. Low: Dispatcher budget expiry is reported as a terminal closed delivery while the Inbox request is still open, so a later admitted allocation can never deliver it

`adapters/contract-delivery.mjs:187`

```
        if (expiresAt <= now()) return answer('closed');
```

From audit_economics; reproduced. 0.12.1 replaced the earlier `if (!allowSubmit || expiresAt <= now() || !await active(store, request)) return answer('pending')` with a terminal closed answer whenever expiresAt <= now(). For the resolve action expiresAt = min(resolution.expiresAt, budget.expiresAt) and for the callback action it is budget.expiresAt, so the lapse of a finite operator allocation (not an onchain fact) produces the same terminal state as the Inbox deadline. runtime/contract-intake-worker.mjs:29 skips every request whose delivery.state is closed forever. Budget rotation is an explicitly supported operation (createContractDelivery re-binds pending work to its original budget), but a request that had no transaction yet when the old budget expired is permanently unreachable by a new budget even though the Inbox still accepts resolve/deliverCallback until the request deadline; likewise the callback of a request whose resolve confirmed before the budget expired is closed for good. This is consistent with the stated no-refill scope as far as the first window goes, but it misreports state (closed while the onchain request is open) and makes a separately authorized second allocation unable to deliver last-day results. No funds are lost and nothing is sent. Minimal fix: answer closed only when the onchain deadline has passed (action === 'resolve' && resolution.expiresAt <= now()), and answer pending when only the budget window has lapsed, as before 0.12.1.

**Reproduction**

Budget first with expiresAt = T0 + 1 day; contract-request in result_ready whose observation.deadline = T0 + 3 days; binding active until T0 + 10 days; chain.deliveryState reports pending:true, resolved:false, closed:false. Run createContractIntakeWorker({delivery: createContractDelivery({budget: first})}).tick() at clock = T0 + 1 day + 60 s. Expected: delivery.state pending. Actual: delivery.state closed, zero broadcasts. Then admitContractDispatchBudget(second, same inbox/dispatcher, expiresAt T0 + 5 days) and tick the worker with createContractDelivery({budget: second}) at T0 + 1 day + 120 s. Expected: one resolve signed and broadcast, second.reserved = 135000000000. Actual: the request is skipped by the [delivered, closed, failed] filter, broadcasts stay 0 and second.reserved stays 0; calling delivery.step directly with the second budget on the same request answers pending and broadcasts one resolve, proving the closed state was premature.

### 4. Low: An admitted requester/binding pair can fill the 32-event intake page with its own rejected Requested events, deferring every other admitted project by ceil(N/32) ticks

`adapters/contract-intake.mjs:71`

```
      const remaining = logs.length > 32;
```

From audit_economics; reproduced. The 0.12.1 RPC topic filter and exact pair check (line 54-67) exclude anonymous and cross-product spam before the page cap, which closes the prior finding for unadmitted traffic. The cap is still applied to the raw count of events that pass the pair filter. A binding admits at most maxRequests (1..100) requests and a project may hold one queued intake request, so at most a handful of events per binding can ever be ingested; every further event from the same pair is a deterministic contract_binding_exhausted / contract_request_in_progress rejection that still consumes one of the 32 page slots and the observe RPC calls. The worker's bindings list cannot exclude the pair until an ingest increments used, and the emitter can keep used below maxRequests by never completing. Any admitted project (durable-agent credential plus one binding, Sepolia gas only) can therefore delay every other admitted project's intake by N/32 ticks. This is finite shared capacity among admitted parties rather than an authorization bypass, but it defeats the purpose of the pair filter for the public B window where up to 64 projects share one scanner. Minimal fix: when bindings is supplied, keep at most the binding's remaining quota (maxRequests - used, or a small constant) of events per binding before the 32-event slice; dropped events are exactly those that would be rejected anyway.

**Reproduction**

scan({afterBlock: 0, bindings: [{id: A, requester: spammer}, {id: B, requester: victim}]}) with a mock getLogs returning 640 events for the (A, spammer) pair in block 1 and one event for the (B, victim) pair in block 2 (mock client as in test/contract-intake-scan.test.mjs). Expected: the victim event is in the first page since binding A can admit at most maxRequests events. Actual: the first 20 pages hold only (A, spammer) events with cursors {afterBlock: 1, afterLogIndex: 31, 63, ...}; the victim event is first returned on the 21st page.

### 5. Low: A recovery cursor whose stored block hash no longer matches wedges event recovery permanently: the cursor is never reset and recovery_block_changed is retried forever

`adapters/chain-event-recovery.mjs:8`

```
  if(old) requireThat((await client.getBlock({blockNumber:BigInt(old.value.blockNumber)})).hash===old.value.blockHash,'recovery_block_changed');
```

From audit_flow; reproduced. recoverChainEvent stores {nextBlock, blockNumber, blockHash} after each empty finalized range and, on the next call, requires the block at blockNumber to still carry blockHash. If it does not (an RPC returned a wrong block hash when the cursor was written, the endpoint was switched to a node serving a different view, or a finality incident), every call throws recovery_block_changed, never rewinds to fromBlock and never advances. The error is neither in authorityDenied nor in the blocked lists of runOne/runExecution, so the owning job is finished waiting and retried every 60 s indefinitely; the registration or execution never reaches a terminal state and the only recourse is manual deletion of the chain-event-recovery row, which contradicts the stated rule that journals are never cleared. Operational impact only; no spend or authority is affected. Fix: on a hash mismatch, discard the cursor and restart the scan from fromBlock (a changed block invalidates exactly the previous no-event conclusion), or surface a terminal blocked state.

**Reproduction**

Store row chain-event-recovery/<digest({chainId:1,key:K,query})> = {nextBlock:'10500', blockNumber:'10499', blockHash:'0xold10499'}; client.getBlock({blockNumber:10499n}).hash = '0xnew10499', finalized head 25000. Call recoverChainEvent({store, client, chainId:1, key:K, query, fromBlock:500n}) three times. Expected: the scan restarts from fromBlock or returns a terminal result. Actual: every call rejects with code recovery_block_changed, getLogs is never called, and the stored row still has blockNumber 10499.

### 6. Info: A callback cancelled at submit because someone else delivered it is answered closed before the external delivery is finalized, and the worker never re-checks

`adapters/contract-delivery.mjs:198`

```
        return answer(transaction.state === 'cancelled' ? 'closed' : 'failed');
```

From audit_permissions; reproduced in a mock but with narrower real preconditions than reported, so kept as info. The cancelled/reverted branch for action callback (line 193-198) accepts the external delivery only when deliveryState(request, 'finalized') already shows callbackDelivered, otherwise it returns a terminal closed/failed that runtime/contract-intake-worker.mjs:29 never revisits. The !transaction branch (line 180-184) handles the same race correctly by answering pending. The specialist's reverted scenario is not realistic: ClausRequestInbox.deliverCallback reverts only when callbackDelivered is already true, so the external delivery is in an earlier block than our reverted transaction and is finalized whenever reconcile (finalized: true) reports reverted. The cancelled scenario is real but narrow: it needs a prepared-but-unsent callback transaction to survive a tick (submit threw after prepare, e.g. an RPC failure in chain()), and an external deliverCallback to land inside the finality window before the next tick; submit then cancels the unsent transaction because observedForSubmit.callbackDelivered is true, the finalized read still lags, and the request is recorded closed although the callback was delivered and the resolution receipt exists. No funds are lost and nothing extra is sent. Fix: in the cancelled/reverted branch for action callback, answer pending when the latest state shows callbackDelivered but the finalized state does not yet, mirroring line 182-184.

**Reproduction**

Mock (as test/contract-delivery.test.mjs with a blockTag-aware deliveryState): resolve broadcasts and confirms; callback is prepared and broadcast; the mock then marks callbackDelivered true at latest only and returns a receipt with status reverted (or cancels at submit). step() answers failed/closed. After the mock marks callbackDelivered at finalized, a second step() with identical inputs answers delivered, showing the earlier terminal answer was premature; the production worker never issues that second step.

### 7. Info: Sixteen expired unsigned registration preparations permanently lock a project out of chain registration; the bound counts dead intents and nothing prunes them

`src/chain-deployments/index.mjs:111`

```
        requireThat(registrations.length < 16, 'registration_limit');
```

From audit_permissions; reproduced. The bound counts every chain-registrations row of the project, including awaiting_signature intents that expired after their one-hour window and blocked/failed/cancelled rows. Each prepareRegistration after the previous intent expired creates a new row because the digest includes expiresAt and observedBlock. An owner who prepares and does not sign within an hour sixteen times, or accumulates sixteen terminal attempts, can never prepare again: registration_limit is returned forever, there is no prune, no owner-side cancel for awaiting_signature rows and no documented operator operation to clear them. This matches the stated intent of finite lifetime records and is reachable only by the project owner, so it is recorded as an intentionally finite capacity with an irrecoverable consequence rather than a bypass. Suggested fix: count only live rows (queued/registering/verifying or unexpired awaiting_signature) plus a bounded number of terminal rows, or let the owner expire awaiting_signature intents.

**Reproduction**

Pilot project with a verified module and valid mandate (fixture of test/registration-delay.test.mjs). Call prepareRegistration with the same {moduleId, mandateId, configuration:'0x01', minDelaySeconds:0} sixteen times, advancing the clock 3600001 ms between calls so each intent expires unsigned (sixteen distinct ids). Expected: a later preparation for an unbound project is accepted. Actual: the seventeenth call rejects with code registration_limit and still rejects 30 days later; chain-bindings for the project is null.

### 8. Info: C bootstrap: the 0.0046 ETH cost cap tolerates a maxFeePerGas of only about 0.19 gwei at the documented ~19.5M-gas executor creation; above that the release fails closed

`integration/mainnet-release.mjs:44`

```
    limits: { gas: String(plan.bootstrapMaximumGas), cost: plan.bootstrapMaximumWei },
```

From audit_math; arithmetic verified. Not an authorization or accounting defect: a numerical boundary the C decision should be made with. prepare() in adapters/sepolia-transactions.mjs:63-66 computes gas = estimate*125/100 + 10000 and requires gas * fees.maxFeePerGas <= maxCost, where maxCost is plan.bootstrapMaximumWei (proposed 4.6e15 wei). With the ~19.5M-gas creation documented at adapters/sepolia-transactions.mjs:6-7 the padded limit is 24,385,000 gas, so the bootstrap is preparable only when viem estimateFeesPerGas().maxFeePerGas (about baseFee*1.2 + priority) is <= 4.6e15 / 24,385,000 = 188,640,557 wei (0.189 gwei); at the full 30M-gas plan cap the ceiling is 0.153 gwei. The check uses the padded limit, not expected usage, so the effective ceiling is 25% tighter than 0.0046 ETH / 19.5M gas suggests. docs/mainnet.md says to estimate before funding and that the release waits or fails closed, but the fee level the allocation actually tolerates is not recorded, and any revised bootstrapMaximumWei needs a re-issued owner approval (planHash). Recommend recording the tolerated fee ceiling next to the 0.0046 ETH figure before approval. The deterministic fixture in test/mainnet-release.test.mjs sets the anvil base fee to 0.1 gwei, below the ceiling.

**Reproduction**

plan.bootstrapMaximumWei = '4600000000000000', plan.bootstrapMaximumGas = 30000000, client.estimateGas = 19,500,000, client.estimateFeesPerGas = { maxFeePerGas: 200,000,000 wei (0.20 gwei), maxPriorityFeePerGas: 1 }. gas = 19,500,000*125/100 + 10,000 = 24,385,000; gas*maxFeePerGas = 4.877e15 > 4.6e15. Expected per the plan: a 0.0046 ETH bootstrap. Actual: deployMainnetRelease -> txs.prepare throws transaction_cost_limit before any reservation or signing; nothing is spent and no journal entry is created. At maxFeePerGas = 188,000,000 wei the same inputs pass (4.584e15 <= 4.6e15).

### 9. Info: B dispatch: the 0.00005 test ETH per-transaction ceiling makes callbacks with large requester-chosen callbackGas deliverable only below about 0.04 gwei

`adapters/contract-delivery.mjs:160`

```
        limits: { gas: budget.maximumGas, feePerGas: budget.maximumFeePerGas, cost: budget.maximumTransaction },
```

From audit_math; arithmetic verified. Capacity observation for the B scope, not an authorization bypass: nothing is spent beyond the ceilings. The public allocation sets maximumTransaction = maximumProject = 5e13 wei. prepare() requires (estimate*125/100 + 10000) * maxFeePerGas <= 5e13. ClausRequestInbox lets a requester choose callbackGas up to MAX_CALLBACK_GAS = 1,000,000 (contracts/src/ClausRequestInbox.sol:28) and deliverCallback forwards that much gas, so the estimate is at least callbackGas plus overhead. For callbackGas = 1,000,000 the padded limit is >= 1,260,000 gas and the callback can only be prepared when maxFeePerGas <= 5e13 / 1,260,000 = 39,682,539 wei (0.040 gwei); a ~200k-gas callback tolerates about 0.19 gwei; the resolve itself (~80-120k gas) tolerates 0.31-0.45 gwei. Above those fees the worker records delivery.reason transaction_cost_limit with a 15-minute retryAt (runtime/contract-intake-worker.mjs:45-52) until budget.expiresAt, after which the callback answers closed (line 187) and the requester receives only the onchain resolve. Because per-project and per-transaction ceilings are both 0.00005, a project whose resolve cost 3e13 wei has at most 2e13 wei left for its callback regardless of fee level. The docs say these are ceilings, not guarantees, but do not tell a requester which callbackGas values are realistically deliverable. Recommend publishing a recommended maximum callbackGas, or stating that callbacks are best-effort below about 0.2 gwei, with the public B announcement.

**Reproduction**

Public budget with maximumTransaction = maximumProject = '50000000000000'; an observed request with callbackGas = 1,000,000; estimateFeesPerGas().maxFeePerGas = 100,000,000 wei (0.1 gwei); estimateGas(deliverCallback) >= 1,000,000. prepare(): gas >= 1,260,000; gas*maxFeePerGas >= 1.26e14 > 5e13 -> throws transaction_cost_limit. Expected by a requester reading docs/contract-intake.md: a callback to its contract. Actual: delivery.state pending with reason transaction_cost_limit and retryAt +15 min on every tick while fees stay above 0.0397 gwei; after budget.expiresAt the callback closes unsent. With callbackGas = 0 the same request completes with the resolve only.

---

Judge's submission `13cdb6ad22f3d16bf0314c8cd0b328e5ba99bee313867e644e13afa870ee6daf`, 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.
