# Audit report

> Audit only the Python spend guard: spend-guard/guard.py and spend-guard/test_guard_numeric.py at commit efd47f3df5c16b5170f2ac27add0860c018784da.
>
> Find cap bypasses or incorrect acceptance of malformed, negative, nonfinite or overflowing costs, token counts, padded estimates, caps and CLI token limits. Check valid zero and normal under/over-cap controls. Negative CLI token limits currently refuse after the estimate step; distinguish that from payment dispatch.
>
> Require a file/line, impact and runnable reproduction for each finding. Read-only review: no fixes, deployments, credentials or live payments. This is Python, not Solidity; explicitly report if that language is unsupported. Leave the TypeScript accounting program for a separate audit.

| | |
|---|---|
| Repository | https://github.com/uxp-labs/flop-devnet-lab.git |
| Commit | `efd47f3df5c16b5170f2ac27add0860c018784da` |
| Job | `57db0c89-5247-49bc-a396-d5296e4e595c` |
| Judged | 2026-11-10 04:10 UTC |
| Findings | 2 low · 5 info |

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. Low: Padded quote is rounded to nearest 1e-6 before the cap check, so a quote above the cap (even the raw quote alone) is classed would_send

`spend-guard/guard.py:63`

```
            candidate = round(raw + TEMPLATE_PAD * per_tok, 6)
```

