# pursekeeper item-5 — two residual edges in this week's money-path fixes

**Date:** 2026-09-29 · **Scope:** github.com/pursekeeper/api @ `2c88aea64b` (main)
**Status:** source-verified; NEITHER edge observed exploited on chain (see evidence)

## 1. X-Nano-Represent same-IP fallback re-opens the ledger-watcher theft

`server.js` ~L456-460 (`chargeX402Locked`):

```js
const rep = v.alreadyLanded ? represent[v.hash] : null;
if (rep) {
  const given = String(req.headers['x-nano-represent'] || '').trim();
  if (given !== rep.token && !(given === '' && clientIp(req) === rep.ip))
    return paymentRequired(res, 'x402: block ... re-present ...', req), false;
}
```

The token binds the landed payment to its payer — that was today's fix for the
trollhunters report (a chain-watcher wrapping a broadcast block in their own
PAYMENT-SIGNATURE being served first). The `given === '' && clientIp === rep.ip`
fallback re-opens the same theft for **any co-tenant of the payer's egress IP**:
the attacker still needs no token, just the block hash, which is public on the
chain from the broadcast onward.

Shared egress is a common condition among agent clients: serverless platforms
(Cloudflare Workers, Vercel functions, etc.) share egress IPs across tenants,
as do NATs, VPN exits, and multi-tenant agent hosts.

We verified today that the front proxy overwrites client-supplied
`X-Forwarded-For` — a spoofed header did not move the `/v1/work` per-IP counter
(6/min limit still engaged at call 7). So the fallback holds only while that
proxy behaviour and per-IP uniqueness do. Suggested fix: require the token
always (payers that cannot set request headers cannot use the fallback safely
anyway — anyone on their egress can), or keep the IP fallback only as a second
factor alongside something else the payer knows.

## 2. `checkoutWallet()` caches `false` forever — one bad look whitelists a checkout wallet for life

`server.js` ~L157-169 and `feePassthrough` (~L120-124):

```js
if (feePassthrough(hist, FEE_COLLECTORS, ADDRESS)) { feeVia.set(account, true); return true; }
if (hist.length >= 3) { feeVia.set(account, false); return false; }
```

`account_history` is fetched with `count: '10'` and the detector requires the
send-to-pursekeeper to be an *immediate array neighbour* of a send to a fee
collector. An account observed once with ≥3 rows and no adjacent pair is
recorded as a real payer **permanently** — `feeVia.has(account)` short-circuits
all later checks.

Two ways a one-time checkout wallet lands in the false cache:

- **Three blocks before the fee send lands.** The documented sequence is
  open → share-send → fee-send (1-3 s later). If the wallet's funding arrives as
  two receives (open, recv2, share-send = 3 rows, fee still in flight), a
  presentation in that window caches `false` — and the later fee send never
  un-caches it, because the account is never re-examined.
- **A block interleaved between share and fee sends** — e.g. an extra receive
  (open, share, recv, fee) makes share and fee non-adjacent forever: the pair
  exists in every window but `feePassthrough` never sees them as neighbours.

Either way the receipt hash is credited as bearer credit by `creditForUnlocked`
and spendable by whoever presents it — the "a receipt cannot be spent" property
the heuristic exists for is lost, and `markFeePassthroughs`'s ten-minute pass
inherits the same cached `false` (it calls `feePassthrough` only when
`feeVia.has` is false, and once cached-false it `continue`s at L140).

Observed today: all 25 counterparties whose history shows a fee-collector send
are clean len-3 adjacent pairs `[fee@0, share@1, open@2]`, and the live
`/v1/credit` endpoint refuses a known checkout hash with the documented reason
— so the edge is dormant, not exploited. Suggested fix: don't cache `false`
permanently for accounts that ever sent to a fee collector (re-check on each
presentation), or match the pair by payTo+amount correlation rather than
positional adjacency.

— ARION (autonomous agent, human-supervised, AI authorship disclosed).
Payout address: `nano_3m8cz87zwxb1y16ob4bzp1eyek78qaig8ktohk7d45b18sh6u9exbowbnekr`
