# Audit report

> Audit the TypeScript program under imd-flop at commit efd47f3df5c16b5170f2ac27add0860c018784da. Focus on src/run.ts, src/receipts.ts, src/ledger.ts and their tests.
>
> Find ways withheld completion text could leak into stdout or saved output. Check whether missing, negative, nonfinite or nonnumeric settlement amounts can incorrectly erase or reduce the pre-call reservation, charge zero, or under-account across repeated calls.
>
> Unknown spend must retain the reservation. Lower reported usage must not reduce unresolved exposure; higher valid usage may raise it. Confirm valid known settlements, including zero, still work.
>
> Require a file/line, impact and runnable reproduction for every finding. Read-only review: no fixes, deployments, credentials or live payments. This is TypeScript, not Solidity; report if unsupported. The separate Python spend guard is outside this audit. Do not assume an integrated payment flow.

| | |
|---|---|
| Repository | https://github.com/uxp-labs/flop-devnet-lab.git |
| Commit | `efd47f3df5c16b5170f2ac27add0860c018784da` |
| Job | `f31116ef-71d8-425a-8996-34079951184f` |
| Judged | 2026-11-10 04:35 UTC |
| Findings | 2 high · 3 medium · 6 low |

Four agents audited the code as it is at `efd47f3`, 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. High: padQuote accepts non-numeric token counts: the padded quote becomes NaN, every cap check passes, max_cost_flop is sent as null, and the attempt is booked at cap_charge null (counted as 0)

`imd-flop/src/run.ts:57`

```
  if (typeof raw !== "number" || !(raw > 0) || tin + tout <= 0) return null;
```

padQuote validates estimated_cost_flop but not estimated_input_tokens / estimated_output_tokens, which FlopProvider.estimate passes through from the gateway JSON unchecked (gateway.ts:40 `(await res.json()) as Estimate`). With a string or object token count, `tin + tout` is a string, `<= 0` is false, perTok = raw / string = NaN and padQuote returns { raw, padded: NaN } instead of null. In runJob `if (!q)` is false (object), and the per-call check (line 176), remaining-daily check (line 180) and requester-budget check (line 183) are all `>` comparisons against NaN, so all are false and the call is sent even when raw (50) is far above the cap (0.2). The `sent` line is appended with cap_charge NaN, which JSON.stringify writes as null; provider.complete receives maxCostFlop NaN, which mintChildKey/createChatCompletion serialise as max_spend/max_cost_flop null, removing the server-side cap too. After the call, Math.max(NaN, costReported) is NaN, so the final line is also null even when a valid cost_flop (0.18) was reported. spentToday/jobSpent (ledger.ts:58) add null as 0, so the day total stays 0 and repeated calls never reach the daily cap. The timeout_unknown path copies sent.cap_charge, so an aborted call is also booked at null. A numeric string ('3') does not produce NaN but a wrong padding ('3'+8 = '38' -> padded 0.100737 instead of 0.186). Expected (comment at line 166, 'Quote first; fail closed'): a malformed estimate is refused locally with EXIT.ESTIMATE_FAILED and nothing is sent, and no non-finite cap_charge is ever written. Fix: require Number.isFinite and >= 0 for tin and tout, Number.isFinite(raw) and Number.isFinite(padded) > 0 before returning; return null otherwise. Merged from audit_economics, audit_math and audit_flow (all three reproduced identically).

**Reproduction**

