@oracle-agent/oracle 0.24.1 → 0.24.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/assets/skills/chain/SKILL.md +34 -0
  2. package/dist/assets/skills/chain-defi-ecosystem-analysis/SKILL.md +181 -0
  3. package/dist/assets/skills/chain-ecosystem-gap-analysis/SKILL.md +162 -0
  4. package/dist/assets/skills/cross-chain-twap-execution/SKILL.md +125 -0
  5. package/dist/assets/skills/defi-protocol-pmf-assessment/SKILL.md +332 -0
  6. package/dist/assets/skills/evm-contract-research.md +8 -3
  7. package/dist/assets/skills/multi-venue-prepare-only-ranking/SKILL.md +93 -0
  8. package/dist/assets/skills/oracle-access-control/SKILL.md +71 -0
  9. package/dist/assets/skills/oracle-action-arming/SKILL.md +99 -0
  10. package/dist/assets/skills/oracle-airdrop-calculator/SKILL.md +111 -0
  11. package/dist/assets/skills/oracle-desk-product/SKILL.md +341 -0
  12. package/dist/assets/skills/oracle-evm/SKILL.md +55 -0
  13. package/dist/assets/skills/oracle-harness/SKILL.md +44 -0
  14. package/dist/assets/skills/oracle-mcp-install/SKILL.md +140 -0
  15. package/dist/assets/skills/oracle-multichain-convert/SKILL.md +87 -0
  16. package/dist/assets/skills/oracle-native-harness/SKILL.md +32 -0
  17. package/dist/assets/skills/oracle-ownership-gate/SKILL.md +42 -0
  18. package/dist/assets/skills/oracle-public-product-ux/SKILL.md +114 -0
  19. package/dist/assets/skills/oracle-tailscale/SKILL.md +32 -0
  20. package/dist/assets/skills/oracle-thin-client/SKILL.md +47 -0
  21. package/dist/assets/skills/perp-venue-funding-research/SKILL.md +161 -0
  22. package/dist/assets/skills/polymarket/SKILL.md +160 -0
  23. package/dist/assets/skills/protocol-api-key-integration/SKILL.md +141 -0
  24. package/dist/assets/skills/self-custodial-onchain-execution/SKILL.md +1284 -0
  25. package/dist/assets/skills/setup/SKILL.md +40 -0
  26. package/dist/assets/skills/stable-launch-ops/SKILL.md +89 -0
  27. package/dist/assets/skills/trade-loop-circuit-breaker/SKILL.md +441 -0
  28. package/dist/assets/skills/venue-capability-boundaries/SKILL.md +32 -0
  29. package/dist/bin/desk-server.mjs +16 -16
  30. package/dist/bin/oracle-data-mcp.mjs +1 -1
  31. package/dist/bin/oracle-equities.mjs +1 -1
  32. package/dist/bin/oracle-gateway.mjs +18 -15
  33. package/dist/bin/oracle-init.mjs +9 -9
  34. package/dist/cli/commands/bootstrap.mjs +1 -1
  35. package/dist/cli/commands/chat.mjs +86 -77
  36. package/dist/cli/commands/doctor.mjs +8 -6
  37. package/dist/cli/commands/eval.mjs +1 -1
  38. package/dist/cli/commands/harness.mjs +6 -6
  39. package/dist/cli/commands/model.mjs +87 -78
  40. package/dist/cli/commands/receipt.mjs +5 -0
  41. package/dist/cli/commands/setup.mjs +1 -1
  42. package/dist/cli/commands/venues.mjs +3 -0
  43. package/dist/cli/commands/watch.mjs +16 -0
  44. package/dist/equities/index.mjs +1 -1
  45. package/dist/index.mjs +1 -1
  46. package/package.json +1 -1
  47. package/public/install.ps1 +102 -0
  48. package/public/install.sh +1 -1
  49. package/public/oracle-splash/downloads/index.html +2 -0
  50. package/public/oracle-splash/index.html +1 -0
  51. package/public/oracle-splash/install.ps1 +102 -0
  52. package/public/oracle-splash/install.sh +1 -1