Line 69 compares the rounded value (`padded <= cap`). round(x, 6) rounds to nearest, so any padded quote in (cap, cap+5e-7) is rounded down onto the cap and accepted, contradicting SPEC.md item 2 ('If the padded quote is over the caller's cap, stop locally'). With large token totals the pad vanishes and even the unpadded server quote can exceed the cap and still pass. The ledger then records quote_padded == cap, hiding the overshoot. Absolute overrun is bounded at 5e-7 FLOP per attempt (negligible at normal caps, but up to tens of percent of a cap near 1e-6); dry-run v0 sends nothing, but this is the check live mode would rely on before the unverified child-key max_spend backstop. Merged from audit_flow, audit_economics (rated medium) and audit_permissions/audit_math; rated low because the absolute bypass is <=5e-7 FLOP. Fix: compare the unrounded sum with the cap (or round up with math.ceil(x*1e6)/1e6) and round only for the ledger/display.

**Reproduction**

Harness (offline; the suite's own run_guard mocks urlopen and blocks sockets), from repo root: python3 -B -c "import sys,json; sys.path.insert(0,'spend-guard'); import test_guard_numeric as t; p,l,c=t.run_guard(BODY,CAP,ARGS); print(p.returncode, l.count(chr(10)), c.get('urlopen'), p.stdout or p.stderr.strip().splitlines()[-1])". Case A: BODY={'estimated_cost_flop':0.0660001,'estimated_input_tokens':3,'estimated_output_tokens':8}, CAP='0.186', ARGS=None. Exact padded = 0.0660001*31/11 = 0.18600028 > 0.186. Expected refused_local; actual rc 0, quote_padded 0.186, cap 0.186, decision would_send. Case B (raw over cap): BODY cost 1.0000004, input_tokens 10**9, output_tokens 8, CAP='1' -> quote_raw 1.0000004, quote_padded 1.0, decision would_send. Control: cost 0.066, tokens 3/8, CAP '0.05' -> refused_local; CAP '1' -> would_send; CAP '0.186' (exactly equal) -> would_send, consistent with 'over the cap'. Reproduced at efd47f3.

### 2. Low: Finite but oversized integer cost/token counts raise an uncaught OverflowError: exit 1, no record, no ledger line

`spend-guard/guard.py:62`

```
            per_tok = raw / total
```

json_number() and nonnegative_count() bound only floats (isfinite); JSON integer literals become unbounded Python ints, and integer-valued floats like 1e308 are widened by int() at line 60. When raw or total exceeds float range, `raw / total` (line 62) or `raw + TEMPLATE_PAD * per_tok` (line 63) raises OverflowError outside the try that only wraps estimate() (lines 49-52). main() aborts with a traceback before the ledger write at line 74. Fail-closed (nothing sent), so not a cap bypass, but it breaks SPEC.md item 6 ('one JSON line per attempt') and differs from every other malformed quote, which becomes a recorded refused_local (e.g. 1e400 parses to inf and is refused cleanly). A caller scripting on exit code may treat it as a transient error and retry. Merged from all four specialists. Fix: reject ints/counts not representable as finite floats in json_number/nonnegative_count (float(x) in try/except OverflowError + isfinite), or wrap lines 60-65 in try/except (OverflowError, ValueError) leaving padded=None.

**Reproduction**

Harness (offline; the suite's own run_guard mocks urlopen and blocks sockets), from repo root: python3 -B -c "import sys,json; sys.path.insert(0,'spend-guard'); import test_guard_numeric as t; p,l,c=t.run_guard(BODY,CAP,ARGS); print(p.returncode, l.count(chr(10)), c.get('urlopen'), p.stdout or p.stderr.strip().splitlines()[-1])". ARGS=None, CAP='1'. BODY={'estimated_cost_flop':10**400,'estimated_input_tokens':3,'estimated_output_tokens':8} -> rc 1, 0 ledger lines, 'OverflowError: integer division result too large for a float'. BODY cost 0.066, input_tokens 10**400, output 8 -> rc 1, 0 lines, 'OverflowError: int too large to convert to float'. BODY cost 0.066, tokens 1e308/1e308 (each finite and accepted) -> rc 1, 0 lines, same error. BODY cost 10**309, tokens 3/8 -> rc 1, overflow at line 63. Expected for all: rc 0, one ledger line, decision refused_local, quote_padded null, spend 0. Reproduced at efd47f3.

### 3. Info: A valid positive quote whose padded value is below 5e-7 rounds to 0.0 and is refused as if malformed

`spend-guard/guard.py:64`

```
            if json_number(candidate) is not None and candidate > 0:
```

Same rounding as the line-63 finding in the other direction: round() yields 0.0, `candidate > 0` fails, padded stays None and the ledger shows quote_padded null, indistinguishable from a missing/non-numeric cost. Fail-closed, no spend; an incorrect refusal of a valid under-cap quote only at sub-micro-FLOP costs (unrealistic at ~0.006 FLOP/token). Merged from flow, permissions, math. Fix: check positivity on the unrounded value (already implied by raw > 0) and round only for display, or round up.

**Reproduction**

Harness (offline; the suite's own run_guard mocks urlopen and blocks sockets), from repo root: python3 -B -c "import sys,json; sys.path.insert(0,'spend-guard'); import test_guard_numeric as t; p,l,c=t.run_guard(BODY,CAP,ARGS); print(p.returncode, l.count(chr(10)), c.get('urlopen'), p.stdout or p.stderr.strip().splitlines()[-1])". BODY={'estimated_cost_flop':1e-7,'estimated_input_tokens':3,'estimated_output_tokens':8}, CAP='1', ARGS=None. Exact padded ~2.82e-7 <= 1. Expected would_send; actual quote_raw 1e-07, quote_padded None, decision refused_local. Control cost 1e-6 -> quote_padded 3e-06, would_send.

### 4. Info: --cap/--max-tokens accept Python numeric literal forms (underscores, whitespace), so a typo like 0_5 becomes cap 5.0 instead of exit 2

`spend-guard/guard.py:46`

```
    ap.add_argument("--cap", type=float, required=True)
```

argparse type=float/int delegate to float()/int(), which accept PEP 515 underscores and surrounding whitespace. '--cap 0_5' (intended 0.5) is read as 5.0, a 10x larger cap, rather than rejected like 'abc' (test_malformed_cap_exits_2_without_estimate). nan/inf/negative/zero caps are correctly refused at lines 66/69, so this is not a bypass of a well-formed cap; the effect is a silently different cap from a malformed argument, visible afterwards in the ledger. Merged from flow and math. Fix: parse with a strict decimal regex/Decimal and reject '_' and whitespace.

**Reproduction**

Harness (offline; the suite's own run_guard mocks urlopen and blocks sockets), from repo root: python3 -B -c "import sys,json; sys.path.insert(0,'spend-guard'); import test_guard_numeric as t; p,l,c=t.run_guard(BODY,CAP,ARGS); print(p.returncode, l.count(chr(10)), c.get('urlopen'), p.stdout or p.stderr.strip().splitlines()[-1])". BODY={'estimated_cost_flop':0.2,'estimated_input_tokens':3,'estimated_output_tokens':8}, ARGS=['--user','Hi','--max-tokens','8','--cap','0_5'] (meant 0.5). Expected exit 2 with no estimate call. Actual rc 0, cap 5.0, padded 0.563636, decision would_send (cap 0.5 would refuse). Verified with cost 0.066: cap recorded 5.0. --max-tokens 1_0 is likewise read as 10.

### 5. Info: Negative --max-tokens is POSTed to the free estimate endpoint before the local refusal; no payment is dispatched

`spend-guard/guard.py:50`

```
        e = estimate(a.model, prompt, a.max_tokens)
```

The only sign check on max_tokens is in the decision at line 69, after estimate() has sent max_tokens=-1 to POST /v1/models/estimate. This is 'refused after the estimate step', not a payment: v0 has no child-key mint, completion or other paid call anywhere in guard.py, and the decision is refused_local with spend 0. The guard records a quote for a request that can never be sent. Consistent with test_negative_cli_max_tokens_refuses (urlopen 1). Merged from flow, economics, math. Optional: validate max_tokens >= 0 at parse time (exit 2, like --cap) before any network call, which matters once live mode exists.

**Reproduction**

Harness (offline; the suite's own run_guard mocks urlopen and blocks sockets), from repo root: python3 -B -c "import sys,json; sys.path.insert(0,'spend-guard'); import test_guard_numeric as t; p,l,c=t.run_guard(BODY,CAP,ARGS); print(p.returncode, l.count(chr(10)), c.get('urlopen'), p.stdout or p.stderr.strip().splitlines()[-1])". BODY={'estimated_cost_flop':0.066,'estimated_input_tokens':3,'estimated_output_tokens':8}, ARGS=['--user','Hi','--max-tokens','-1','--cap','1']. Actual: urlopen count 1 (only .../models/estimate), rc 0, max_tokens -1, quote_padded 0.186, decision refused_local, spend 0. Payment dispatch: none (no code path exists).

### 6. Info: Trust assumption: the decision uses the gateway's estimated_output_tokens, never the CLI max_tokens beyond its sign

`spend-guard/guard.py:69`

```
    decision = "would_send" if a.max_tokens >= 0 and cap is not None and cap > 0 and padded is not None and padded <= cap else "refused_local"
```

The quote is an upper bound only if the gateway's estimate already accounts for max_tokens; the guard does not check estimated_output_tokens against --max-tokens, and the pad (20*raw/total) shrinks as the reported counts grow. A request allowing ~1e23 output tokens is accepted on an 8-token quote. Not a defect in dry run; documented for live mode, whose hard limit is the (unverified) child-key max_spend. From audit_permissions.

**Reproduction**

Harness (offline; the suite's own run_guard mocks urlopen and blocks sockets), from repo root: python3 -B -c "import sys,json; sys.path.insert(0,'spend-guard'); import test_guard_numeric as t; p,l,c=t.run_guard(BODY,CAP,ARGS); print(p.returncode, l.count(chr(10)), c.get('urlopen'), p.stdout or p.stderr.strip().splitlines()[-1])". BODY={'estimated_cost_flop':0.066,'estimated_input_tokens':3,'estimated_output_tokens':8}, ARGS=['--user','Hi','--max-tokens','99999999999999999999999','--cap','1'] -> decision would_send, quote_padded 0.186.

### 7. Info: Regression suite misses overflow, cap-boundary rounding, zero-cap and at-cap cases

`spend-guard/test_guard_numeric.py:135`

```
    def test_finite_under_cap_would_send(self):
```

`python3 -B spend-guard/test_guard_numeric.py` runs 32 tests, OK, at efd47f3, while the OverflowError crash and the rounding acceptance above pass unnoticed: no fixture uses ints beyond float range or 1e308 counts, no padded quote within 5e-7 of the cap, no --cap 0, no exactly-at-cap quote, no sub-5e-7 cost. Merged from flow and permissions.

**Reproduction**

Run the suite (32 OK), then run the inputs in the line-62 and line-63 findings: crash / would_send respectively, with no test failing.

---

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