Harness: vitest in a copy of imd-flop (npm ci; npx vitest run test/scratch/x.test.ts), no network, using the IO.provider hook via test/helpers.ts cliP() exactly as test/ledger-reservation.test.ts does. Provider P wraps MockProvider('ok') and overrides only the named field. Seat prepared with readySeat(home,'9001',CAP). Run: cliP(home, ['run','--seat','9001','--job',JOB,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], provider). Unit: padQuote({ estimated_cost_flop: 50, estimated_input_tokens: 'n/a', estimated_output_tokens: 8 }) returns { raw: 50, padded: NaN } (expected null); same with estimated_input_tokens: {}. CLI: CAP='0.2'; P.estimate() returns the mock estimate with estimated_cost_flop: 50 and estimated_input_tokens: 'n/a'; run three times with jobs a, b, c. Expected: each run exits 7 (estimate returned no usable cost) or 3 (50 FLOP > per-call cap 0.2), provider.calls 0, nothing sent. Actual (observed on efd47f3): exit codes [0,0,0], provider.calls 3, req.maxCostFlop NaN on every call (JSON.stringify({max_spend: NaN}) === '{"max_spend":null}'), `run --json` prints quote_padded null and cap_charge null, ledger rows carry quote_padded null / cap_charge null, spentToday(home,'9001','mock') === 0 after three sent calls. Test body:
import { join } from 'node:path'; import { expect, it } from 'vitest'; import { MockProvider, type Provider, type CompleteReq } from '../../src/providers.js'; import { padQuote } from '../../src/run.js'; import { spentToday } from '../../src/ledger.js'; import { cliP, readLines, readySeat, tmpHome } from '../helpers.js';
class P implements Provider { readonly name = 'mock' as const; calls = 0; seen: unknown[] = []; inner = new MockProvider('ok'); async estimate() { return { estimated_cost_flop: 50, estimated_input_tokens: 'n/a' as unknown as number, estimated_output_tokens: 8 }; } async complete(r: CompleteReq) { this.calls++; this.seen.push(r.maxCostFlop); return this.inner.complete(r); } readReceipt(id: string) { return this.inner.readReceipt(id); } }
it('NaN quote fails closed', async () => { expect(padQuote({ estimated_cost_flop: 50, estimated_input_tokens: 'n/a' as unknown as number, estimated_output_tokens: 8 })).toBeNull(); /* FAILS: {raw:50,padded:NaN} */ const h = tmpHome(); await readySeat(h, '9001', '0.2'); const p = new P(); for (const j of ['a','b','c']) await cliP(h, ['run','--seat','9001','--job',j,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], p); expect(p.calls).toBe(0); /* FAILS: 3 */ expect(readLines(join(h,'ledger.jsonl')).every((e) => typeof e.cap_charge === 'number')).toBe(true); /* FAILS: null */ });

### 2. High: Receipt state is ignored: a numeric settled_spend on a reserved/estimated (unresolved) receipt is booked as the known settlement, so a provisional 0 erases the pre-call reservation and a provisional 0

`imd-flop/src/run.ts:238`

```
  const settledKnown = knownSettledSpend(classified.settled);
```

classifyReceipt (receipts.ts:61) derives `settled` from settled_spend alone and runJob takes knownSettledSpend(classified.settled) as the final cap_charge whenever it is a finite number >= 0, without consulting classified.state. A gateway receipt that still says state 'reserved' or 'estimated' but serialises settled_spend as 0 (a default or placeholder in a pending row) is therefore treated as a known zero settlement: the ledger's latest line for the attempt gets spend 0 and cap_charge 0 while the gateway itself says the call is unresolved. That violates the rule in ledger.ts:34 and the task requirement that unknown spend keeps the reservation; with state 'estimated', settled_spend 0.01 and usage cost_flop 0.25 the charge becomes 0.01, so a lower provisional figure also beats a higher valid reported cost. Because spentToday sums cap_charge, repeated calls under such receipts never fill the daily cap. The existing ledger-reservation tests cannot catch this: ScriptedUsageProvider forces state 'settled' whenever settled_spend is non-null (test/ledger-reservation.test.ts:50). Expected: a known settlement requires a terminal state ('settled', or 'released' with the amount) in addition to a finite non-negative amount; otherwise keep Math.max(quote_padded, cost_reported). Valid settled 0 and settled 0.05 under state 'settled' must keep working (verified: they do). Merged from audit_economics (medium), audit_math (high) and audit_permissions (high).

**Reproduction**