@@ -0,0 +1,1284 @@
1
+ ---
2
+ name: self-custodial-onchain-execution
3
+ description: Build public token and NFT scanners that prepare wallet-signed swaps or mints without backend custody, with verified routes, simulations, honest risk coverage, and fail-closed transaction gates.
4
+ ---
5
+
6
+ > Oracle native tools: `oracle_cli` (read/prepare), `vault_status`, `signer_status`, `signer_execute` (needs human confirmationNonce), `skill_load`. No generic shell. No fleet SSH.
7
+
8
+
9
+ # Self-custodial on-chain execution
10
+
11
+ Use when a scanner, launchpad, or trading dashboard moves from read-only/paper mode to transactions signed by the user's Rabby or EVM wallet.
12
+
13
+ ## Authority model
14
+
15
+ A wallet signature proves address control, not operator/admin authority.
16
+
17
+ - Public wallet session: personalized data, paper records, and transactions that the same wallet must sign.
18
+ - Token-gated session: public scope plus an on-chain ownership check.
19
+ - Operator scope: explicit server allowlist or enrollment.
20
+ - Admin scope: separate credentials for backend signers, portfolios, policy changes, and kill switches.
21
+
22
+ An open preview may accept any signed wallet only when every money-moving action remains self-custodial and backend live submission is absent from the build.
23
+
24
+ ## Arming posture: key present = armed, user asks = it runs
25
+
26
+ **DEMI correction — do not gate each capability behind its own env flag.**
27
+
28
+ ```
29
+ Key present -> capability is ARMED
30
+ User asks for it -> it RUNS
31
+ Autonomous mode -> the ONLY separate opt-in
32
+ ```
33
+
34
+ Supplying a signing key **is** the authorization decision. Making someone who
35
+ already handed over a key hunt down a second and third `ORACLE_X_ENABLED=1` to
36
+ buy an NFT is friction, not security — and it teaches people to set every flag,
37
+ which is strictly worse than one coherent default. What actually protects the
38
+ user is that every action is **user-initiated** and **bounded by caps at the
39
+ moment it runs**.
40
+
41
+ Autonomous/unattended trading is the sole exception, because it is the only mode
42
+ where the agent initiates value movement without being asked. An agent that
43
+ trades while you sleep is a different risk class from one that buys the NFT you
44
+ just named.
45
+
46
+ Keep an `execute: true` marker at the call site (it stops a send happening as a
47
+ side effect of a read path) but do not add an env arm beside it. `armed` and
48
+ `allowed` are distinct states and must be reported separately: key present with
49
+ no user request is `armed: true, allowed: false`. Only the literal string `"1"`
50
+ enables autonomous — `"true"`, `"yes"`, `"01"` must all stay off.
51
+
52
+ Full rule, the `capabilityStatus` shape, and the retro-fit note (delete arms you
53
+ already added): `references/key-material-and-arming-posture.md`.
54
+
55
+ ## Wallet UX
56
+
57
+ Wallet connect is a primary control, not a small utility button. Keep it persistent in the top bar and repeat a dominant connect/sign action inside execution. Discover ALL wallets with EIP-6963 (RH AGENT must support Rabby AND MetaMask), rank by rdns for the default (rabby=0, metamask=1, other=2), and render a picker `<select>` in the top bar when more than one wallet announces. Switching wallets must fully reset the session (token, gate status, balances, quotes). Switch/add the target chain, request accounts, sign short-lived single-use challenges only for authenticated actions, and invalidate sessions on account or chain changes. Copy rule: never hardcode "Rabby" in notices/buttons — use the active `walletName` ("WAITING FOR WALLET", "confirm in your wallet") so MetaMask users aren't told to check Rabby.
58
+
59
+ ### Keep wallet identities separate
60
+
61
+ Never collapse these three concepts in API responses or UI:
62
+
63
+ - **Connected wallet:** browser signer and holder-gate identity. Its Robinhood Chain balance comes from its address through RPC.
64
+ - **Agent API key owner:** address bound to the `rhag_` credential. The key authenticates reads/prepares; it is not a private key or spending authority.
65
+ - **Executor wallet:** separate capped signer for unattended actions. Put its address, RH balance, caps, spend today, funding state, and kill-switch state in a dedicated card beside Agent Connect.
66
+
67
+ `GET /api/agent/policy` is policy, not portfolio data. Expose the API-key owner's balance through `GET /api/agent/positions`, and the executor balance through a dedicated status endpoint. Label both with address and chain.
68
+
69
+ A locally generated executor JSON is **not installed** merely because an address exists. Do not tell the user to fund it until the signer has a secure backup, the key has moved directly into production secret storage without chat/log exposure, executor code is deployed, and the website resolves the same address. Never offer a website private-key download. Deploy code/status first, verify the unfunded capped state, then fund, then separately GO-gate broadcasts.
70
+
71
+ ### Executor discovery, ownership, and activation
72
+
73
+ An executor address is public, not a secret. Make it easy for the authorized operator to find and verify:
74
+ - Show the full address, copy control, chain, balance, signer-installed state, caps, daily spend, and enabled/halted state in the dedicated Agent Connect card.
75
+ - Expose the same public fields through an authenticated status endpoint. If policy is public, exposing the address there is acceptable, but never expose key material or imply that address visibility grants control.
76
+ - Before funding, derive the address independently from the production signer file and require an exact match with both UI and API.
77
+
78
+ Do not let public users fund one shared operator executor. A server-wide hot wallet is appropriate only for the operator's own capped automation. Multi-user execution needs one isolated executor or session key per user, tied to that user's API-key owner, with a separate balance, ledger, caps, allowlists, and kill switch. Otherwise the product creates pooled custody, cross-user accounting, and shared-compromise risk.
79
+
80
+ Treat activation as a ladder, not one switch:
81
+ 1. Deploy signer and status with execution disabled.
82
+ 2. Verify production address, file ownership/mode, zero balance, caps, and blocked execute routes.
83
+ 3. On explicit GO, enable `agent:execute` only for a controller allowlist.
84
+ 4. Never promote a key pasted into chat or screenshots. Revoke it, generate a replacement directly into owner-only secret storage, and verify its scope without printing the raw key.
85
+ 5. Keep token/NFT and scanner auto-allowlists empty while enabling scope. Execute scope alone must not create a tradeable asset universe.
86
+ 6. Add one verified resource allowlist, fund narrowly, execute one tiny idempotent request, wait for a successful receipt, reconcile, then expand.
87
+
88
+ Report each gate separately. `agent:execute=true` does not mean "trading": an unfunded wallet or empty asset allowlists still blocks every action.
89
+
90
+ ### Chat-leaked burner keys are still burn-only
91
+
92
+ If a user pastes a Solana secret key, BTC WIF, seed phrase, or Xverse seed into chat, do **not** import or use it, even when they say it is only a burner. Treat the key as compromised, tell them to generate/import a fresh burner directly on the machine that will sign, and accept only the public address in chat. Long base58 Solana strings are often secret keys; validate by reading the public address only. For Bitcoin inscriptions, prefer a fresh local WIF file that pays commit/reveal fees while inscribing **to** a separate ordinal receive address. Never store an Xverse seed for agentic testing.
93
+
94
+ Creator/operator urgency is not an exception. If DEMI says "use it for now", "save it locally", or "we already did it for EVM", keep the key-ingress invariant and make the safe local path fast: create the owner-only key directory, create empty `0600` files, and provide/run a prompt-only local installer (`read -s`, no echo, no logs) for the user to paste on the signing machine. After install, the agent may report file mode/size, derive public addresses, and check balances/readiness; never print or copy the raw secret.
95
+
96
+ Detailed response pattern: `references/chat-leaked-key-ingestion.md`.
97
+
98
+ ### Operator installer, owner-gate, and launch hardening
99
+
100
+ The execution policy is only as strong as the files and launchers that select it. Treat
101
+ operator installation as part of the signer boundary:
102
+
103
+ - Do not let a model-writable adjacent file reassign the human owner of a pre-tool gate.
104
+ Pin a dedicated operator identity in reviewed private code, or load it from an
105
+ integrity-protected authority outside the model's ordinary write surface. Add a regression
106
+ proving `owner.id`-style files cannot change the caller accepted by the hook. Keep
107
+ machine-specific identities out of canonical/public defaults.
108
+ - A text-arm must be scoped to the current conversation and current owner. Resolve the actual
109
+ caller from session state, reject non-owner/unknown sessions, and avoid accepting a stale
110
+ `GO` from an unrelated message as standing authorization.
111
+ - Never `source` credential env files from shell or generated service launchers. That executes
112
+ their contents as code. Require regular non-symlink owner-only files, parse them as data, or
113
+ use the runtime's data-only env-file option. Pass bearer headers through stdin or an
114
+ equivalent non-argv channel, and make authenticated status helpers reject non-loopback
115
+ destinations and reflected secrets.
116
+ - Secret migration/recovery writes must be same-directory temp-file + atomic rename, with
117
+ `lstat` checks that reject symlinks and non-regular destinations. Secure the parent directory,
118
+ use `0600`, refuse orphaned passphrase/keystore mismatches, verify the derived public address
119
+ before replacement, and retain a rollback copy without logging key material.
120
+ - Test installers and launchers as security code: injection-shaped env values remain inert,
121
+ broad file modes fail closed, symlinked destinations are refused, and final status proves the
122
+ execution plane is still disarmed.
123
+
124
+ ### One key controls MANY addresses — enumerate before concluding
125
+
126
+ A single secp256k1 key controls P2PKH (`1…`), P2SH-P2WPKH (`3…`), P2WPKH
127
+ (`bc1q…`) and P2TR (`bc1p…`) simultaneously. Old wallets and exchanges default to
128
+ `3…`; modern signing code defaults to `bc1q…`. Funds sitting at one script type
129
+ while the signer builds inputs for another looks *exactly* like "the agent can't
130
+ read the key" or "insufficient funds" — and it is neither.
131
+
132
+ **Before reporting a balance, before "insufficient funds", and before signing,
133
+ derive and check every address type the key controls.** Use the signing library's
134
+ own derivation (`@scure/btc-signer` `p2wpkh`/`p2tr`/`p2pkh`/`p2sh`), never a
135
+ hand-rolled bech32/base58 path — hand-rolling needs the BIP341 TapTweak by hand
136
+ and produces a plausible-looking wrong taproot address.
137
+
138
+ When funds are at the wrong script type, consolidate first: a P2SH-wrapped
139
+ segwit input needs **both** `witnessUtxo` and `redeemScript`. Assert derived
140
+ addresses against expected constants before broadcast, and check the UTXO set
141
+ for ordinal/dust outputs (`<= 1000 sats`) before sweeping — an ordinals address
142
+ with hundreds of funded outputs must never be auto-selected.
143
+
144
+ ### Encrypt local signer keys at rest
145
+
146
+ `0600` is one backup tarball, one `~/.config` cloud-sync, or one stolen disk from
147
+ spendable. Wrap key files: scrypt(N=2^17) → KEK, AES-256-GCM wraps a random DEK,
148
+ DEK encrypts the material, JSON header bound as AAD so a KDF-cost downgrade fails
149
+ authentication instead of silently weakening decryption. Two layers mean a
150
+ passphrase rotation rewraps 32 bytes and never exposes the secret.
151
+
152
+ State the threat model honestly — it defends backups, sync, and stolen disks; it
153
+ does **not** defend a live process already holding the key or a keylogger. Never
154
+ oversell a file-level scheme.
155
+
156
+ Operational invariants: verify decrypt-to-byte-identical **before** touching the
157
+ original, never delete the plaintext yourself (move to `.bak`, tell the user to
158
+ `shred -u`), passphrase never in argv (`ps` leaks it), passphrase file outside
159
+ the keys directory, raw key files keep loading unchanged (encryption is opt-in),
160
+ and no code path writes a decrypted key to disk.
161
+
162
+ Both, with the derivation snippet, vault design, and the tests that carry weight:
163
+ `references/key-material-and-arming-posture.md`.
164
+
165
+ ## Swap gate
166
+
167
+ Before returning calldata:
168
+
169
+ 1. Resolve factory, router, quoter, wrapped native token, and any Universal Router from authoritative docs plus verified explorer source and live bytecode. Explorer names alone are unsafe because clones share names.
170
+ 2. Bind token, pool, fee, side, amount, wallet, and quote to the current server-side market record.
171
+ 3. Confirm the advertised pool equals `factory.getPool(token, wrappedNative, fee)`.
172
+ 4. Quote on-chain, timestamp it, and reject stale or mismatched quotes.
173
+ 5. Apply a bounded slippage cap and nonzero `amountOutMinimum`.
174
+ 6. Use a short deadline.
175
+ 7. For sells, approve only the exact amount. Never default to unlimited approval.
176
+ 8. Simulate the exact transaction with `eth_call` from the connected wallet.
177
+ 9. Return a wallet-signable transaction. The server never signs.
178
+ 10. Re-quote and re-simulate immediately before preparation, not only when displaying the quote.
179
+
180
+ ## Atomic pre-buy sellability simulation
181
+
182
+ When honeypot APIs do not support a chain, use a matching verified Universal Router to simulate a full buy and sell in one state-free `eth_call`:
183
+
184
+ 1. `WRAP_ETH` into the router.
185
+ 2. `V3_SWAP_EXACT_IN` from WETH to token, recipient router, internal payer.
186
+ 3. `V3_SWAP_EXACT_IN` from token back to WETH using `CONTRACT_BALANCE`.
187
+ 4. Require a minimum WETH balance.
188
+ 5. `UNWRAP_WETH` to the simulated sender.
189
+
190
+ Quote in both directions first and set minimum round-trip return below the no-tax reverse quote by a bounded tolerance. Treat quote failure, transfer failure, insufficient sender balance, router revert, or minimum-return miss as `FAIL`.
191
+
192
+ Chain-specific verified addresses, ABI differences, command bytes, and live probe results belong under `references/`. For Robinhood Chain, load `references/robinhood-chain.md` before implementation.
193
+
194
+ ### Venue-agnostic round-trip sell-sim (any chain, any DEX)
195
+
196
+ When there is no Universal Router to simulate through, get the same signal from
197
+ quotes: **quote the buy, then quote selling exactly what that buy would yield,
198
+ and compare against what went in.** Pass in a quote function so one module covers
199
+ every chain and venue.
200
+
201
+ DEMI asks for this by name ("a sell sim so people don't ape an unsafe contract
202
+ address or honeypot"). It is the single highest-value guard in the product: the
203
+ most expensive failure in memecoin trading is not a bad entry, it is an entry you
204
+ cannot exit, and a honeypot looks identical to a healthy token on a chart *and*
205
+ on a buy-side quote.
206
+
207
+ Four verdicts, never a bare boolean — the caller must be able to show the user
208
+ why:
209
+
210
+ | Verdict | Trigger |
211
+ |---|---|
212
+ | `HONEYPOT` | sell leg fails to quote, returns zero, or round-trip loss ≥ 5000 bps |
213
+ | `CAUTION` | round-trip loss ≥ 1500 bps (high tax or thin liquidity) |
214
+ | `SAFE` | round trip within normal fees |
215
+ | `UNKNOWN` | buy leg unroutable, or unparseable quote output |
216
+
217
+ Rules that matter:
218
+
219
+ - **Sell the tokens the buy actually produced**, not a guessed amount. Assert
220
+ this in a test by capturing the amount passed to the sell leg.
221
+ - **Buy routes + sell won't quote = HONEYPOT**, not `UNKNOWN`. That asymmetry is
222
+ the classic shape.
223
+ - **Fail CLOSED on `UNKNOWN`.** "Could not verify you can exit" must never read
224
+ as "safe to ape". `allowUnknown` exists only for research surfaces where no
225
+ money moves.
226
+ - Garbage quote output (`outAmount: "not-a-number"`) is `UNKNOWN`, never a false
227
+ `SAFE`.
228
+ - Attach the full result to the thrown error (`err.sellSim`) so the refusal
229
+ carries its evidence.
230
+
231
+ ### Structural risk is a SECOND question — run it beside the sell-sim
232
+
233
+ A sell-sim is **behavioural**: it proves the token is sellable *right now*. It
234
+ cannot see a killswitch that has not fired. A token passes a sell-sim perfectly
235
+ while its owner holds `blacklist()`, a mint function, or a fee setter — the
236
+ honeypot is armed, just not triggered yet. So also ask the **structural**
237
+ question: what powers does the deployer still hold?
238
+
239
+ Read bytecode for retained-power selectors (mint / blacklist / pause / fee /
240
+ upgrade) plus proxy implementation slots. Rules that carry the weight:
241
+
242
+ - **Derive selectors with `keccak_256` in code, never transcribe hex.** A wrong
243
+ security constant fails *silently* — it never matches and the guard reports
244
+ clean forever.
245
+ - **Check every proxy slot.** EIP-1967 alone reported **USDC on mainnet as "not
246
+ a proxy"**, because its implementation sits in the older unstructured
247
+ OpenZeppelin slot. Check EIP-1967 + zeppelinos + EIP-1822 and report which
248
+ matched plus the implementation address.
249
+ - **Never return "safe".** Three levels: `unknown-risk` / `elevated` / `high`.
250
+ The best result is "no owner powers found, which is weak evidence, not a
251
+ guarantee" — proxies hide the real logic and absence proves nothing.
252
+ - **An RPC failure is `unavailable`, never clean.** No code at the address is
253
+ `high` (an approval there is unrecoverable). `owner()` alone is `elevated`,
254
+ not `high` — crying wolf on every ordinary token trains people to ignore it.
255
+ - **Take the transport as an option** (`opts.rpcCall`): ESM exports cannot be
256
+ mocked (`Cannot redefine property`), and injection keeps the suite offline.
257
+
258
+ Selector table, proxy slot constants, the NFT drainer-flagging heuristics (the
259
+ marketplace `spam` flag cannot see an unlisted airdrop, and NSFW is not spam),
260
+ and the full registry-wiring checklist for adding a provider:
261
+ `references/structural-contract-and-nft-risk.md`.
262
+
263
+ ## NFT access gate
264
+
265
+ Use direct contract RPC as the source of truth for holder-only dashboard access. Do not use OpenSea for access control, it can lag or fail.
266
+
267
+ ### But DO use OpenSea as the cross-chain NFT *index*
268
+
269
+ Access control and discovery are different jobs. OpenSea v2 indexes NFTs across
270
+ ~23 chains keyed by a chain **slug**, so it is the one place to resolve a bare
271
+ contract address on **any** chain (DEMI: "nfts across all chains should be
272
+ scanned through opensea").
273
+
274
+ Keep a slug↔chainId map (`ethereum:1`, `matic:137`, `base:8453`,
275
+ `arbitrum:42161`, `ape_chain:33139`, `berachain:80094`, `abstract:2741`,
276
+ `ronin:2020`, `zora`, `blast`, `unichain`, `soneium`, `bsc`, … plus `solana`
277
+ with a `null` chainId since it is non-EVM). Expose both directions: the slug is
278
+ what the API path wants, the chainId is what the rest of the agent speaks.
279
+
280
+ Users paste an address without knowing the chain, and guessing wrong is how
281
+ people buy the wrong asset. So fan out across the supported set with bounded
282
+ concurrency (~6) and report **every** hit rather than the first:
283
+
284
+ - `400`/`404` from one chain means "not on this chain" during a fan-out — return
285
+ `null` and keep going, do not abort the scan.
286
+ - one chain erroring must not fail the whole sweep
287
+ - return `{ chainsScanned, found, matches[] }` so "not found anywhere" is
288
+ distinguishable from "we only looked at one chain"
289
+
290
+ Pattern:
291
+ 1. User connects wallet and signs a short-lived nonce challenge.
292
+ 2. Backend verifies the recovered signer.
293
+ 3. Operator allowlist bypasses holder checks for DEMI/admin wallets.
294
+ 4. Everyone else must pass `balanceOf(wallet) > 0` against the configured ERC-721 contract, or `balanceOf(wallet, tokenId) > 0` for ERC-1155.
295
+ 5. Session token stores the access class, e.g. `operator` or `nft-holder`; API routes check the session server-side.
296
+ 6. The locked frontend must not fetch protected scanner/market/risk routes before auth, otherwise expected 401s become noisy console errors and hide real issues.
297
+
298
+ When a specific production wallet needs access without owning the NFT, use a separate access-only allowlist that maps to `nft-holder`, never the development operator bypass. Deployment and verification pattern: `references/production-access-whitelist.md`.
299
+
300
+ Locals Only holder gate reference, pulled from the Polymarket bot UI: HyperEVM chain `999`, contract `0x62FCFAf7573AD8B41a0FBF347AfEb85e06599A75`, name `Locals Only`, symbol `LOCALS`.
301
+
302
+ ## NFT mint gate
303
+
304
+ - Discover ERC-721 and ERC-1155 contracts from canonical RPC logs with a bounded startup window and incremental block cursor; use explorer registries only as optional enrichment.
305
+ - Require verified source and ABI.
306
+ - Allow only recognized quantity functions such as `mint(uint256)`, `publicMint(uint256)`, or `mintPublic(uint256)`.
307
+ - Reject ambiguous `mint(address,uint256)` because the integer may be a token ID.
308
+ - Cap quantity.
309
+ - For payable mints, derive price only from a verified no-argument view function.
310
+ - Enforce gas-war limits before returning any wallet-signable mint tx: total gas spend (`gasLimit * maxFeePerGas` or legacy `gasLimit * gasPrice`) must fit the grant/user cap, and optional per-unit `maxFeePerGas` plus `maxPriorityFeePerGas` tip caps must fail closed when exceeded.
311
+ - Treat mint price/value caps and gas caps as separate money bounds. A mint bot with a max price but uncapped gas is not public-safe.
312
+ - Simulate from the connected wallet before returning the transaction.
313
+ - Never accept raw user calldata.
314
+
315
+ ### A guard nobody calls is not a guard
316
+
317
+ `validateNftMintGasWar()` shipped with a full test suite and **zero call sites** —
318
+ its only appearance outside its own file was a barrel re-export. The caps were
319
+ documented, tested, and enforced on nothing. Tests prove a guard *works*, not
320
+ that it *runs*:
321
+
322
+ ```bash
323
+ rg -n 'guardName\(' src/ bin/ apps/ | grep -v 'export function' | grep -v 'export {'
324
+ ```
325
+
326
+ Zero means the feature is unprotected regardless of its test file. The
327
+ re-export line is what makes a naive grep look healthy.
328
+
329
+ ### Discover the entrypoint; never assume it
330
+
331
+ ERC-721 standardizes ownership and transfer, **not creation**, so every
332
+ collection invents its own entrypoint. Discover it in deployed bytecode; report
333
+ `unsupported` rather than encoding a guess (guessing wrong in a contested mint
334
+ burns gas *and* the allocation), and report **ambiguous** when several match.
335
+ Verified live: Doodles → `mint(uint256)`, BAYC → **none** (it uses `mintApe`),
336
+ which is the proof discovery is reading bytecode rather than hoping.
337
+
338
+ Derive every 4-byte selector with `keccak_256` in code. Two silent bugs in one
339
+ session: `publicMint(uint256)` is `2db11544` (not `26092b83`), and `mintActive()`
340
+ is `25fd90f3` — the value first written, `1249c58b`, is actually **`mint()`**, so
341
+ the mint-open check would have probed a mint *function* as a state flag.
342
+
343
+ ### Bid competitively, but bounded
344
+
345
+ Tips come from `eth_feeHistory` **reward percentiles** (p50/p75/p90 by urgency),
346
+ not `eth_maxPriorityFeePerGas` — one node's opinion hides the distribution, and
347
+ in a gas war the distribution is the signal. Median across blocks so one whale
348
+ cannot skew it.
349
+
350
+ `maxFeePerGas` is a **refundable ceiling** (headroom against a climbing basefee
351
+ costs nothing); the tip is real spend that orders you against competitors. So:
352
+ generous multiplier on the ceiling, disciplined percentile on the tip.
353
+
354
+ When a cap would push the bid below observed market, return `viable: false` with
355
+ the arithmetic — **never silently clamp to a losing number**. Paying gas to lose
356
+ is worse than not bidding.
357
+
358
+ ### Arm only on an exact request
359
+
360
+ Execute requires **all** of: `actionMode` equal to the exact string `"execute"`,
361
+ `ORACLE_AUTONOMOUS_TRADING=1`, and a grant naming this exact contract and chain
362
+ with `maxTotalGasWei`, `maxValueWei`, and a bounded expiry. `"Execute"`,
363
+ `"exec"`, `true`, `1`, `"arm"`, `"fire"` all resolve to `alert_only`. Downgrade
364
+ with a stated reason rather than erroring — an error teaches the caller to retry
365
+ with looser input. Decide arming at creation so a trigger cannot self-upgrade,
366
+ re-check the spend ceiling at fire time, and treat an unreadable sale flag as
367
+ **not open**.
368
+
369
+ Selector tables, the percentile/multiplier table with live values, the
370
+ `mint(address,uint256)` ambiguity conflict, and the registry-wiring checklist:
371
+ `references/nft-mint-discovery-and-gas-bidding.md`.
372
+
373
+ ## OpenSea drop mint + floor sweep gate
374
+
375
+ Marketplace mint/sweep is a second path beside verified-ABI local mints. Same custody model: backend prepares, wallet signs.
376
+
377
+ 1. Keep marketplace API keys server-only (`OPENSEA_API_KEY`). Never `VITE_*` or client bundles.
378
+ 2. Accept only opensea.io collection URLs; parse slug server-side.
379
+ 3. Hard-fail non-target chains (RH bot: Robinhood Chain only). Scanner NFT counts are not proof of mint/sweep readiness.
380
+ 4. Drop mint: build mint tx via marketplace API, validate `to`/hex calldata, simulate with `eth_call` from the connected wallet, return wallet-signable payload only after PASS.
381
+ 5. Floor listings: best listings sorted ascending; filter to target chain + ACTIVE; expose tokenId, orderHash, price, currency for UI caps.
382
+ 6. Floor sweep: cap max items and max price per item (and total spend) before fulfillment. Preview listings against the cap, then request sweep/fulfillment steps.
383
+ 7. Normalize marketplace step blobs into concrete `{from,to,data,value}` txs. OpenSea sweep responses may wrap `from` and `to` as `{ value, chainArch }`, return `value` as decimal wei, and expose the chain under `chainIdentifier.chainId`; unwrap and validate all three before simulation. Reject wrong buyer, wrong chain, non-hex data, invalid targets, or missing steps.
384
+ 8. Simulate every returned sweep step before returning to the client. On multi-step sweeps, client confirms each step in order and waits for receipt between steps.
385
+ 9. Never auto-broadcast. No backend signer. No raw user-supplied marketplace calldata.
386
+
387
+ RH OpenSea endpoint shapes, payment-token native zero-address, and test fixtures: `references/opensea-rh-mint-sweep.md`.
388
+
389
+ For Rootio/Photon-style RH bot scanner + mint UI patterns, including honest `UNKNOWN` treatment for unavailable tax/initial-liquidity data, Quick Buy constraints, gas display, and browser verification, use `references/rhbot-rootio-ui-gates.md`.
390
+
391
+ ## Agent Connect API + MCP (RH AGENT)
392
+
393
+ API keys first, MCP second. Full surface, scopes, caps, and verification: `references/agent-connect-api-mcp.md`. For deploy/regression proof before claiming RH AGENT works, use `references/rh-agent-api-mcp-smoke.md`.
394
+
395
+ For limited automatic execution through a separate capped hot wallet, use `references/rh-agent-capped-hot-wallet-executor.md`. That reference also carries the fail-closed security-review defect taxonomy for auditing an executor diff (kill-switch TOCTOU, uncapped gas, provenance-as-trust bypass, non-atomic caps, replay/idempotency, create-time-only authz).
396
+
397
+ Short rules:
398
+ - Locals/operator session creates hashed `rhag_` keys; raw key shown once.
399
+ - Prepare-only scopes are markets/nfts/positions/watch/prepare/shadow.
400
+ - `agent:execute` must be invalid unless `RH_AGENT_EXECUTE_ENABLED=1`; prod deploys should keep it blocked by default.
401
+ - MCP (`server/mcp.js`) is a thin wrapper over the same REST endpoints; exposing an MCP execute tool does not enable execution unless server policy allows it.
402
+ - Signing stays with the connected wallet, a separate capped session key, or a tiny hot wallet explicitly GO-gated by DEMI. Never use DEMI's main seed/private key.
403
+ - x402 is payments later, not trade authority.
404
+
405
+ ### Multi-user distribution end-state (DEMI product rule)
406
+ Everyone gets agent auto-prepare; **hot wallet is theirs, on their machine** — not the house executor.
407
+ **Users never set up two Hermes profiles** (dual profiles = operator cost plumbing only).
408
+
409
+ **Two website setup paths (AGENTIC TRADING tabs on rhagent.demi.la):**
410
+ 1. **Hosted** — connect wallet → create `rhag_` key → TEST → COPY Hermes/Claude MCP → user signs.
411
+ 2. **Your VPS** — self-host rhbot → local MCP (`RH_AGENT_BASE_URL=http://127.0.0.1:8792`) → same prepare model; optional thin hot wallet on *their* box only.
412
+
413
+ Docs: `/agent/setup.md` (server file `public/agent-setup.md`).
414
+
415
+ **Split buys/sells (same plan house + users + MCP):**
416
+ - `server/split-exec.js`: `planSplitAmounts`, `executeSplitSwap`, `prepareSplitSwaps`.
417
+ - `POST /api/agent/execute-trade` → multi-tx house broadcast; fail mid-split **stops**.
418
+ - `POST /api/agent/prepare-trade` + `POST /api/swaps/prepare` → `mode: prepare-split`, `slices[]`, back-compat `prepared` = slice 1; user signs in order with gap.
419
+ - Exit large sells re-slice when above threshold.
420
+ - MCP `rh_prepare_trade` / `rh_prepare_trade_split` → prepare only (still needs API key to some backend).
421
+ - Env: `RH_SPLIT_ENABLED`, `RH_SPLIT_CHUNKS`, `RH_SPLIT_MAX_CHUNK_ETH`, `RH_SPLIT_GAP_MS`. Multi-tx over time is intentional; single-block multicall is weak/optional.
422
+ - Detail: `references/prepare-trade-response-shape.md` + `references/split-execution-and-setup-paths.md`.
423
+ - Local: `rhag_` + born-encrypted keystore → prepare → sign → broadcast.
424
+
425
+ ## NFT floor exit plans (minimum price + take profit)
426
+
427
+ NFT positions get server-side exit plans evaluated against the live OpenSea floor, same custody model as token TP/SL: the server tracks and flags, the wallet sells.
428
+
429
+ - Plan record: `{ slug, contract?, entryPriceEth, minPriceEth (defaults to entry), takeProfitPct }` → derived `targetFloorEth = entry * (1 + tp/100)`. Reject targets below the minimum.
430
+ - Evaluation against live floor (OpenSea `/collections/{slug}/stats` → `total.floor_price`): `BELOW_MIN` (floor under minimum, refuse to sell), `WAITING`, `TRIGGERED` (floor ≥ target, tell user to list/sell in wallet), `UNKNOWN` when floor is missing/zero — never treat a missing floor as a trigger.
431
+ - Routes owner-bound via `requireUser`; check endpoint filters by actor + `type === "nft_take_profit"` and audits every check. No auto-sale, ever.
432
+ - OpenSea stats + recent mints (zero-address transfers filtered to target chain) are read surfaces exposed to both UI and Agent Connect (`rh_get_nft_stats`, `rh_get_nft_mints`) so an agent can watch floors without a browser tab.
433
+
434
+ Implementation shape: `server/nft-exit-plan.js` (pure build/evaluate, unit-testable), reuses the existing `exitPolicies` store.
435
+
436
+ ## Autonomous SELL execution (exit engine `executeSell` wiring)
437
+
438
+ The token exit engine (`server/exit-engine.js`) ships PREPARE-ONLY — on a stop-loss/TP trigger it prepares the swap and emits `requiresWalletSignature:true`, status `TRIGGERED`; it does NOT sign/broadcast (`executeSell` is passed `null`). To make it truly hands-off (auto sell-back to native ETH on trigger), wire an `executeSell` closure to a NEW sell-only executor (`executePreparedSell` + `assertSellExecutionAllowed`) behind its own flag `RH_AGENT_EXECUTOR_ALLOW_SELL`. This is the ONE legit backend-hot-key unattended path: it's downside protection on a manually-funded, manually-armed, capped, GO-gated position — NOT entry aping (keep buy/sell conceptually split). Full wiring, the sell gate (value==0, skip ETH-cap, keep halt/router/gas/allowlist), the three blocking traps (wallet-alignment, fresh-wallet `kind:"approval"` blocking the first fire, scanner-quoteToken dependency), the rhbot `loadEnvFile`-not-`dotenv` verify gotcha, and the tiny-size live test sequence: `references/autonomous-exit-executor-sell-wiring.md`.
439
+
440
+ ## Merge / branch inventory before "shipped" claims
441
+
442
+ When DEMI asks if NFT/OpenSea work is in and ready to merge:
443
+
444
+ 1. Diff `main` against feature branches (`origin/feat/*`), not only the current worktree.
445
+ 2. Grep for mint + floor/sweep/fulfillment paths. Mint-only hardening is not floor sweep.
446
+ 3. State explicitly what would be left out if current `main` were committed/pushed alone.
447
+ 4. Prefer integrating missing gates into the active UI branch over claiming a sibling branch is included by adjacency.
448
+ 5. For RH AGENT main claims: confirm gate + Agent Connect + shadow agent + `rhagent.demi.la` prod markers, not only the open feature branch.
449
+
450
+ ## Real-time discovery and conditional execution
451
+
452
+ A live scanner and a live executor are separate authority domains.
453
+
454
+ - Detect launches from factory events at block cadence, dedupe by transaction hash plus log index, then enrich through the normal scanner before presenting executable data.
455
+ - Push launch and refresh signals to clients with SSE or WebSocket, but retain bounded periodic snapshot polling as fallback.
456
+ - Persist conditional orders with token contract, side, target price, amount, actor, and explicit execution mode.
457
+ - Browser-evaluated limits and TP/SL are **tab-armed** only. Say clearly that closing the tab disables trigger handling.
458
+ - A trigger opens a fresh trade ticket. Re-quote, re-run sellability/simulation, enforce slippage and size limits, then require Rabby confirmation.
459
+ - Never describe tab-armed triggers as unattended execution. Unattended execution requires an audited conditional-order contract or a restricted smart-wallet/session-key permission path. A backend hot key is not an acceptable shortcut.
460
+ - Persist TP/SL entry price and token contract with each rule; percentages without an entry reference cannot be evaluated correctly.
461
+ - Deduplicate triggered rule IDs client-side so each price update does not reopen the same ticket.
462
+ - A `BLIND APE` UX may skip manual evidence review, but it must never bypass route membership, reverse quote/sell simulation, position caps, slippage caps, or wallet confirmation.
463
+
464
+ Implementation pattern and DexScreener embed caveats: `references/realtime-conditional-orders.md`.
465
+
466
+ RPC-native NFT and V3 factory discovery, live proof layers, marketplace runtime verification, temporary smoke-access cleanup, and per-source freshness: `references/rpc-native-discovery-and-live-proof.md`.
467
+
468
+ ## Extensible multi-chain scanners (chain = config, not code)
469
+
470
+ When the ask is "do we have a scanner for every chain", the answer is a **framework**,
471
+ not N hand-wired integrations. Define a capability contract, implement it once
472
+ generically over standard JSON-RPC, and let a new chain register from a config object.
473
+
474
+ Two rules keep it honest:
475
+
476
+ - **Unimplemented ≠ faked.** An unsupported capability throws naming what *is*
477
+ supported; it never returns `undefined` a caller reads as a negative result.
478
+ - **Capabilities that need a verified per-chain router (`quote`, `sellSimulation`,
479
+ `prepareUnsignedTx`) stay ABSENT until provenance is recorded.** A chain with no
480
+ verified venue is fail-closed for routing value — read/research still works. That is
481
+ a safe state to advertise, not a gap to apologize for.
482
+
483
+ Ship ONE chain wired end-to-end as the reference so the pattern is copyable, and leave
484
+ the rest at the generic tier rather than bulk-adding unverified routers.
485
+
486
+ Full contract, the `this`-binding trap that silently blinds `scoreRisk`, evidence/risk
487
+ label semantics, round-trip retention thresholds, the `getAmountsOut` array-decode
488
+ trap, and the non-yielding slippage ceiling: `references/extensible-chain-scanner.md`.
489
+
490
+ ### Venue adapters are per-AMM-shape, never one bent to fit
491
+
492
+ V3 is not V2 with different addresses: no `getAmountsOut`, prices through a separate
493
+ Quoter, needs an explicit fee tier per hop. Encoding a V3 swap with V2 assumptions
494
+ yields a tx that reverts — or worse, routes through the wrong pool at a price nobody
495
+ quoted. Write a **sibling** adapter and assert separation on the *selector*
496
+ (`d06ca61f` vs `c6a5026a`), which cannot appear in prose by accident.
497
+
498
+ Select by declared shape and require the whole set: a V3 chain needs **both** quoter
499
+ and router. A router alone stays fail-closed — it could encode a swap with no priced
500
+ expectation to guard against, which is exactly the state that produces an unbounded
501
+ fill.
502
+
503
+ **Search every fee tier (100/500/3000/10000) and keep the best.** Load-bearing, not
504
+ thoroughness theatre: liquidity concentrates in one tier and which tier varies by pair
505
+ and chain. Live on Arbitrum WETH→USDC the 500 tier returned 1908.72 vs 1862.15 on the
506
+ 100 tier — 2.4% a hardcoded 0.3% default would have eaten.
507
+
508
+ ### Verify a venue functionally — codesize proves deployment, not identity
509
+
510
+ Ask the candidate to **price a pair whose answer you can sanity-check**. A contract
511
+ that correctly quotes WETH→USDC *is* a working quoter, whatever any doc claims.
512
+
513
+ The canonical mainnet QuoterV2 address returns ~2109 bytes of bytecode on Base — some
514
+ contract, just not one that can price that chain's pairs. A codesize check allowlists
515
+ it; the functional probe caught it and pointed at Base's real quoter (8273 bytes).
516
+
517
+ Commit the prober as a re-runnable script (`npm run verify:venues`), not a one-off,
518
+ and record provenance as `{ method, source, date, chainId }` — with a test asserting
519
+ `verified.chainId` matches the chain it is listed under. An address verified on
520
+ Arbitrum is not verified on Ethereum, even when the deployment shares an address.
521
+
522
+ ## Best-execution routing (cheapest swap / bridge)
523
+
524
+ Cheapest ≠ highest quote. Rank on **net received after gas and fees**, querying every
525
+ source in parallel. Gas is part of the price, and the crossover between "better quote"
526
+ and "cheaper gas" moves with trade size — so any fixed preference is wrong on one side
527
+ of it. Intent venues (CoW) where a solver eats the gas routinely win on net while
528
+ losing on gross.
529
+
530
+ Unknown cost is **never** scored as zero — that is how the worst route wins a
531
+ comparison. Mark it, rank on gross, warn. Quote a spread only when the top two routes
532
+ measure cost the same way, and name the basis.
533
+
534
+ Per-source cost-semantics table, the spread-basis bug caught live, source-outage
535
+ handling (Odos 410 mid-build), bridge duration reporting, and the deterministic test
536
+ targets: `references/best-execution-routing.md`.
537
+
538
+ ### Hand the winner to prepare — a comparison is not a signature
539
+
540
+ A ranked winner the user must rebuild by hand is a leak, not a feature. Close the loop:
541
+ **compare → pick → re-quote → build the signable artifact**, so "best route" and "the tx
542
+ you sign" are provably the same route.
543
+
544
+ Two rules dominate:
545
+
546
+ - **Artifact kinds are not interchangeable.** `unsigned-transaction` (LI.FI, ParaSwap,
547
+ 0x) gets broadcast; `typed-data-order` (CoW) is signed and submitted to an order API
548
+ with **nothing to broadcast**. Return `artifactKind` and branch on it — conflating
549
+ them fails at *signing time, after tokens were already approved*.
550
+ - **`requiresApproval.spender` is not `destination`.** CoW pulls funds through the
551
+ **vault relayer**, not the settlement contract; approving the wrong one yields an
552
+ order that silently never fills. Read the provider's field, never derive it.
553
+
554
+ Prepare must **re-quote** (the comparison is already stale) and report `driftBps` against
555
+ a bounded tolerance — zero tolerance fails constantly between blocks, unbounded silently
556
+ prepares a worse trade than the one agreed to.
557
+
558
+ Validate the taker locally: reject missing, malformed, and placeholder/burn addresses
559
+ before any network call. **Quoting is anonymous; preparing is not.** ParaSwap rejects
560
+ `0x…dEaD`; other venues happily build a transaction for an address nobody controls, so
561
+ relying on the venue means relying on the least careful one.
562
+
563
+ Some venues refuse to build calldata until the approval **already exists on-chain**
564
+ (ParaSwap: `Not enough WETH allowance given to TokenTransferProxy`). That is a genuine
565
+ ordering constraint — classify it (`approval-required-first`) rather than surfacing a
566
+ bare HTTP 400 that reads like the router is broken. Read `err.body`, not just
567
+ `err.message`: the status line alone flattens every venue rejection into a generic
568
+ provider error.
569
+
570
+ Never dead-end and never substitute silently: an unpreparable winner names an
571
+ `alternative`, and a forced non-winner is flagged `wasWinner: false`.
572
+
573
+ Full artifact table, the CoW relayer assertion, error-classification pattern, drift math,
574
+ and the key-gate test trap that exposed an unreachable preparer:
575
+ `references/route-prepare-handoff.md`.
576
+
577
+ ### Bridge prepare: the artifact is a sequence, and confirmation is not arrival
578
+
579
+ Same compare→prepare shape, but every bridge failure is **unrecoverable** — funds leave
580
+ the origin chain before anything confirms on the destination. Four rules:
581
+
582
+ - **Always return a LIST of transactions**, even when there is one. Some routes need an
583
+ approval *and* a deposit; a bare object for one source and a list for another
584
+ guarantees someone signs the first item and believes they are done — **approved, not
585
+ bridged**. State the count, and that multi-tx routes must be signed in order.
586
+ - **Echo both chains.** Transactions execute on the origin and credit the destination;
587
+ showing only the signing chain hides half the trust decision. **Refuse** a tx whose
588
+ `chainId` ≠ origin (`chain-mismatch`) rather than letting a wallet prompt on the wrong
589
+ network.
590
+ - **Say "not atomic" unprompted.** Origin confirmation does not mean funds arrived. This
591
+ is the most common bridging panic and silence causes **double-sends** — ship the ETA
592
+ and an explicit "do not re-send while pending".
593
+ - **Duration is reported, never scored.** Saving $2 over 30 minutes vs paying $2 more to
594
+ land in 20 seconds is the caller's trade-off. Assert duration never enters score math.
595
+
596
+ **Every fact stated in prose that a caller may act on needs a structured field carrying
597
+ it.** A `no-prepare-path` branch named the fallback in its message but returned
598
+ `alternative: undefined` — and that branch fires often, because Across wins on gross
599
+ (unreported gas) yet cannot be prepared. An agent must not have to regex an error string.
600
+
601
+ ### Never brand a public artifact with a private name
602
+
603
+ A prepared transaction carried `madSlippage` — the *private* codebase's name — in the
604
+ payload handed to users (16 occurrences, 14 files). This leak class is invisible to a
605
+ secret scan (no key material), a portability test (no path), and a docs test (not prose):
606
+ it lives in the **output payload shape**. Audit field names in anything a stranger
607
+ receives, not just source comments and env vars.
608
+
609
+ Migrate with a **dual-read window**, never a hard cut: producers emit only the neutral
610
+ name, consumers accept `new ?? legacy` (an artifact prepared *before* the rename must not
611
+ fail its guard *after* it), and keep one legacy-named fixture so back-compat stays
612
+ exercised rather than assumed.
613
+
614
+ ## Non-EVM prepare lanes (Solana, HyperCore, Bitcoin)
615
+
616
+ Same custody model, different artifact. A Jupiter or Magic Eden prepare is a base64
617
+ **serialized Solana transaction**; a HyperCore stake/delegate/unstake is **EIP-712
618
+ typed data** signed with an EVM key and POSTed to `/exchange` (there is no chain to
619
+ broadcast to — never call it "broadcast"); Bitcoin is a PSBT. Returning one generic
620
+ "prepared" blob across these is the same category error as conflating an unsigned tx
621
+ with a CoW order.
622
+
623
+ Two rules that carry most of the weight:
624
+
625
+ - **Prepare is not proof; simulate is.** Jupiter builds a valid-looking swap for a
626
+ placeholder pubkey and it fails one step later at
627
+ `simulateTransaction: ... failed to sanitize accounts offsets correctly`. An E2E that
628
+ stops at "returned base64" proves nothing — assert `err === null` from a real
629
+ simulate against a real funded pubkey, parameterized by env.
630
+ - **Enforce prepare-only structurally.** Scan the module's own export names for
631
+ `submit|broadcast|send|sign` and fail the test if one exists, rather than trusting
632
+ convention. (Watch the `/sign/i` vs `HL_SIGNATURE_CHAIN_ID` false positive — use an
633
+ explicit allowlist, not a lookahead.)
634
+
635
+ Provider auth tiers probed live, the Magic Eden cap-before-network-call test, HYPE's
636
+ 8-decimal wei and the two-bucket stake/delegate/undelegate/unstake sequence with its
637
+ 7-day queue, and the public-constant test-fixture rule:
638
+ `references/non-evm-prepare-lanes.md`.
639
+
640
+ ### Ordinals inscriptions: a confirmed transaction is not a working inscription
641
+
642
+ The hardest Bitcoin failure class is one where **everything reports success**. The
643
+ broadcast returns 200, the tx confirms in a block, your local content hash matches
644
+ byte-for-byte — and the inscription is dead. It is valid Bitcoin that simply is not
645
+ an inscription, and nothing in the send path can tell you.
646
+
647
+ Three encoding bugs produced exactly that on one release, each cheap to detect
648
+ locally and expensive to detect on-chain:
649
+
650
+ - **A spendable taproot internal key means the envelope never enters the witness.**
651
+ `p2tr(xOnly, {script: leaf})` lets the signer take the cheaper key path, so the
652
+ body is never revealed and the node rejects it as an *invalid Schnorr signature* —
653
+ which reads as a signing bug and is not one. Ordinals need a **NUMS (unspendable)**
654
+ internal key: `p2tr(undefined, {script: leaf})`. **The tell is `vsize`**: a 28KB
655
+ payload cannot fit in 111 vbytes, so check the size before debugging the signature.
656
+ - **`allowUnknownInputs: true` is required to finalize.** An ordinals envelope is not
657
+ a standard script template, so `@scure/btc-signer` classifies the leaf as `unknown`
658
+ and throws `Finalize: Unknown tapLeafScript`. `allowUnknownOutputs` alone is not
659
+ enough — different flags, different sides of the transaction.
660
+ - **The body tag is a bare `OP_0` (`0x00`), not a 1-byte push of zero.**
661
+ `encodePush([0])` emits `01 00`; ord reads that as an unknown tag, skips it, and
662
+ records an inscription **with an empty body**. This one confirms *and indexes*, so
663
+ the only symptom is a blank render and `content size: 0 bytes`.
664
+
665
+ **Diff against a live indexed inscription rather than reading the spec.** Pulling a
666
+ recently-indexed reveal off mainnet settles in one command what spec-reading argues
667
+ about for an hour.
668
+
669
+ **Gate the reveal on envelope structure before it spends anything** — assert the
670
+ content-type tag is `0101`, the body tag is `0x00`, `vsize > 1000`, and the body head
671
+ appears in the raw hex. Then verify *after* broadcast by pulling the tx back and
672
+ reassembling the body from the witness pushes, never by re-hashing your local file:
673
+ that matched perfectly through both dead attempts.
674
+
675
+ Funds at a superseded **key-spendable** commit are recoverable via the key path by
676
+ rebuilding the *original* leaf encoding — assert the derived address equals the funded
677
+ one before signing. A NUMS commit is not recoverable (that is the point), so size its
678
+ output at exactly `revealFee + 546`.
679
+
680
+ Byte-level envelope layout, the `finalizeIdx` source that mandates
681
+ `allowUnknownInputs`, the reassembly parser, the recovery script, and why the
682
+ operator's "0 bytes" report *was* the diagnosis:
683
+ `references/ordinals-inscription-envelope.md`.
684
+
685
+ ## Pre-publish security audit (before a repo goes public)
686
+
687
+ Run an adversarial review with **several independent model lineages** before
688
+ open-sourcing anything that moves money. One model is a spot check; three is an
689
+ audit. Agreement across lineages is the strongest evidence a finding is real
690
+ (Grok and Opus independently reported the same three CRITICALs), and each lineage
691
+ catches defects the others miss (Fable alone found a capless transfer guard that
692
+ authorized `MaxUint256 - 1`).
693
+
694
+ Mechanics that matter: dispatch a **model** through a neutral profile
695
+ (`hermes -p <neutral> -m <model-id>`), because a dispatch-only profile's SOUL will
696
+ refuse and try to hand the audit to a bus instead; the brief must **forbid
697
+ delegation** or subagents time out and return an empty log; run in background
698
+ with `notify_on_complete`, since a real audit takes 20-45 minutes.
699
+
700
+ **Provider safety filters block adversarial framing.** OpenAI-family models
701
+ refuse with a cyber-risk flag — that is provider policy, not a harness bug, so do
702
+ not debug the gateway. Reframe honestly as *pre-release engineering review of my
703
+ own first-party code*, ask about **defect classes** ("input shapes where a limit
704
+ is silently not applied") rather than exploits, and drop the words *attacker*,
705
+ *exploit*, *bypass*, *drain*. If it still refuses, that lane is unavailable —
706
+ say so and proceed with the lanes that answered.
707
+
708
+ **Verify every fix by execution**, one script printing `HOLDS | BROKEN` with
709
+ observed evidence per finding. This caught a wrong function name in the probe
710
+ itself; an unverified fix and an unverified probe look identical on paper. Assert
711
+ the optimization still survives alongside the fix (dedupe still collapses
712
+ same-credential calls after the cache key was hardened).
713
+
714
+ **Report findings without loss verbs.** Writing that a hostile object "drained
715
+ Satflow, Magic Eden, 1inch, 0x, OpenSea" made DEMI ask *"wait they drained us?"* —
716
+ these were code defects found by reading source, not incidents. Say "would have
717
+ allowed" / "was reachable in code", state up front that nothing was exploited,
718
+ name why each finding could not have fired (a cross-tenant cache leak needs two
719
+ concurrent users with different keys; a private repo has one), and **verify
720
+ balances live and show the numbers** the moment the owner sounds alarmed — before
721
+ any explanation. Finding these while the repo is still private is the system
722
+ working, not failing.
723
+
724
+ Full brief template, reframing language, verification pattern, and the HMAC
725
+ retro-fit note (re-sign mutations so bound-tests still test bounds, and add one
726
+ unsigned-mutation test for forgery): `references/prepublish-multimodel-audit.md`.
727
+
728
+ Round-7 concrete repros (allowance decode parity, fresh-window on approvals,
729
+ approvalLike dest allowlist, mandatory slippage HMAC, HL action allowlist,
730
+ keyFile non-leak) and the test-secret pinning pattern:
731
+ `references/oracle-round7-guard-parity.md`.
732
+
733
+ ### When your own scanner flags your own public output, change the OUTPUT
734
+
735
+ A public-plane response scanner returning 500 `secret-leak-blocked` on a route
736
+ that ships no secret is a signal about the payload, not a reason to soften the
737
+ rule. The audit chain's genesis hash (64 zero hex chars) tripped
738
+ `raw-32-byte-hex-bare`, and it was right to: that shape is indistinguishable from
739
+ a private key.
740
+
741
+ Wrong fixes, in ascending order of damage: add the field to the hex64 allowlist
742
+ (converts a control into a formality, and the next value under that name is
743
+ unchecked forever); special-case "all zeros is fine" (paddable — invites a
744
+ "mostly zeros" rule a real key can satisfy); `0x`-prefix it (fails
745
+ `raw-32-byte-hex-key`, correctly, since that is the canonical key shape).
746
+
747
+ Right fix: **stop shipping a key-shaped value.** Genesis carries zero entropy, so
748
+ describe it (`genesisBytes: 32`, `genesisDescription: "32 zero bytes"`) and let
749
+ the integrator reconstruct it. Leave a comment saying why, or a later session
750
+ "simplifies" it back. Ask in order: does the consumer need this exact value or a
751
+ description; can it be a length/boolean/label/prefix instead; and only then, is
752
+ the field name allowlist-worthy on its own merits independent of today's
753
+ inconvenience.
754
+
755
+ **Keep both scan layers** — they disagreed here. The graph walk gives the
756
+ actionable path (`$.audit.genesis`); the serialized scan is authoritative because
757
+ `toJSON()`, getters, symbol keys and BigInt coercion put bytes on the wire the
758
+ graph never visits. One intermediate attempt passed serialized while still
759
+ failing graph.
760
+
761
+ **Dogfooding a secret scanner over first-party source returns false positives, and
762
+ you must say so.** 67 files produced 7 findings, all benign on inspection: a
763
+ zero-address constant, an ERC1967 bytecode fragment, and — four times — the
764
+ scanner's own rule definitions (`{ rule: "keystore-path", re: /keystore/i }`).
765
+ Same class as a forbidden-token test that scans its own test file: exclude the
766
+ rule-defining files or strip obvious regex sources first. Report the real
767
+ false-positive rate rather than "7 blockers found", and do not tune the scanner
768
+ into silence either — the public-boundary scan (0 findings) was the check that
769
+ actually mattered and it was clean.
770
+
771
+ Full worked case, the two-layer disagreement, and the false-positive table:
772
+ `references/secret-scanner-flags-own-output.md`.
773
+
774
+ ## Venue datapoint joins (derive once, not per surface)
775
+
776
+ When a venue's read API returns **parallel arrays** and stringly-typed numbers
777
+ (Hyperliquid `metaAndAssetCtxs` → `[{universe}, [ctx…]]`, joined by index), every
778
+ chart, card and alert ends up re-deriving the same math and they drift. Do the
779
+ join **once** in a provider module and expose flat rows.
780
+
781
+ The derivations worth centralizing, because they are the ones people get wrong:
782
+
783
+ - **Index-joined metadata + context.** `universe[i]` pairs with `assetCtxs[i]`;
784
+ leverage/decimals come from one, price/OI/funding from the other.
785
+ - **Open interest is denominated in the COIN.** Multiply by mark for USD.
786
+ - **Funding is a per-interval decimal** (8h on HL). Annualize explicitly
787
+ (`rate * (8760/8) * 100`) and return **both** forms — consumers reach for APR
788
+ and silently mislabel the raw rate as one.
789
+ - **Filter delisted markets** out of boards and totals.
790
+ - **Missing numeric → `null`, never `NaN`.** A sparse context with no
791
+ `prevDayPx` must yield `change24hPct: null`, not a value that renders as
792
+ `NaN%`.
793
+
794
+ Ship the board cuts (`gainers`/`losers`/`byVolume`/`byOpenInterest`/funding
795
+ extremes) from the same joined rows so a leaderboard can never disagree with the
796
+ market table. Test against **fixed fixtures**, asserting the arithmetic
797
+ (10 coins @ $100 → `1000` OI USD; `0.0001` per 8h → `10.95%` APR) rather than
798
+ live values.
799
+
800
+ **Read lanes are not trade lanes — say so unprompted.** Hyperliquid `/info`
801
+ gives markets, positions, fills and book; **placing a perp order is a separate
802
+ action set** (`order`, `cancel`, `modify`, `updateLeverage`) EIP-712-signed to
803
+ `/exchange`. An `/exchange` URL constant existing in a staking module does not
804
+ mean orders are wired. When the owner asks "can we trade perps?", grep for the
805
+ order-placement function and answer from that, not from the presence of a venue
806
+ integration.
807
+
808
+ ### Perp order lanes (limit / market / leverage / brackets)
809
+
810
+ When that answer is "no" and the owner asks for it, the trade lane is its own
811
+ prepare-only module. Two things carry most of the risk:
812
+
813
+ - **Venue precision is a correctness requirement, not tidiness.** Hyperliquid
814
+ rejects over-precise orders, and a rejected order is indistinguishable from a
815
+ missed fill at the moment it matters. Price obeys **both** a 5-significant-figure
816
+ cap and a `6 − szDecimals` decimal cap (tighter wins; integers are exempt);
817
+ size rounds to `szDecimals`. Round in exact decimal with `BigInt`, never through
818
+ a float, and **throw** when a size rounds to zero rather than sending it.
819
+ - **There is no market order type.** A "market" order is an IOC limit priced
820
+ aggressively off mark (`mark * (1 ± bps/10_000)`), so it *requires* a live mark
821
+ price and reuses the house **100 bps** slippage ceiling.
822
+
823
+ Leverage is bounded by the asset's `maxLeverage`, cross vs isolated is explicit
824
+ (never a silent default that hides which balance is at risk), and above ~20x
825
+ attach the liquidation distance — `100 / leverage` ≈ the adverse move that
826
+ liquidates, before fees. Bracket protection legs must be **reduce-only and on the
827
+ opposite side**; a non-reduce-only leg opens a *new* position on trigger.
828
+
829
+ Wire shapes, the rounding table with observed values, asset-index resolution, the
830
+ full guardrail list, and the fixture trap (asserting sig-figs with `szDecimals=5`
831
+ actually exercises the decimal limit): `references/hyperliquid-perp-orders.md`.
832
+
833
+ ### Crossing into live execution (sign + submit)
834
+
835
+ When the owner asks to **trade, not prepare**, the signing lane is its own
836
+ provider — not a flag on the prepare module. Keep the prepare modules free of
837
+ write exports and add one execution provider beside them.
838
+
839
+ Three things carry the risk:
840
+
841
+ - **Verify the signer against the venue's own known-answer vector, and PULL that
842
+ vector from the SDK source rather than recalling it.** An unverified signer
843
+ produces orders the venue silently rejects, which is indistinguishable from
844
+ "not trading". A first attempt here failed against a fixture reconstructed from
845
+ memory — the implementation was correct and the *expected value* was invented;
846
+ fetching the real `signing_test.py` fixture matched immediately.
847
+ - **A transport `200` is not a fill.** Hyperliquid returns `status: "ok"` while
848
+ individual entries in `response.data.statuses[]` carry rejections. Surface the
849
+ venue's error and return **no order id** rather than inventing one.
850
+ - **The key never appears in a returned object** — assert it with a
851
+ `JSON.stringify(result).includes(...)` test. The derived address is fine to
852
+ surface; the material is not.
853
+
854
+ Give execution **its own provider id** and exempt exactly that id from the
855
+ write-op denylist, so the exemption is one greppable line rather than a weakened
856
+ predicate — and assert the boundary on a **structured** catalog field
857
+ (`execution: "live"`), never a regex over free-text notes the catalog projection
858
+ may not even expose.
859
+
860
+ When a prepare-only project grows an exec lane, grep the docs for "never signs"
861
+ and **scope** the claim (routing still never signs) instead of leaving a blanket
862
+ promise that is now false in one module.
863
+
864
+ Action-hash construction, the phantom-agent EIP-712 envelope, the msgpack
865
+ key-order dependency, key resolution, and venue-verdict parsing:
866
+ `references/hyperliquid-live-execution.md`.
867
+
868
+ ## Testing security guards that live in the transport
869
+
870
+ Two traps make a security test **pass while the hole is open**:
871
+
872
+ - **`fetch()` rewrites the `Host` header from the URL.** A DNS-rebinding test
873
+ written with `fetch(url, { headers: { Host: 'attacker.example' } })` silently
874
+ sends the loopback Host, gets `200`, and reports the guard broken — or worse,
875
+ reports it working when it is not. Use a raw `node:net` socket and write the
876
+ request line and headers verbatim.
877
+ - **A spawned server hangs the runner.** Give the test an explicit
878
+ `{ timeout: N }`, spawn `detached: true` and kill the **process group**
879
+ (`process.kill(-proc.pid)`) — an orphan keeps the port and the next run blocks
880
+ on startup. And pass the server's **actual** env var names: a generic `PORT`
881
+ that the server reads as `ORACLE_DATA_PORT` starts it on its default port and
882
+ the readiness poll never succeeds, which reads as "server is broken."
883
+
884
+ ### `assert.throws` on an ASYNC function asserts nothing
885
+
886
+ `assert.throws(() => asyncFn(...), /pattern/)` **always fails** — the call returns
887
+ a promise rather than throwing, so the assertion is testing the wrong thing while
888
+ appearing to cover the guard. Found live: a 0/0 unprotected-LP-exit guard whose
889
+ implementation was correct the whole time, but whose test had never actually
890
+ exercised it.
891
+
892
+ ```js
893
+ // WRONG: the guard is never asserted; the rejection is unhandled
894
+ assert.throws(() => lpPrepareDecrease({ amount0Min: "0", amount1Min: "0" }), /unprotected/);
895
+
896
+ // RIGHT
897
+ await assert.rejects(() => lpPrepareDecrease({ ... }), /unprotected/);
898
+ ```
899
+
900
+ Grep the suite for `assert.throws` whose callee is `async` after adding any
901
+ `await` to a guard path. A test that has never seen its guard fire is not
902
+ coverage, and this shape hides that fact.
903
+
904
+ ### Two test files binding the same port collide under a concurrent runner
905
+
906
+ `node --test` runs FILES in parallel. Two suites that each `spawn` a server on a
907
+ hardcoded `PORT = 18801` pass individually and fail together — one process loses
908
+ the bind, and its tests surface as `404`/`ECONNREFUSED` assertion failures that
909
+ read exactly like a broken product route.
910
+
911
+ **The tell: green in isolation, red in the full run.** That combination is almost
912
+ never a product bug. Give every server-spawning test file a distinct port and
913
+ comment why. Verify the same way you found it — run the file alone, then run the
914
+ full suite, and require both green.
915
+
916
+ ## Risk coverage
917
+
918
+ Do not call holder concentration bundle detection. Bundles require launch-block transaction clustering and common-funder evidence. Label each check `LIVE`, `STALE`, `UNKNOWN`, or `UNAVAILABLE`, include timestamps and limitations, and distinguish near-real-time polling from block-by-block surveillance.
919
+
920
+ ## Public data-plane HTTP hardening
921
+
922
+ A self-hosted scanner runs on free public venue APIs, where the two dominant failures
923
+ are rate limiting and one slow venue poisoning a whole health sweep. Fix both in the
924
+ shared HTTP helper, not in thirty providers: bounded retry that honours `Retry-After`
925
+ and **never retries a 4xx**, single-flight dedupe so N concurrent identical GETs cost
926
+ one upstream call, and `mapLimit` fan-out (~8) instead of `Promise.all` across every
927
+ provider. Dedupe is a request collapser, not a cache — no TTL, so quotes and balances
928
+ never go stale. Prove dedupe by counting upstream calls, never by timing.
929
+
930
+ Full rules, the key-shape that keeps keyed and keyless callers separate, and the
931
+ pitfalls: `references/public-data-plane-hardening.md`.
932
+
933
+ ## Verification
934
+
935
+ - Unit-test route mismatch, stale quote, slippage overflow, exact approval, deadline, simulation revert, ambiguous mint ABI, unsupported fee paths, OpenSea non-RH chain rejection, max-price sweep block, and mint/sweep simulation failure.
936
+ - Run a live read-only quote against a real pool.
937
+ - Run one live atomic round-trip `eth_call` against a known executable pool and one failing token fixture.
938
+ - Verify the merged server boots, not only frontend build/tests. Confirm cwd/port is the worktree under test, not a stale sibling checkout.
939
+ - For public unsigned BFFs beside an executor, run them as a separate loopback-only service with execution envs explicitly off; verify the public/unsigned headers and prove executor routes 404/405 on that public port before claiming the self-custody boundary is enforced.
940
+ - When another coequal agent intentionally lands a private execution plane in the same repo, do not delete those files as "pollution" by default. Reconcile on a fresh branch from the integrated base, preserve both lanes, and enforce the custody boundary instead: public Oracle/BFF/UI code must not import private signer/executor/Solana execution modules.
941
+ - Browser-test wallet discovery, chain switching, quote, approval, swap, local mint, OpenSea load, and sweep UI states without broadcasting funds during QA.
942
+ - Before merge-ready claims for NFT scope: inventory sibling branches and confirm mint + floor-sweep paths exist in the tree being pushed.
943
+
944
+ ## Pitfalls
945
+
946
+ - Paper endpoints presented as execution.
947
+ - Any-wallet sessions accidentally inheriting operator/admin powers.
948
+ - Trusting a named explorer contract without checking constructor-bound factory/WETH.
949
+ - Testing only the buy route and calling the token safe.
950
+ - Client-supplied market objects or calldata reaching the signer unchanged.
951
+ - Treating a successful quote as proof that token transfers or sells work.
952
+ - Claiming OpenSea mint/sweep shipped because a scanner shows NFT collections or a sibling branch is nearby.
953
+ - Merging mint-only OpenSea hardening and calling floor sweep done.
954
+ - Putting marketplace API keys in `VITE_*` or frontend bundles.
955
+ - Accepting marketplace fulfillment steps without per-step simulation and spend caps.
956
+ - Calling an SSE `ready` handshake proof that a real factory launch was observed.
957
+ - Calling browser-only limit or TP/SL monitoring unattended execution.
958
+ - Claiming a DexScreener graph is working because the iframe shell loads when no candles render.
959
+ - Broad wallet-trigger selectors such as `[aria-expanded="true"] span` rotating provider/balance text when connected-state markup wraps content in a span. Give the chevron a semantic class, rotate only that class, and regression-test the selector plus the open state.
960
+ - Testing only logged-out wallet UI. Connected/open states may render different wrappers, balances, provider names, and transforms; inspect computed styles for every child and verify the deployed asset hash when PWA caching or concurrent branches are possible.
961
+ - Capping only `tx.value` and calling the executor "capped." Gas is separate money — unbounded gasLimit/fee drains the wallet outside the ETH caps. See the executor security-review taxonomy.
962
+ - **Shipping a guard with zero call sites.** A green test file proves the guard
963
+ works, not that anything invokes it — `validateNftMintGasWar` had a full suite
964
+ and was enforced on nothing. Grep for real callers, excluding the definition
965
+ and barrel re-exports (the re-export is what makes a naive grep look healthy).
966
+ - Assuming a mint signature instead of discovering it from bytecode. ERC-721 does
967
+ not standardize creation; BAYC uses `mintApe(uint256)`. Return `unsupported`
968
+ rather than encoding a guess, and report ambiguity when several selectors match
969
+ instead of silently taking the first.
970
+ - Encoding `mint(address,uint256)` as a quantity mint. The uint256 may be a
971
+ **tokenId** — this is why the mint gate rule rejects the signature outright.
972
+ Its presence in an inherited selector table is not validation of the shape.
973
+ - Reading gas-war tips from `eth_maxPriorityFeePerGas`. It is one node's opinion
974
+ with no distribution; a contested inclusion needs `eth_feeHistory` reward
975
+ percentiles, medianed across blocks so one whale cannot skew the read.
976
+ - Treating `maxFeePerGas` as spend. It is a refundable ceiling — padding it is
977
+ free protection against a climbing basefee, and being stingy strands you on a
978
+ spike. The tip is the real money.
979
+ - Clamping a bid below the observed market and returning it anyway. Report
980
+ `viable: false` with the arithmetic; paying gas to lose is worse than not
981
+ bidding. And never let a clamp produce `maxFeePerGas < maxPriorityFeePerGas`.
982
+ - Arming an unattended action from a truthy or near-miss value. Exact string only
983
+ — `"Execute"`, `"exec"`, `true`, `1`, `"arm"` must all downgrade, and an
984
+ unrecognized mode should downgrade *with a reason* rather than error, since an
985
+ error teaches the caller to retry with looser input.
986
+ - Deciding arming at fire time rather than creation (a trigger that can
987
+ self-upgrade), accepting a grant for a different contract, or skipping the
988
+ fire-time spend re-check — gas moved between arming and firing, which is the
989
+ entire premise.
990
+ - Treating an unreadable sale flag as open. A probe that timed out is `unknown`;
991
+ firing on it is guessing in the most expensive direction.
992
+ - Trusting scanner-window membership as an allowlist for money movement, or reading a kill switch once at handler entry and broadcasting on that stale value seconds later.
993
+ - Local smoke against a stale server: a leftover `node server.js` from an earlier session can own the smoke port and serve OLD routes — new endpoints return 404/`Cannot POST` and look unimplemented. Before diagnosing "route missing", check the background process log for `EADDRINUSE` and `lsof -t -i :<port>` for a stale PID; kill it and re-smoke.
994
+ - Wiring an autonomous SELL and reusing the BUY executor: the buy gate hard-rejects `side !== "buy"` and asserts `value > 0`, so a sell (value==0) needs a PARALLEL sell-only gate — do not weaken the buy gate. And the exit policy's recorded `wallet` (user session) must be realigned to the EXECUTOR address that actually holds the token, or the balance-read/prepared-sender/signer disagree and the swap fails.
995
+ - A fresh executor wallet with ZERO router allowance makes `prepareSwap` return `kind:"approval"` (not `"swap"`), so the first stop-loss trigger THROWS the sell gate — pre-approve token→router once before expecting an autonomous sell.
996
+ - Verifying rhbot `.env` flags via `dotenv`: rhbot uses its own `loadEnvFile` (dotenv isn't a dep), so `pm2 env` shows nothing and inline `node -e` `import("dotenv")` fails — write the check script INTO the repo and import `./server/env.js`'s loader instead.
997
+ - Reading `getAmountsOut` at `amounts[0]` (the INPUT) instead of the last element; on a single hop the wrong number is obvious, on a multi-hop path it silently passes.
998
+ - Advertising `quote`/`prepare` support for a chain whose router was never verified on that chain, or bulk-adding routers to raise a coverage number.
999
+ - Verifying a third-party venue by **codesize alone**. Bytecode proves deployment, not identity — the canonical QuoterV2 address has code on Base but cannot price its pairs. Probe behaviour.
1000
+ - Bending a V2 adapter to cover V3 (or vice versa). Different pricing call, different selector, explicit fee tier — a mis-encoded swap reverts or routes through the wrong pool.
1001
+ - Defaulting to one fee tier because 0.3% is common; liquidity concentrates in a tier that varies by pair and chain.
1002
+ - Ranking routes on headline `amountOut`. Gas is part of the price and the winner flips with trade size.
1003
+ - Treating a missing gas figure as zero — it silently promotes the worst route to first place.
1004
+ - Quoting a spread between two routes that measure cost differently (it compares third vs fourth while reading as first vs second).
1005
+ - Letting one dead aggregator fail the whole comparison, or spending a timeout on a provider already known to return 410. Check `sunset`/`deprecation` response headers before assuming rate-limit.
1006
+ - Guessing a USD gas cost from gas UNITS with no gas price; report `null` instead.
1007
+ - Folding bridge duration into the score rather than surfacing it beside the price.
1008
+ - Ranking a winner and leaving the user to rebuild it by hand. Hand the winner to
1009
+ prepare so the compared route and the signed route are provably the same.
1010
+ - Returning one "prepared" blob for both an unsigned transaction and an EIP-712 order.
1011
+ A CoW order is signed and submitted, never broadcast; conflating them fails at signing
1012
+ time, after the user already approved tokens.
1013
+ - Deriving the approval spender from the destination. CoW pulls funds through the
1014
+ **vault relayer**, so approving the settlement contract yields an order that silently
1015
+ never fills.
1016
+ - Preparing against the comparison's quote. Re-quote inside prepare and report drift —
1017
+ a minimum computed from a stale quote is not a minimum.
1018
+ - Passing the quote placeholder (`0x…dEaD`) into a prepare call. It is correct for
1019
+ anonymous quoting and a category error for preparing; some venues reject it and some
1020
+ build a transaction for an address nobody controls.
1021
+ - Reporting a venue's on-chain precondition as a bare HTTP 400. "Not enough allowance"
1022
+ is a fixable ordering constraint (approve first), not a router bug — and the real
1023
+ message is usually on `err.body`, not `err.message`.
1024
+ - Silently substituting a different venue when the winner cannot be prepared, or
1025
+ presenting a caller-forced non-winner as the best route.
1026
+ - A preparer wired to a **key-gated** source, so on a keyless machine it is unreachable
1027
+ dead code that still looks like coverage. Stub the keys in the test, and pin the
1028
+ quote-only set so a source becoming unpreparable fails CI instead of a user.
1029
+ - Returning a bare transaction object for a bridge because *this* route happened to be
1030
+ single-tx. Another source returns an approval plus a deposit, and the caller who
1031
+ learned the object shape signs one of them and believes the bridge is done.
1032
+ - Reporting only the signing chain for a bridge, or passing along a transaction whose
1033
+ `chainId` is not the origin — the wallet then prompts on a network the user cannot
1034
+ reason about.
1035
+ - Letting the user infer that an origin-chain confirmation means funds arrived. It does
1036
+ not, and the silence is what causes double-sends. State non-atomicity and the ETA.
1037
+ - Stating a fact only in an error **message** that a caller must act on (the fallback
1038
+ venue, the failure class) while leaving the structured field `undefined`. Agents
1039
+ consume fields, not prose.
1040
+ - Emitting a field named after the private codebase (`madSlippage`) in a payload handed
1041
+ to users. Secret scans, portability tests, and docs tests all miss it because it lives
1042
+ in the output shape — and a hard rename breaks artifacts already in flight, so keep a
1043
+ dual-read window on consumers.
1044
+ - `rg -c 'pattern' -r ''` to count occurrences: `-r` is *replace*, so it rewrites the
1045
+ output you were trying to read. Use `rg -n` to list, `rg -o pattern | wc -l` to count.
1046
+ - Calling a Solana prepare verified because it returned base64. Jupiter builds a
1047
+ swap for a placeholder pubkey that dies at `simulateTransaction: failed to sanitize
1048
+ accounts offsets correctly` — only a simulate against a real funded pubkey proves it.
1049
+ - Hardcoding a probe wallet in an E2E script. Parameterize it; a placeholder default
1050
+ turns a wallet-less machine red and reads as a code regression.
1051
+ - Checking a `maxPrice` cap on the venue's *response*. The cap must reject before any
1052
+ network call — assert it with a call counter, not just a thrown error.
1053
+ - Passing a listing's `tokenAddress` (the ATA) where the mint belongs, or letting
1054
+ lamports and decimal SOL both reach the caller. Normalize units at the provider edge.
1055
+ - Treating HYPE staking as 18 decimals. It is **8** — `1.5 HYPE = 150000000` — and an
1056
+ over-precise amount must be rejected, not silently rounded.
1057
+ - Preparing a HyperCore `cWithdraw` without reading `delegatorSummary` first. It only
1058
+ moves the **undelegated** bucket, so "unstake" is two actions (undelegate, then
1059
+ withdraw) and the user pays a round trip to discover that.
1060
+ - Describing a HyperCore action POST to `/exchange` as a broadcast. It is a signed
1061
+ action submission; there is no chain transaction and no hash.
1062
+ - A `/sign/i` export-name guard that fails on a legitimate `HL_SIGNATURE_CHAIN_ID`
1063
+ constant. Use an explicit allowlist rather than a negative lookahead.
1064
+ - Using a real personal burner address as a test fixture. It trips the pre-publish
1065
+ private-term scan and leaks an address into a public repo even when it holds dust —
1066
+ use public constants (wrapped-SOL / USDC mints, `0x0…abc`).
1067
+ - Collapsing "provider needs a user key that isn't configured" into `fail` in an E2E
1068
+ report. Report `keyed` as its own state or a correctly-configured machine looks broken.
1069
+ - Retrying a 4xx, or ignoring `Retry-After` in favour of your own shorter backoff.
1070
+ - Deduping a POST or a request with a body, or sharing a deduped response between a
1071
+ keyed and a keyless caller.
1072
+ - Unbounded `Promise.all` across every provider in a health sweep — it trips public
1073
+ rate limits and the resulting 429s read as an outage.
1074
+ - Keying a single-flight dedupe on whether an auth header is **present** rather than
1075
+ on its value. Two callers with different API keys collapse into one upstream request
1076
+ and the second receives data fetched with the first one's credential. Hash the full
1077
+ identity header set into the key.
1078
+ - Replaying a non-idempotent request after a **transport-level** failure. A status code
1079
+ means the server refused it (safe to retry); no status means the server may have
1080
+ already applied it and only the response was lost. Restrict statusless retries to
1081
+ `GET/HEAD/OPTIONS/PUT/DELETE`.
1082
+ - Reporting a Bitcoin balance from ONE derived address. One key controls `1…`, `3…`,
1083
+ `bc1q…`, and `bc1p…` simultaneously — funds at the wrong script type read as a
1084
+ missing key, a bad key, or an empty wallet, and none of those is the actual problem.
1085
+ - Signing an input whose script type differs from the address the funds sit at. Same
1086
+ key, different `scriptPubKey`, invalid signature. Consolidate first.
1087
+ - Sweeping an address with many small UTXOs. Ordinal-range outputs (`<= 1000 sats`)
1088
+ can be burned as fee; refuse auto-selection and make the user name the UTXO.
1089
+ - `Number(wei)` on a `uint64` EIP-712 field. Above 2^53 the wallet is handed a
1090
+ different amount than the one prepared and displayed — silently, and in the field
1091
+ the user is looking at. Keep it exact as a string above `MAX_SAFE_INTEGER`.
1092
+ - `Number()` on any user-supplied price or amount: it accepts `"1e2"` → 100, `"0x10"`
1093
+ → 16, and `Infinity`. Require a plain decimal.
1094
+ - **A cap accepted as a JS `number` above 2^53 rounds UP, widening itself.**
1095
+ `9007199254740995` arrives as `...996`, so a guard built from it authorizes one
1096
+ more unit than the user asked for — the cap silently becomes larger than the
1097
+ intended one, which is the wrong direction to fail. Reject `!Number.isSafeInteger`
1098
+ on any bound and tell the caller to pass a decimal string or `BigInt`. Distinct
1099
+ from the `Number(wei)` pitfall above: that one corrupts the *displayed* amount,
1100
+ this one corrupts the *limit* protecting it.
1101
+ - Gating an approval guard on `approve(address,uint256)` alone. **Every other
1102
+ allowance-granting entrypoint then skips it entirely** — `increaseAllowance`,
1103
+ SafeERC20 `forceApprove`/`safeApprove`/`safeIncreaseAllowance`, Permit2
1104
+ `approve`/`permit`, and EIP-2612 / DAI-style `permit` all grant or raise spend
1105
+ rights. Guard the full set, and **derive each selector with
1106
+ `keccak_256(signature)` in code rather than transcribing hex**: closing this exact
1107
+ finding I hand-wrote the `forceApprove` and `safeApprove` selectors and *both were
1108
+ wrong*, so the fix would have shipped the hole it claimed to close while passing a
1109
+ test that asserted the same recalled constants. Real values: `0x095ea7b3` approve,
1110
+ `0x39509351` increaseAllowance, `0x61f49ed6` forceApprove, `0xeb5625d9` safeApprove,
1111
+ `0xf9255c1f` safeIncreaseAllowance, `0x87517c45` Permit2 approve, `0xd505accf`
1112
+ EIP-2612 permit, `0x8fcbaf0c` DAI permit. Assert the negative arm too —
1113
+ `transfer` (`0xa9059cbb`), `transferFrom`, and `decreaseAllowance` must NOT match.
1114
+ Same rule for event topics, EIP-712 type hashes, and domain separators: a wrong
1115
+ security constant fails **silently**, unlike a wrong test constant.
1116
+ **After expanding recognition, expand the decoder in the same diff** — Grok
1117
+ round 7 caught the half-fix (recognition without decode).
1118
+ - Recording a spend cap on the response instead of enforcing it before the call. A
1119
+ `maxPriceSol` that is echoed but never compared is worse than no cap — the caller
1120
+ believes a ceiling held.
1121
+ - Accepting a **pre-signed** transaction payload from a marketplace API in a "prepare"
1122
+ path. Take only the documented unsigned field; a compromised or impersonated
1123
+ endpoint otherwise laundres arbitrary bytes past every local check.
1124
+ - Forwarding an env API key to a caller-controlled `baseUrl`. That is credential
1125
+ exfiltration and SSRF dressed as configuration — allowlist the hosts and require an
1126
+ explicit opt-out env var for local mocks.
1127
+ - Passing a provider flag that widens a bound you already enforced (Jupiter
1128
+ `dynamicSlippage: true` voids the bps ceiling while the returned guard still
1129
+ advertises the capped number). Force it off and reject the request.
1130
+ - A destination allowlist that fails **open** when empty. "No allowlist configured"
1131
+ must refuse, not permit everything — the permissive branch fires exactly when the
1132
+ operator has configured the least.
1133
+ - Gating a real market submission on a caller-supplied boolean alone. A steered agent
1134
+ can set `execute: true`; what it cannot set is the credential's presence or the
1135
+ autonomous opt-in.
1136
+ - **Enforcing a bound only when its field happens to be present.** Two separate
1137
+ CRITICALs came from this one shape: a transfer guard checked `amount` and
1138
+ `maxAmount` each `!= null`, so a guard omitting **both** skipped every check and
1139
+ authorized `MaxUint256 - 1`; and a session grant compared `maxGasWei` only when the
1140
+ *request* volunteered `gasWei`, so omitting the field satisfied a grant whose
1141
+ ceiling would have rejected it. If a guard bounds something, **require** the field —
1142
+ a capless guard is not a guard, and the caller who omits it is exactly the caller you
1143
+ are guarding against. Also reject internally incoherent guards (`amount > maxAmount`).
1144
+ - A spend cap that is stripped as an "internal" field and then never compared to
1145
+ anything (Satflow `maxSats`). Enforce it against the venue's **settled** price on the
1146
+ response, not just the caller's requested price, and withhold the artifact when the
1147
+ price is unknown.
1148
+ - Authenticating a guard with nothing at the **signer** boundary. If the party
1149
+ supplying the calldata also supplies the object vouching for it, the bound is
1150
+ decorative — `quoteAmountOut: "1"` makes any floor trivially satisfiable.
1151
+ HMAC the guard with the same attestation secret the route/vault attestations
1152
+ use, and verify **before** the bounds checks when signing/broadcasting.
1153
+ **Round 7 + Sol merge:** quote/prepare may attach an *unsigned* bound (self-host
1154
+ UX without a forced secret). `enforceTxPolicy` must pass `requireSigned: true`
1155
+ so missing secret / missing signature fails closed at sign and broadcast.
1156
+ Catching a missing secret and returning `null` *at that gate* was the Opus
1157
+ CRITICAL (forged 1-wei floor on 100 WETH). Never document unauthenticated
1158
+ bounds as acceptable for sign/broadcast. Mint a secret at `oracle-init --apply`
1159
+ into `~/.config/oracle/exec.env`. Pin a dummy secret in the npm test script
1160
+ under `ORACLE_TEST_ISOLATE_SECRETS=1`. See `references/oracle-round7-guard-parity.md`.
1161
+ - **Recognising a selector without decoding it bricks legitimate usage.** Round 7
1162
+ Grok DO_NOT_SHIP: allowance selectors were added to the *recognition* list so
1163
+ the guard demanded a bound approval, then `decodeApproveCalldata` only knew
1164
+ `approve()` — so `increaseAllowance` / SafeERC20 / EIP-2612 / Permit2 became
1165
+ unsignable. Recognition and decoding must cover the **same** set; an
1166
+ unknown-but-recognised selector fails closed (`recognised but not decodable`),
1167
+ never silently passes. Slot map: approve/increaseAllowance = w0/w1; SafeERC20
1168
+ and EIP-2612/Permit2 = w1/w2; DAI permit allowed bool at w4.
1169
+ - **Every time-bounded guard must call the shared `fresh-window` helper.**
1170
+ `Number(nowMs) > Number(expiresAtMs)` is false for NaN/null/"later"/{}, so a
1171
+ broken clock turns a short TTL permanent. Round 7: approval was the only guard
1172
+ still hand-rolled and accepted timeless + NaN clocks. Grep for `fresh-window`
1173
+ after any new guard.
1174
+ - **Do not exempt approval-shaped calldata from the destination allowlist.**
1175
+ `!approvalLike` lets any unreviewed contract with an allowance selector skip
1176
+ the strongest drain control. `assertApprovalGuard` binds the *spender*, not
1177
+ `to`. The token (or Permit2) being called is a destination like any other.
1178
+ - **`key present = armed` is trading posture, not withdrawal posture.** A signing
1179
+ lane that only checks `prepared.action` is truthy will sign `withdraw3` /
1180
+ `usdSend` / `approveAgent`. Allowlist prepare-emitted types only
1181
+ (`order`/`cancel`/`updateLeverage`/`updateIsolatedMargin` for HL).
1182
+ - **Never construct `new Wallet(raw)` on unvalidated key material.** ethers embeds
1183
+ the full value in `invalid BytesLike value (… value="…")` — a file-read oracle
1184
+ via `keyFile`. Validate `^0x?[0-9a-fA-F]{64}$` first; rethrow naming the source,
1185
+ never the contents.
1186
+ - A write-op denylist built from an anchored **name** regex. It blocked `sendFoo` while
1187
+ letting `txBroadcast`, `doSend`, `rawSign`, `orderSubmit` and `forceExecute` straight
1188
+ through, and blocked nothing real because every money-moving op was named `prepare*`.
1189
+ The actual boundary is the catalog: a provider may only be asked for an op it
1190
+ declares. Keep substring matching as defence in depth, not as the control.
1191
+ - Treating loopback bind as a security boundary for a local data server. Any page the
1192
+ operator visits can reach `127.0.0.1:<port>`, and DNS rebinding reaches it under an
1193
+ attacker hostname — both arriving with the server's own API keys. Validate the `Host`
1194
+ header is a loopback name and reject a cross-origin `Origin`.
1195
+ - `bad-txns-inputs-missingorspent` on a Bitcoin input you already verified is
1196
+ confirmed and unspent. That error is almost always **txid byte order**, not
1197
+ availability — a helper that pre-reverses the txid before handing it to a signer
1198
+ that also reverses internally produces a reference to a nonexistent outpoint.
1199
+ Decode your own signed tx, compare the input txid to the real one, and pass the
1200
+ display-order hex string rather than pre-reversed bytes.
1201
+ - Building a reveal against an **unconfirmed** commit, or re-deriving the prepare
1202
+ with different parameters than were funded. Assert `contentSha256` and
1203
+ `commitAddress` against the funded values before signing the reveal.
1204
+ - Debugging an `Invalid Schnorr signature` on an ordinals reveal as a signing bug.
1205
+ Check `vsize` first: ~111 vbytes for a multi-KB payload proves the **key path** was
1206
+ taken and the envelope never entered the witness. The commit must be built on a
1207
+ **NUMS** internal key (`p2tr(undefined, {script: leaf})`) so script-path is the only
1208
+ way to spend.
1209
+ - Setting `allowUnknownOutputs` on an ordinals reveal and not `allowUnknownInputs`.
1210
+ `@scure/btc-signer` classifies the envelope leaf as `unknown` and throws
1211
+ `Finalize: Unknown tapLeafScript` — two different flags, and only one of them is the
1212
+ one you need.
1213
+ - Encoding the ord **body tag** with a push helper. `encodePush([0])` emits `01 00`;
1214
+ the separator is a bare `OP_0` (`0x00`). The wrong form still confirms *and indexes*,
1215
+ producing an inscription with `content size: 0 bytes` — the most expensive kind of
1216
+ success. Diff your envelope bytes against a live indexed inscription instead of
1217
+ reasoning from the spec.
1218
+ - Verifying an inscription against your **local** file. The sha256 matched perfectly
1219
+ through two dead attempts; only reassembling the body from the on-chain witness (and
1220
+ a third-party indexer's view) can see an envelope-structure failure.
1221
+ - Attributing a **precise** operator symptom to indexer lag. "Blank" is vague; "content
1222
+ size 0 bytes" is a measurement that proves the indexer parsed the envelope and found
1223
+ no body — which waiting never fixes. `GET /content/<id>` returning 404 on a confirmed
1224
+ tx settles it in one command.
1225
+ - Trusting your own envelope parser's `body bytes: 0` as evidence the content is gone.
1226
+ Validate the parser against a known-good inscription first — searching for a
1227
+ zero-length push instead of a 1-byte zero push reports an intact inscription as empty.
1228
+ - Verifying a `package.json` change without regenerating `package-lock.json`. A bin/
1229
+ export drift guard fails on the lockfile, and the message reads like a missing file
1230
+ rather than stale metadata.
1231
+ - Fixing a **class-level** finding only on the files named in the repro. "Every keyed
1232
+ provider follows this pattern" means enumerate the full set by grepping the *shape*,
1233
+ fix all of them behind a shared helper, and add a table-driven test that iterates the
1234
+ membership — otherwise a later provider that forgets the helper ships the hole again.
1235
+ - Editing the repo while an audit reads it. Test counts and failures stop being
1236
+ attributable to any commit, and hours go into phantoms. Freeze the tree or have each
1237
+ auditor work from its own `git archive <sha>` snapshot.
1238
+ - Rolling a "looks correct on reading" fix into an all-clear. `NOT VERIFIED` is a third
1239
+ verdict beside `HOLDS`/`BROKEN` and must survive into the summary.
1240
+ - A secret resolver with a `~/.config` fallback left active under test. The maintainer's
1241
+ suite silently disagrees with CI and with every contributor; add a test-only isolation
1242
+ switch and set it in the `test` script.
1243
+ - Writing a DNS-rebinding test with `fetch()` and a manual `Host` header. `fetch`
1244
+ rewrites `Host` from the URL, so the test never sends the attack. Use a raw socket.
1245
+ - Spawning a server in a test without `{ timeout }`, without `detached: true` +
1246
+ process-group kill, or with env var names the server does not actually read — each
1247
+ produces a hang or a false "server is broken".
1248
+ - `assert.throws` on an **async** function. It never catches the rejection, so the
1249
+ guard it claims to cover has never once fired in the suite while the test file
1250
+ reads as coverage. Use `await assert.rejects`, and grep for the shape after adding
1251
+ any `await` to a guard path.
1252
+ - Two test files hardcoding the **same port**. `node --test` runs files in parallel,
1253
+ so one loses the bind and its tests fail as `404`/`ECONNREFUSED` that look like
1254
+ broken product routes. Green-alone plus red-in-full-suite is almost never a product
1255
+ bug — give each server-spawning file its own port.
1256
+ - Triaging a **wall** of unrelated test failures as separate bugs. When they share one
1257
+ syscall, one errno, or one port, they are one cause: ~40 failures across a dozen
1258
+ files, all at `writeFileSync`/`appendFileSync`/`mkdtemp` with `UNKNOWN: unknown
1259
+ error, write` and `errno: -122`, were a single exhausted `/tmp` tmpfs quota
1260
+ (`-122` is `EDQUOT`, which Node surfaces as a useless `UNKNOWN`). Probe with a
1261
+ one-line `mkdtempSync`+`writeFileSync`, check `df -h /tmp` and `mount | grep /tmp`
1262
+ for `usrquota`, and prove any reclaimed file is dead (`lsof` empty AND `tar -tf`
1263
+ shows a finished artifact) before deleting.
1264
+ - Reading a venue's parallel-array response per surface instead of joining once.
1265
+ Open interest denominated in the coin, funding as a per-interval decimal, and
1266
+ index-paired metadata each get re-derived slightly differently and the chart stops
1267
+ matching the card. Missing numerics must become `null`, never `NaN`.
1268
+ - Claiming a venue supports trading because a read integration and an `/exchange` URL
1269
+ constant exist. Order placement is a separate signed action set — grep for the
1270
+ placement function before answering "can we trade here?".
1271
+ - Asserting a signing implementation against a known-answer vector **recalled from
1272
+ memory**. A plausible-but-invented fixture fails, and the failure looks like a bug
1273
+ in correct code — costing a debugging detour into msgpack and byte order. Fetch the
1274
+ vector from the SDK's own test file, then compare.
1275
+ - Naming an internal module after something that collides with the owner's **brand**
1276
+ (`hl-media` when "HL Media" is a separate company). It reads as that product in
1277
+ every log line, catalog listing and doc. Rename to what it actually is
1278
+ (`hl-markets`), and do the rename with `git mv` plus a longest-first ordered
1279
+ substring pass so `hlMediaHealth`/`hl-media.mjs`/`"hl-media"` all migrate without
1280
+ double-replacing.
1281
+ - A `new URL()`-based host guard rejecting a test's mock hostname. `https://mock.0x` is
1282
+ genuinely unparseable (invalid TLD) while `https://mock.1inch` parses — fix the
1283
+ fixture rather than loosening the guard, and treat a blank `baseUrl` override as
1284
+ absent rather than throwing.