Harness: vitest in a copy of imd-flop (npm ci; npx vitest run test/scratch/x.test.ts), no network, using the IO.provider hook via test/helpers.ts cliP() exactly as test/ledger-reservation.test.ts does. Provider P wraps MockProvider('ok') and overrides only the named field. Seat prepared with readySeat(home,'9001',CAP). Run: cliP(home, ['run','--seat','9001','--job',JOB,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], provider). Case 1: CAP='0.2'; P.readReceipt() returns the mock receipt patched with { state: 'reserved', settled_spend: 0 }; run five times (jobs rz-0..rz-4). Expected: run 1 exits 0 with cap_charge 0.18 (unresolved), runs 2..5 exit 3 (over the remaining daily cap), spentToday 0.18. Actual (observed): exit codes [0,0,0,0,0]; latest ledger line per attempt {state:'reserved', spend:0, cost_reported:0.18, cap_charge:0, quote_padded:0.18}; spentToday === 0. Case 2: CAP='5'; receipt { state: 'estimated', settled_spend: 0.01 } and usage.cost_flop 0.25. Expected cap_charge 0.25 (max of reservation and reported cost, spend unresolved). Actual: {state:'estimated', spend:0.01, cost_reported:0.25, cap_charge:0.01}, spentToday 0.01. Control: { state:'settled', settled_spend: 0 } gives cap_charge 0 and { state:'settled', settled_spend: 0.05 } gives 0.05 (correct). Test body:
import { join } from 'node:path'; import { expect, it } from 'vitest'; import { MockProvider, type Provider, type CompleteReq } from '../../src/providers.js'; import { spentToday } from '../../src/ledger.js'; import { cliP, readLines, readySeat, tmpHome } from '../helpers.js';
class P implements Provider { readonly name = 'mock' as const; calls = 0; inner = new MockProvider('ok'); estimate(b: { model: string; prompt: string; max_tokens: number }) { return this.inner.estimate(b); } complete(r: CompleteReq) { this.calls++; return this.inner.complete(r); } async readReceipt(id: string) { const rr = await this.inner.readReceipt(id); return { ...rr, receipt: { ...rr.receipt!, state: 'reserved' as const, settled_spend: 0 } }; } }
it('reserved + settled_spend 0 keeps the reservation', async () => { const h = tmpHome(); await readySeat(h, '9001', '0.2'); const p = new P(); const codes: number[] = []; for (let i = 0; i < 5; i++) codes.push((await cliP(h, ['run','--seat','9001','--job','rz-'+i,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], p)).code); const last = readLines(join(h,'ledger.jsonl')).at(-1); expect(last.cap_charge).toBe(last.quote_padded); /* FAILS: 0 vs 0.18 */ expect(codes).toContain(3); /* FAILS: all 0 */ expect(spentToday(h,'9001','mock')).toBeGreaterThan(0); /* FAILS: 0 */ });

### 3. Medium: The receipt's request_id (and correlation_id) is never checked against the attempt: another request's settlement becomes this call's charge, is saved under our request_id, and match still reports 'mat

`imd-flop/src/receipts.ts:55`

```
    request_id: r?.request_id ?? ctx.requestId ?? "",
```

classifyReceipt copies the gateway's request_id into the classified receipt and takes settled_spend from the same object, but nothing compares r.request_id with ctx.requestId (the Idempotency-Key the run looked up). matchReceipt (match.ts:21-43) compares job, seat, did and correlation_id but never request_id, and it runs after appendLedger (run.ts:240-242), so even a detected correlation_id mismatch only annotates the printed receipt and does not stop the foreign settled_spend from becoming cap_charge. A wrong, cached or replayed receipt with settled_spend 0 (or any amount below the quote) therefore drops this attempt's 0.18 reservation to that amount, is written to receipts/<ourId>.json with the foreign request_id inside, and `receipt <ourId>` later prints the foreign id with match 'matches'. Expected: a receipt whose request_id (or correlation_id) differs from this attempt is not a known settlement for it: keep the unresolved charge, flag a 'request_id' mismatch, and have matchReceipt compare receipt.request_id with the ledger row. Merged from audit_economics, audit_math and audit_permissions.

**Reproduction**

Harness: vitest in a copy of imd-flop (npm ci; npx vitest run test/scratch/x.test.ts), no network, using the IO.provider hook via test/helpers.ts cliP() exactly as test/ledger-reservation.test.ts does. Provider P wraps MockProvider('ok') and overrides only the named field. Seat prepared with readySeat(home,'9001',CAP). Run: cliP(home, ['run','--seat','9001','--job',JOB,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], provider). CAP='5'. Case 1: P.readReceipt() returns the mock receipt patched with { request_id: 'mock-9001-0000000000000000', state: 'settled', settled_spend: 0 }; run job 'foreign'. Expected: receipt rejected as not ours, cap_charge 0.18, match 'mismatch'. Actual (observed): exit 0; printed request_id mock-9001-<random> but receipt.request_id mock-9001-0000000000000000; cap_charge 0; receipt.match {match:'matches', fields:[]}; latest ledger line cap_charge 0, spend 0; spentToday 0; receipts/mock-9001-<random>.json contains request_id 'mock-9001-0000000000000000'; `receipt mock-9001-<random> --json` prints request_id 'mock-9001-0000000000000000' with match 'matches'. Case 2: receipt { request_id: 'someone-else', correlation_id: 'imd-job-other', state: 'settled', settled_spend: 0 }. Actual: match {match:'mismatch', fields:['job(correlation_id)']} yet the ledger line still has cap_charge 0 (expected 0.18). Test body:
class P implements Provider { readonly name = 'mock' as const; calls = 0; inner = new MockProvider('ok'); estimate(b) { return this.inner.estimate(b); } complete(r) { this.calls++; return this.inner.complete(r); } async readReceipt(id: string) { const rr = await this.inner.readReceipt(id); return { ...rr, receipt: { ...rr.receipt!, request_id: 'mock-9001-0000000000000000', state: 'settled' as const, settled_spend: 0 } }; } }
it('foreign receipt is not a settlement', async () => { const h = tmpHome(); await readySeat(h, '9001', '5'); const r = await cliP(h, ['run','--seat','9001','--job','foreign','--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], new P()); expect(r.json().receipt.match.match).toBe('mismatch'); /* FAILS: matches */ const last = readLines(join(h,'ledger.jsonl')).at(-1); expect(last.cap_charge).toBe(last.quote_padded); /* FAILS: 0 vs 0.18 */ });

### 4. Medium: spentToday / jobSpent sum cap_charge without validation: a missing value makes the total NaN so the daily cap check fails open, and a null value (which the program itself writes in the NaN-quote case)

`imd-flop/src/ledger.ts:58`

```
      .reduce((a, e) => a + e.cap_charge, 0),
```

The daily and per-job totals add e.cap_charge straight from JSON.parse output. The TypeScript type `cap_charge: number` gives no runtime guarantee: a line without the field (truncated, hand-edited, or written by another tool) turns the sum into NaN; in runJob `left = NaN`, `left <= 0` is false and `q.padded > NaN` is false, so every subsequent call passes the daily-cap check, and `status --json` prints today_utc.mock null. A line with cap_charge null, which appendLedger writes whenever cap_charge is NaN (see the padQuote finding), adds 0 and silently drops that attempt's reservation. Both outcomes contradict the ledger's own contract (line 34: while spend is unknown, the padded quote). Expected: treat a non-finite or missing cap_charge as the row's quote_padded, or refuse with a ledger-integrity error; a NaN total must refuse, never admit; appendLedger should assert Number.isFinite(cap_charge) before writing. Merged from audit_math and audit_flow.

**Reproduction**

Harness: vitest in a copy of imd-flop (npm ci; npx vitest run test/scratch/x.test.ts), no network, using the IO.provider hook via test/helpers.ts cliP() exactly as test/ledger-reservation.test.ts does. Seat prepared with readySeat(home,'9001',CAP). Run: cliP(home, ['run','--seat','9001','--job',JOB,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], provider). CAP='0.2'. One normal run (job ok-1, charged 0.18, exit 0). Then append to ledger.jsonl a copy of the last row with request_id 'mock-9001-ffffffffffffffff' and the cap_charge key removed. Expected: spentToday is finite and the next run is refused with exit 3 (0.18 of 0.2 already reserved). Actual (observed): spentToday(home,'9001','mock') is NaN; the next run (job 'after') exits 0 and is sent; `status --json` shows today_utc {flop:0, mock:null}. Variant: on a fresh seat append a row with state 'sent', quote_padded 0.18, cap_charge null. Actual: spentToday 0 and the next 0.18 run exits 0 under a 0.2 daily cap. Test body:
import { appendFileSync } from 'node:fs'; import { join } from 'node:path'; import { expect, it } from 'vitest'; import { spentToday } from '../../src/ledger.js'; import { cli, readLines, readySeat, tmpHome } from '../helpers.js';
const RUN = (j: string) => ['run','--seat','9001','--job',j,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'];
it('ledger total is validated', async () => { const h = tmpHome(); await readySeat(h, '9001', '0.2'); expect((await cli(h, RUN('ok-1'))).code).toBe(0); const path = join(h, 'ledger.jsonl'); const last = readLines(path).at(-1); appendFileSync(path, JSON.stringify({ ...last, request_id: 'mock-9001-ffffffffffffffff', cap_charge: undefined }) + '\n'); expect(Number.isFinite(spentToday(h, '9001', 'mock'))).toBe(true); /* FAILS: NaN */ expect((await cli(h, RUN('after'))).code).toBe(3); /* FAILS: 0 */ });

### 5. Medium: Secret scan misses a key-shaped hex value inside a longer hex run (e.g. a 128-hex Ed25519/Solana secret key), so the completion text is not withheld: it is saved and printed by `run --json`

`imd-flop/src/secrets.ts:52`

```
const HEX64 = /(?<![0-9a-fA-F])(?:0x)?[0-9a-fA-F]{64}(?![0-9a-fA-F])/;
```

The withholding at run.ts:220-226 depends entirely on assertNoSecrets. HEX64 matches only a run of exactly 64 hex characters: a 128-hex string (the common seed||pubkey Ed25519 secret-key encoding) or a 64-hex key with any hex character adjacent to it matches nothing, although the module header (secrets.ts:8) promises to reject 'any 64-hex run'. The completion text is therefore stored in responses/<id>.json with text intact and returned as RunResult.text, which `run --json` prints to stdout. secrets.ts is outside the three focus files, but it is the only gate between a completion and stdout/saved output, which is the leak path the audit asks about. The rest of the withheld path behaves correctly (64-hex marker: text null, withheld reason, nothing in stdout or the file; plain output never prints text). Expected: flag any hex run of 64 or more characters (e.g. /(?:0x)?[0-9a-fA-F]{64,}/) outside PUBLIC_HEX_FIELDS. From audit_economics (low); raised to medium because the result is the key-shaped value on stdout and on disk.

**Reproduction**

Harness: vitest in a copy of imd-flop (npm ci; npx vitest run test/scratch/x.test.ts), no network, using the IO.provider hook via test/helpers.ts cliP() exactly as test/ledger-reservation.test.ts does. Provider P wraps MockProvider('ok') and overrides only the named field. Seat prepared with readySeat(home,'9001',CAP). Run: cliP(home, ['run','--seat','9001','--job',JOB,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], provider). Unit: scanForSecrets({ text: 'ab'.repeat(64) }) returns [] and scanForSecrets({ text: 'x' + 'ab'.repeat(32) + 'c' }) returns [], while scanForSecrets({ text: 'ab'.repeat(32) }) returns [{path:'text', reason:'64-hex (key-shaped) value'}]. CLI: CAP='5'; P.complete() returns text 'key: ' + 'ab'.repeat(64); run job hex128 with --json. Expected: printed text null, withheld 'secret scan blocked writing response: text: ...', response file text null. Actual (observed): exit 0, stdout contains the full 128-hex value, printed withheld is undefined, and responses/<id>.json contains the value. Test: it('128-hex is withheld', async () => { const h = tmpHome(); await readySeat(h,'9001','5'); const key = 'ab'.repeat(64); const r = await cliP(h, RUN('hex128'), new P(/* complete() sets text 'key: '+key */)); expect(r.out.includes(key)).toBe(false); /* FAILS: true */ expect(readFileSync(r.json().response_file,'utf8').includes(key)).toBe(false); /* FAILS: true */ });

### 6. Low: A completion without a usage object throws TypeError after the paid call: no final ledger line, the known settlement (even settled 0.1) is never recorded, no output even with --json, exit 1

`imd-flop/src/run.ts:233`

```
  const costReported = typeof res.usage.cost_flop === "number" ? res.usage.cost_flop : null;
```

FlopProvider.complete passes res.usage through as-is (providers.ts:153), so a 200 completion whose body omits usage reaches this line and throws 'Cannot read properties of undefined (reading cost_flop)'. By then the paid call happened and the response file and receipt file were written, but the final appendLedger (line 240) never runs: the ledger stays at `sent` (reservation kept, which is the safe direction) while the saved receipt may already say settled, `run --json` prints nothing although the text was stored, and `status` lists the attempt as pending. A non-string choices[0].message.content (array of content parts) fails the same way one line earlier at utf8ToBytes(res.text) ('string expected'). Expected: missing usage is handled as cost_reported null and the run completes with the reservation kept; non-string content is rejected or stringified before hashing. Fix: `typeof res.usage?.cost_flop === 'number'` plus normalising usage/text in FlopProvider.complete. Merged from audit_economics, audit_flow and audit_permissions.

**Reproduction**

Harness: vitest in a copy of imd-flop (npm ci; npx vitest run test/scratch/x.test.ts), no network, using the IO.provider hook via test/helpers.ts cliP() exactly as test/ledger-reservation.test.ts does. Provider P wraps MockProvider('ok') and overrides only the named field. Seat prepared with readySeat(home,'9001',CAP). Run: cliP(home, ['run','--seat','9001','--job',JOB,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], provider). CAP='5'; P.complete() returns the mock result with usage set to undefined; P.readReceipt() returns { state: 'settled', settled_spend: 0.1 }. Expected: exit 0, final ledger line state 'settled', spend 0.1, cap_charge 0.1, cost_reported null, JSON printed. Actual (observed): exit 1, stderr 'imd-flop: Cannot read properties of undefined (reading 'cost_flop')', stdout empty, ledger rows for the attempt = [['sent', 0.18, null]] only, responses/ and receipts/ both contain the file. Variant: text = [{type:'text', text:'hi'}] -> exit 1, stderr 'imd-flop: string expected'. Test: it('missing usage keeps the run alive', async () => { const h = tmpHome(); await readySeat(h,'9001','5'); const r = await cliP(h, RUN('nousage'), p); expect(r.code).toBe(0); /* FAILS: 1 */ expect(readLines(join(h,'ledger.jsonl')).at(-1).state).not.toBe('sent'); /* FAILS */ });

### 7. Low: The withheld-text fallback re-writes the unscanned gateway usage object, so a scan hit in usage throws out of runJob after the paid call: benign text lost, no receipt, ledger stuck at `sent`

`imd-flop/src/run.ts:225`

```
    writeJsonSafe(responseFile, { ...responseRecord, text: null, withheld }, "response");
```

The try/catch at lines 220-226 assumes the only field the secret scan can reject is `text`. responseRecord also spreads `usage: res.usage`, which for FlopProvider is the gateway's usage object verbatim. If it carries a secret-named key (token, key, api_key) or a 64-hex/Bearer/flop_-shaped value, the first writeJsonSafe throws, `withheld` is set and text nulled, and the second writeJsonSafe throws again for the same usage field. That CliError leaves runJob: readReceipt is never called, no response or receipt file is written, the post-call ledger line is never appended, and the benign completion is discarded. Exposure is not lowered (the sent line keeps cap_charge 0.18) and the error message names the path, not the value, so nothing leaks; the defect is a paid answer thrown away and an attempt left pending. Expected: the fallback persists a record that cannot trip the scan (text null, withheld reason, usage reduced to the numeric fields the tool reads) and still writes the receipt and the final ledger line. Merged from audit_math and audit_flow.

**Reproduction**

Harness: vitest in a copy of imd-flop (npm ci; npx vitest run test/scratch/x.test.ts), no network, using the IO.provider hook via test/helpers.ts cliP() exactly as test/ledger-reservation.test.ts does. Provider P wraps MockProvider('ok') and overrides only the named field. Seat prepared with readySeat(home,'9001',CAP). Run: cliP(home, ['run','--seat','9001','--job',JOB,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], provider). CAP='5'; P.complete() returns text 'benign output' and usage { prompt_tokens:1, completion_tokens:1, total_tokens:2, cost_flop:0.01, token:'abcdefgh12345678' }. Expected: exit 0, response and receipt files written, final ledger line state 'estimated' with cap_charge 0.18. Actual (observed): exit 5, stderr 'imd-flop: secret scan blocked writing response: usage.token: secret-named field "token"', stdout empty, provider.calls 1, responses/ and receipts/ absent, ledger rows [['sent', 0.18]], `status --json` lists the attempt under pending_receipts with ledger_state 'sent' and today_utc.mock 0.18. Test: it('fallback write survives a non-text scan hit', async () => { const h = tmpHome(); await readySeat(h,'9001','5'); const r = await cliP(h, RUN('usage-token'), p); expect(r.code).toBe(0); /* FAILS: 5 */ expect(existsSync(join(h,'receipts'))).toBe(true); /* FAILS */ });

### 8. Low: A gateway receipt string that trips the secret scan (error, settled_spend_raw) aborts runJob before the post-call ledger line, so a known settlement including an explicit 0 is never recorded

`imd-flop/src/run.ts:231`

```
  writeJsonSafe(join(receiptsDir(h), `${requestId}.json`), { ...classified, seat, did: signer.identity.did, ss58: signer.identity.ss58, network: net.name, ...(provider.name === "mock" ? { mock_scenario: (provider as { scenario?: string }).scenario ?? "ok" } : {}) }, "receipt");
```

classifyReceipt copies gateway-controlled strings (`error`, `settled_spend_raw` as settled_raw, correlation_id, state) into the receipt record. settled_raw and error are not in PUBLIC_HEX_FIELDS, so a 64-hex value in either (a raw amount encoded as hex, a transaction hash quoted in an error message) makes this writeJsonSafe throw after the paid call and after the response file is written but before appendLedger at line 240. The settled amount just read, even an explicit 0 that must charge 0, is never booked; the ledger stays at `sent` with the full reservation, no receipt file exists, and `receipt <id> --refresh` fails the same way while the gateway returns that string. Over-accounting direction only, but the settlement can never be persisted. Expected: append the ledger line before or independently of the receipt-file write, and sanitise free-text gateway fields (replace 64-hex runs with '[hash]' as the pre-send path at line 204 already does). From audit_flow.

**Reproduction**

Harness: vitest in a copy of imd-flop (npm ci; npx vitest run test/scratch/x.test.ts), no network, using the IO.provider hook via test/helpers.ts cliP() exactly as test/ledger-reservation.test.ts does. Provider P wraps MockProvider('ok') and overrides only the named field. Seat prepared with readySeat(home,'9001',CAP). Run: cliP(home, ['run','--seat','9001','--job',JOB,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], provider). CAP='5'; P.readReceipt() returns the mock receipt with settled_spend 0, state 'settled' and the ReceiptRead error field 'rejected tx ' + 'ab'.repeat(32). Expected: exit 0, ledger second line {state:'settled', spend:0, cap_charge:0}, today_utc.mock 0. Actual (observed): exit 5, stderr 'imd-flop: secret scan blocked writing receipt: error: 64-hex (key-shaped) value'; ledger rows [['sent', null, 0.18]]; receipts/ absent, responses/ present; `status --json` today_utc {flop:0, mock:0.18}. Variant with settled_spend_raw: 'cd'.repeat(32): exit 5, 'secret scan blocked writing receipt: settled_raw: 64-hex (key-shaped) value'. Test: it('receipt string does not block the settled-0 line', async () => { const h = tmpHome(); await readySeat(h,'9001','5'); const r = await cliP(h, RUN('rcpt'), p); expect(r.code).toBe(0); /* FAILS: 5 */ expect(readLines(join(h,'ledger.jsonl')).at(-1).cap_charge).toBe(0); /* FAILS: 0.18 */ });

### 9. Low: padQuote rounds to the nearest 1e-6 with no lower bound: a small positive quote becomes a 0 reservation (or one below the raw quote), and an unresolved call is then booked at 0 with max_cost_flop 0

`imd-flop/src/run.ts:59`

```
  return { raw, padded: Math.round((raw + TEMPLATE_PAD * perTok) * 1e6) / 1e6 };
```

Math.round can round down to 0. Any padded value under 5e-7 becomes 0, and 1.4e-6 raw gives padded 1e-6, below the quote it is meant to pad. runJob accepts 0 (not > per-call cap, not > remaining), writes the sent line with cap_charge 0, calls the provider with maxCostFlop 0 (which the gateway schema declares invalid: exclusiveMinimum 0), and if usage carries no cost_flop and the receipt is unresolved, Math.max(0, ...) stays 0: an attempt with unknown spend is booked at 0 against the daily cap. Needs gateway pricing far below the current 0.006 FLOP/token, so it is a boundary defect rather than a live risk. Expected: round the padded quote up (Math.ceil) and require padded >= raw > 0, returning null otherwise. Merged from audit_economics and audit_flow.

**Reproduction**

Unit: padQuote({ estimated_cost_flop: 4e-7, estimated_input_tokens: 10, estimated_output_tokens: 1000 }) returns { raw: 4e-7, padded: 0 }; padQuote({ estimated_cost_flop: 0.0000014, estimated_input_tokens: 10, estimated_output_tokens: 100000 }) returns padded 0.000001 < raw; padQuote({ estimated_cost_flop: 1e-8, estimated_input_tokens: 1, estimated_output_tokens: 1 }) returns { raw: 1e-8, padded: 0 }. Expected padded >= raw > 0 or null. Harness: vitest in a copy of imd-flop (npm ci; npx vitest run test/scratch/x.test.ts), no network, using the IO.provider hook via test/helpers.ts cliP() exactly as test/ledger-reservation.test.ts does. Provider P wraps MockProvider('ok') and overrides only the named field. Seat prepared with readySeat(home,'9001',CAP). Run: cliP(home, ['run','--seat','9001','--job',JOB,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], provider). CAP='0.1'; P.estimate() returns { estimated_cost_flop: 1e-8, estimated_input_tokens: 1, estimated_output_tokens: 1 }, P.complete() returns usage without cost_flop, receipt is the mock's (state estimated, settled null). Actual (observed): exit 0, provider.calls 1, req.maxCostFlop 0, ledger rows [state, quote_raw, quote_padded, cap_charge, cost_reported] = [['sent',1e-8,0,0,null],['estimated',1e-8,0,0,null]].

### 10. Low: --timeout-ms is not validated: NaN, 0 or a value above 2^31-1 becomes a 1 ms timer, so the paid call is aborted at once and booked as timeout_unknown with the full reservation

`imd-flop/src/cli.ts:322`

```
            timeoutMs: Number(values["timeout-ms"] ?? 60000),
```

Unlike --max-tokens (checked in runJob) and --job-budget / cap values (checked by num()), the timeout is passed to setTimeout at run.ts:192 as whatever Number() produced. Node clamps NaN, 0 and anything above 2147483647 to 1 ms (TimeoutNaNWarning / TimeoutOverflowWarning). The AbortController fires before any real gateway can answer: with the flop provider the child key is already minted (its own 15 s signal), the completion is aborted, the run exits 6 with 'no answer within NaN ms', the ledger records timeout_unknown with cap_charge = padded quote, and a paid answer is never collected. A holder asking for a long wait (3000000000) or mistyping the flag gets the opposite of what they asked for and the daily cap is consumed for no output. Expected: reject non-finite, non-positive and > 2147483647 values with EXIT.USAGE before any ledger write, as the other numeric flags do. Outside the three focus files but on the run path. Merged from audit_math, audit_flow and audit_permissions.

**Reproduction**

Harness: vitest in a copy of imd-flop (npm ci; npx vitest run test/scratch/x.test.ts), no network, using the IO.provider hook via test/helpers.ts cliP() exactly as test/ledger-reservation.test.ts does. Provider P wraps MockProvider('ok') and overrides only the named field. Seat prepared with readySeat(home,'9001',CAP). Run: cliP(home, ['run','--seat','9001','--job',JOB,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], provider). CAP='5'; P.complete() waits 30 ms honouring the abort signal, then delegates to the mock. Run with --timeout-ms abc, --timeout-ms 0 and --timeout-ms 3000000000. Expected: exit 2 usage error (or, for 3000000000, the call waits and succeeds). Actual (observed): each returns in under 10 ms with exit 6, stderr 'imd-flop: no answer within NaN ms. Spend is UNKNOWN ...' / 'within 0 ms' / 'within 3000000000 ms', provider.calls 1, latest ledger line {state:'timeout_unknown', cap_charge:0.18}, Node prints TimeoutNaNWarning / TimeoutOverflowWarning; --timeout-ms 1000 exits 0. Three aborted attempts leave 0.54 FLOP charged to the day. Test: expect((await cliP(h, [...RUN('t'), '--timeout-ms', 'abc'], new Slow())).code).toBe(2); /* FAILS: 6 */

### 11. Low: receipt --refresh rewrites the receipt file but never the ledger: a later, higher gateway settlement does not raise cap_charge or the day total

`imd-flop/src/cli.ts:404`

```
          writeJsonSafe(path, merged, "receipt");
```

The refresh path re-reads the gateway receipt and overwrites receipts/<id>.json but appends nothing to ledger.jsonl, so spentToday/jobSpent keep whatever cap_charge the original run recorded. The gateway documents that later reconciliation can change a receipt; if the run observed settled 0.05 and a refresh shows settled 0.15, the same `receipt --json` output prints settled 0.15 next to ledger_spend 0.05, and the daily cap is computed from 0.05. Under the requirement that higher valid usage may raise exposure, the refreshed higher figure should raise cap_charge (a lower one must not lower it). Under-count is bounded by quote minus first settlement because max_cost_flop caps the gateway. Expected: on --refresh with state settled, append a ledger line with cap_charge = max(previous cap_charge, new known settlement). From audit_permissions.

**Reproduction**

Harness: vitest in a copy of imd-flop (npm ci; npx vitest run test/scratch/x.test.ts), no network, using the IO.provider hook via test/helpers.ts cliP() exactly as test/ledger-reservation.test.ts does. Provider P wraps MockProvider('ok') and returns state 'settled' with settled_spend read from a mutable variable. Seat prepared with readySeat(home,'9001',CAP). Run: cliP(home, ['run','--seat','9001','--job',JOB,'--prompt','Say hi','--max-tokens','8','--provider','mock','--json'], provider). CAP='5'. Variable 0.05: run job 'refresh' with --json -> cap_charge 0.05. Set the variable to 0.15, then cli(home, ['receipt', <request_id>, '--refresh', '--json'], { provider }) and cli(home, ['status','--seat','9001','--json']). Expected: refreshed receipt settled 0.15 with ledger_spend/cap_charge raised to 0.15 and today_utc.mock 0.15. Actual (observed): refresh prints settled 0.15 with ledger_spend 0.05; latest ledger line cap_charge 0.05; status today_utc {flop:0, mock:0.05}.

---

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