Collective Intelligence Protocol
HomeCrisisOne PriceObservatoryResearch
Beta guide - under review. Describes NightWatch v1.0 beta; features marked v1.1 / v1.2 are planned.
livegenerated from `svc/common/mandate_vault_rulebook.py` — do not hand-edit this file, run `python scripts/gen_rulebook_md.py` instead (tests/test_gen_rulebook_md.py checks the two never drift apart)

The Mandate Vault Rulebook

Chapters 27 and 30 explain the Mandate Vault in prose, for a depositor, a maker, a supplier, or an auditor reading top to bottom. This chapter is the same rules in one shape instead: a table per topic, machine-readable, with every value traced to exactly what enforces it — a contract constant, a contract immutable, the NightWatch server, or a NightWatch policy decision that is not code at all. The same rows are available as JSON at GET /rules/mandate-vault (optionally ?topic=Fees) and as the MCP tool get_rules, so an AI agent never has to parse this prose to get an exact answer.

Status column key — live-testnet: true today, against the currently deployed testnet contracts and/or the server code running against them. mainnet-target: audited and/or planned for the mainnet deploy, not yet live on any deployed contract. policy: a NightWatch decision that governs the product but is not itself a contract or server code path. closed: a rule that no longer applies; its number and end date are kept.

Deposits & withdrawals

RuleValueEnforced bySourceStatus
A deposit does not mint shares immediatelydeposit() creates a PendingDeposit (cash, outside NAV) until the settler deploys itcontract constantSpotMandateVault.sol:deposit/PendingDepositlive-testnet
Settler batch-start service level15 minutes after deposit (keeper ops SLA, not an on-chain timer)NightWatch policydocs/pm/MANDATE_VAULT_ETF_ISSUANCE_ORDER.md §1 rule 2 (DEPLOY_SLA)policy
Unused pending deposit stale timeout24 hours, then the depositor chooses refund (cash back) or mint at that moment's NAVcontract constantSpotIssuanceLib.sol:PENDING_STALE_AFTER / resolveStale()live-testnet
Withdrawal orderFIFO queue: cash first, then sell basket assets, then the dust write-off, then (after 3 days unresolved) self-settle, then in-kind redemptioncontractSpotMandateVault.sol withdrawal queuelive-testnet
Dust write-off threshold1e12 wei = 0.000001 USDT or less is treated as settled; stayers, not leavers, bear thiscontract constantSpotSettlementLib.sol:SETTLE_DUSTlive-testnet
Deposit/withdrawal feesNone — the vault charges a performance fee onlycontract (no fee code path on deposit/withdraw)docs/pm/MANDATE_VAULT_V0_SPEC.md §0 (fees row)live-testnet
Unwhitelisted (stray) tokens sent to the vaultNot counted in NAV and not refunded automatically; the factory PAUSER can sweep them to the treasurycontractSpotSettlementLib.sol:sweepStraylive-testnet
On-chain minimum deposit (Robin 2026-09-29)None — deposit() accepts any positive amount, on-chain or in the contract. The deposit screen recommends $10 as a practical floor; that recommendation is UI copy only, not an enforced limit.contract (no minimum coded) + NightWatch policy (screen copy)frontend/app/(site)/forge/vault/[id]/VaultDepositPanel.jslive-testnet

NAV & pricing gate

RuleValueEnforced bySourceStatus
Price-agreement rule (D1)Every configured source must agree within 1%, OR a single Chainlink feed alive within its own maxAge is sufficient alonecontractReferenceOracle.sol:fairPricelive-testnet
Disagreement tolerance1% (100 bps)contract constantReferenceOracle.sol:DISAGREEMENT_BPSlive-testnet
Buy gateask1 x 1.01 (Binance best ask)contract constantReferenceOracle.sol:BUY_GATE_BPSlive-testnet
Sell gatebid1 x 0.99 (Binance best bid)contract constantReferenceOracle.sol:SELL_GATE_BPSlive-testnet
Keeper snapshot freshness required30 seconds or lesscontract constantReferenceOracle.sol:SNAPSHOT_MAX_AGElive-testnet
Pyth price freshness (adapter exists; not wired on mainnet today)60 seconds or lesscontract constantReferenceOracle.sol:PYTH_MAX_AGEmainnet-target
Impairment (D2) — the escape for a genuinely dead price feedA held asset priced stale for more than 24 hours may be marked impaired (valued 0 in NAV) by the factory PAUSER; clears itself the moment a valid price returnscontract call (markImpaired/clearImpaired) gated by a policy preconditionSpotMandateVault.sol:markImpaired/clearImpaired — the >24h precondition is off-chain/process, not tracked on-chainpolicy
Maximum whitelisted basket assets16contract constantSpotMandateVault.sol:MAX_ASSETS / SpotMandateVaultFactory.sol:MAX_ASSETSlive-testnet

Rebalancing

RuleValueEnforced bySourceStatus
Execution venuePancakeSwap v3 only (exactInputSingle/exactOutputSingle)contractSpotMandateVault.sol:rebalanceSwaplive-testnet
Per-leg notional capThe gate-derived limit; a caller may only make it tighter, never loosercontractSpotMandateVault.sol / maxLegAssetslive-testnet
Per-asset PancakeSwap pool fee tierSet by the factory (poolFeeOf) at vault creation, per asset, from measured pool depth — not a single constant: BTCB 0.05% (500) and ETH 0.05% (500) at their deepest pool, WBNB 0.01% (100), SOL 0.25% (2500) and XRP 0.25% (2500); the 0.25% (2500) POOL_FEE_DEFAULT applies only to an asset left with no explicit entrycontract constant + deploy configurationSpotMandateVault.sol:POOL_FEE_DEFAULT/poolFeeOf — docs/pm/MANDATE_VAULT_SPOT_ORDER.md pool-depth remeasurementlive-testnet
Per-leg swap deadline5 minutescontract constantSpotIssuanceLib.sol:LEG_DEADLINElive-testnet

Supply auction

RuleValueEnforced bySourceStatus
Bid window120 secondscontract constantBondedSupplyAuction.sol:BID_WINDOWlive-testnet
Award grace period after the bid window30 secondscontract constantBondedSupplyAuction.sol:AWARD_GRACElive-testnet
Delivery window (fixed per deployment; BSC, R9 onward)1,800 seconds (30 minutes)contract (immutable constructor param)BondedSupplyAuction.sol:deliveryWindow — deploy_spot_vault.py --delivery-window defaultlive-testnet
Minimum partial delivery per installment20% of the awarded quantity (the final remainder is exempt)contract constantBondedSupplyAuction.sol:MIN_DELIVERY_BPSlive-testnet
Penalty on an undelivered remainder0.5% of the remaining notional, paid to the treasury; the vault is separately made wholecontract constantBondedSupplyAuction.sol:SLASH_BPSlive-testnet
Gate-failure grace (degraded-mode delivery/expiry window)2 minutescontract constantBondedSupplyAuction.sol:GATE_FAILURE_MAX_AGElive-testnet
Tier share-cap ceiling and floor (on-chain bound)1% floor (T-1 probation) to 100% ceilingcontract constantBondedSupplyAuction.sol:MIN_MAX_SHARE_BPS / MAX_SHARE_BPSlive-testnet
Tier bond-ratio ceiling and floor (on-chain bound)25% floor (T3 Anchor) to 100% ceilingcontract constantBondedSupplyAuction.sol:MIN_BOND_RATIO_BPS / MAX_BOND_RATIO_BPSlive-testnet
Award trimming (R10 onward)award() trims each winner to the minimum of their tier's share cap, their bond limit, and the auction's remaining quantity; a trim to zero reverts the whole award() batchcontractBondedSupplyAuction.sol:award (R10)live-testnet
SELL-side delivery settlement orderThe supplier's own posted bond is consumed before any wallet transfer (paidFromBond)contractBondedSupplyAuction.sol:deliverlive-testnet
Concurrent unresolved auctions per (vault, asset) pair1 — a second openAuction() on the same vault+asset reverts (AuctionAlreadyOpen) until the first resolvescontract (openAuctionOfVaultAsset mapping)BondedSupplyAuction.sol:openAuction/AuctionAlreadyOpenlive-testnet

Supplier tiers

RuleValueEnforced bySourceStatus
T-1 Probation — award share cap and bond ratioshare cap 1% of the auction, bond ratio 100%, floor $10, cap $100; must deliver its whole auction award in one single full delivery — no partials, unlike every other tierserversvc/common/auction_tiers.py:_TIER_LIMITSlive-testnet
T0 Newcomer — award share cap and bond ratioshare cap 25% of the auction, bond ratio 100%serversvc/common/auction_tiers.py:_TIER_LIMITSlive-testnet
T1 Supplier — award share cap and bond ratioshare cap 50% of the auction, bond ratio 75%serversvc/common/auction_tiers.py:_TIER_LIMITSlive-testnet
T2 Market maker — award share cap and bond ratioshare cap 100% of the auction, bond ratio 50%serversvc/common/auction_tiers.py:_TIER_LIMITSlive-testnet
T3 Anchor — award share cap and bond ratioshare cap 100% of the auction, bond ratio 25%serversvc/common/auction_tiers.py:_TIER_LIMITSlive-testnet
Promotion out of T-1 Probationa minted SBT AND (>= 3 completed deliveries OR >= $1,000 cumulative delivered notional)serversvc/common/auction_tiers.py:PROMOTION_MIN_DELIVERIES / PROMOTION_MIN_DELIVERED_USDlive-testnet
Demotion triggerfill rate below 90% over the last 20 awards, or any expiry in the last 30 days, drops one tier (floor T0); a second expiry in that window drops straight to T0serversvc/common/auction_tiers.py:tier_for / DEMOTION_FILL_RATE_THRESHOLDlive-testnet
Demotion lookback window30 daysserversvc/common/auction_tiers.py:DEMOTION_WINDOW_DAYSlive-testnet
Cumulative delivered-notional thresholds (T1/T2/T3)T1 >= $100,000, T2 >= $1,000,000, T3 >= $100,000,000serversvc/common/auction_tiers.py:_TIER_DELIVERED_THRESHOLDS_USDlive-testnet

AuctionHouse P0

RuleValueEnforced bySourceStatus
What this isa SEPARATE public supply-auction house (anyone screened + whitelisted may open an order), distinct from the vault's own BondedSupplyAuction above -- orderer escrow instead of auctionPay, its own SupplierRegistry (cash bond only in P0) and HouseReferenceOracle (consolidated 7-venue reference price, KuCoin excluded)contractdocs/pm/AUCTION_HOUSE_V1_SPEC.md §3mainnet-target
Bid window / award grace120s / 30s (award enforced on-chain not before openedAt+BID_WINDOW; extend-once by BID_WINDOW when zero bids at the grace deadline)contract + servercontracts/AuctionHouse.sol:BID_WINDOW/AWARD_GRACEmainnet-target
Supplier tiers (0..3, no probation tier)T0 25% share/100% cash min · T1 50%/75% · T2 100%/50% · T3 100%/25% -- identical numbers to the vault's own tier table above; "T-1 folded into T0": a brand-new supplier starts at T0 with its first award capped at $500, single full delivery onlyserver (mirrors contract)svc/common/auction_tiers.py:house_tier_params / T0_FIRST_AWARD_CAP_USDmainnet-target
Orderer's price-improvement fee15% of the price improvement they actually got against the reference, capped at 0.3% of the fill's own notional -- zero if there was no improvement, zero on a fallback (DEX) fill, and never more than that cap even on a large improvement; suppliers pay no feecontractcontracts/AuctionHouse.sol:FEE_SHARE_BPS (1500) / FEE_CAP_BPS (30)mainnet-target
No-show slash0.5% of the award notional to the treasury (SLASH_BPS), same formula the vault's own BondedSupplyAuction uses -- diff-notional to the orderer first, remainder to treasurycontractcontracts/AuctionHouse.sol:SLASH_BPS (50)mainnet-target
Reference priceconsolidated best bid/ask across 6 supply venues (binance/bybit/okx/bitget/gateio/mexc -- KuCoin excluded), median ±2% outlier exclusion, degrades to Binance-only below 2 fresh venuesserversvc/common/house_reference_snapshot.py:OUTLIER_TOLERANCE_PCT / MIN_FRESH_VENUESmainnet-target
Fallback (DEX) tolerance±30bp vs. the reference (mode ③ "fill-or-fallback" only; modes ①/② refund an unfilled remainder instead of routing to the DEX)contractcontracts/AuctionHouse.sol:FALLBACK_TOL_BPS (30) / MAX_SLACK_BPS (50)mainnet-target
Chain / venue scopeBSC only in P0; KuCoin excluded from both the reference price and delivery-feasibility checks (feedback_kucoin_excluded.md)policydocs/pm/AUCTION_HOUSE_V1_SPEC.md §4-E / §14 item 8mainnet-target
Key separation7 separate keys total: the house keeper (tx-sending, NW_HOUSE_KEEPER_PK) is a DIFFERENT key from the vault settler; the house snapshot signer (NW_HOUSE_SNAPSHOT_PK) and screening signer (NW_HOUSE_SCREENING_PK) are each their own key toopolicydocs/pm/AUCTION_HOUSE_V1_SPEC.md §14 item 6mainnet-target
Revenue sourcesauction_price_improvement (the orderer's 15% fee) and auction_penalty (treasury's slash share) -- shared source names with the vault's own BondedSupplyAuction, recognized from raw on-chain facts by a separate cursor-driven worker, never guessedserversvc/common/revenue.py:VALID_SOURCES / svc/worker/nw_auction_house_revenue_recognize.pymainnet-target
Depositor-supplier Cherry rebate (principle, Robin 2026-10-01)The 15% improvement fee is paid by the orderer, so a supplier discount is impossible. Instead, a fixed share of the treasury's take from auctions that a depositor-supplier (someone who both deposits into vaults and supplies in auctions) delivered is paid back to that person as a Cherry rebate. The ratio is set after the first month of live data; it is never a discount on the orderer-paid fee. Planned for v1.1 (docs/pm/P1_SHARE_COLLATERAL_ORDER.md)NightWatch policydocs/pm/P1_SHARE_COLLATERAL_ORDER.mdpolicy
Order sources on The Floor's desk (§4-I, revised 2026-09-28 finding 6) -- v1.1 for AUTO and for executing a queued MANUAL orderevery order carries a source: VAULT·AUTO (the vault keeper triggers it for a reason -- rebalance, deposit, or withdrawal), VAULT·MANUAL (a vault operator queues it, inside mandate room), or ORDER (anyone, self-funded escrow, the ONLY source that ever opens on AuctionHouse). A vault is never an AuctionHouse orderer -- its order executes through the vault's OWN existing on-chain path (SpotMandateVault.rebalanceSwap, BondedSupplyAuction.openAuction as the gate-failure fallback), decided 2026-09-28 after rejecting a design that would have routed vault orders through AuctionHouse with the settler as orderer. TODAY: an operator can queue and cancel a signed MANUAL order; the vault keeper picking a queued row up and executing it, and any code path creating a VAULT·AUTO row at all, both ship in v1.1serverdocs/pm/AUCTION_HOUSE_V1_SPEC.md §4-I / svc/common/auction_house_vault_orders.pymainnet-target
Vault manual order endpointsPOST /market/vaults/{vault_id}/orders (owner-wallet EIP-712 auth over the WHOLE order, PlaceVaultOrder(vaultId, side, token, amountUsdt, qty, priceMode, limitPrice, slackBps, fallbackToleranceBps, allowPartial, extendOnce, nonce, requestedAt)) queues a VAULT·MANUAL order after a server-side mandate-room PRE-CHECK (UX only -- the vault contract enforces its own mandate on-chain regardless); GET /market/vaults/{vault_id}/orders (owner-signed ReadVaultOrders) lists AUTO+MANUAL together; POST /market/vaults/{vault_id}/orders/{queue_id}/cancel (owner-signed CancelVaultOrder(vaultId, queueId, nonce, requestedAt)) -- a queued VAULT-MANUAL order can be cancelled any time before the vault's keeper picks it up (immediate, nothing sent on-chain yet); once picked up, a rebalanceSwap leg is a single atomic on-chain call (not a multi-stage bid/award process), so there is nothing left to cancel. Audit V4 fix (2026-09-29): "owner" for all three routes means the vault's LIVE SpotMandateVault.owner(), read from chain on every call (60s TTL cache) -- never nw_auction_house_vault_registry.owner_address, which ops sets once at registration and never re-syncs on an on-chain ownership transfer. The vault uses Ownable2Step, so a transfer takes effect once the new owner calls acceptOwnership; within 60 seconds of that call the new owner can sign and the old owner no longer can. An RPC failure refuses the request (503), it never falls back to that registry rowserversvc/api/routes/market.py:post_vault_order / get_vault_orders / cancel_vault_ordermainnet-target
Which key executes a vault order (AUTO or MANUAL) -- v1.1NW_SPOT_SETTLER_PK -- the SAME key that already sends every other vault-authorized action (deployPending/rebalanceSwap/settleWithdrawals/crystalliseFees/claimFees), sending SpotMandateVault.rebalanceSwap for the leg (or, on the vault's own ±1% gate failing, whatever opens the BondedSupplyAuction fallback -- still a manual sizing step today, for AUTO legs exactly as much as MANUAL ones). No new key, no new on-chain role, no contract change: this is the vault's own existing execution path, not AuctionHouse. Vault cash and bought tokens never sit in the settler's own wallet -- they move directly between the vault contract and the AMM/auction. The earlier AuctionHouse-orderer design (a settler-held escrow, every vault sharing one wallet's own AuctionHouse limits) was considered and rejectedserver + opssvc/worker/nw_spot_vault_manual_legs.py (module docstring: architecture decision + "WHICH KEY SIGNS")mainnet-target
Mandate room for a manual order (server pre-check, not the only enforcement)the token must be on the vault's own mandate allow-list (empty allow-list = every P0-listed asset allowed); monthly turnover room defaults to 100% of the vault's own NAV ON FILE (an ops-maintained snapshot in the vault's registered mandate JSON, not a live on-chain read) per calendar month (2026-09-28, replacing an earlier flat $50,000 constant entirely) -- a vault's own mandate JSON may set a different share (monthly_turnover_nav_bps) instead of the 100% default, and may still set an absolute-dollar override directly, which wins over either NAV figure; a vault with NO nav_usd on file and no override gets REJECTED, never treated as unlimited. Queued and executed orders both count toward whichever cap applies, so submitting several before any of them executes still uses up the same room, checked under a per-vault row lock; no leverage on this desk (a manual order executes as a rebalanceSwap leg trading the vault's own already-held assets, never a borrow -- see finding 6: it is never escrowed into AuctionHouse); it must also fit the live per-symbol cap read from HouseReferenceOracle.capUsd, applying PER ORDER (a large order can be split into several smaller ones to stay under it -- the monthly turnover limit is the backstop) -- an unconfigured cap blocks the order rather than passing it through unchecked. This check is a UX pre-check in front of the vault contract's own on-chain mandate enforcement (allowedAssets, maxLegAssets, the ±1% gate), not a substitute for it. A signed order or cancellation request is valid for 5 minutes (300s) from when it was signedserversvc/common/auction_house_vault_orders.py:validate_mandate_room / compute_mandate_roommainnet-target
Orderer daily cap and per-auction cap (ORDER-source, ratio-based, 2026-09-28)replacing an earlier flat $5,000/day and $1,000/auction: an orderer's DAILY cap (all auctions they open in the UTC day, across every token) is max($5,000 pilot floor, 25% of the free supplier bond pool). A single AUCTION's own cap is min(the oracle's symbol cap, 10% of that same pool, the on-chain caps.maxAuctionUsd) -- when the pool is exactly 0 the 10%-of-pool term is dropped entirely rather than capping every auction to $0, so a fresh house can still fill through the on-chain fallback path. "Free supplier bond pool" = the unlocked USDT bond of every registered, screened, non-suspended supplier, read from SupplierRegistry state at check time (a cache of up to 15s is fine) -- ONE shared pool, not per token (a per-token pool is v1.1). The on-chain caps.maxAuctionUsd stays $1,000 through P0 (an admin raises it as the pool grows). Pure math only in this pass (svc/common/auction_house_caps.py, fully tested); wiring it into the screening-issuance/auction-opening path is v1.1 -- today the only ceiling actually enforced is the on-chain $1,000server (math) / v1.1 (enforcement wiring)svc/common/auction_house_caps.py:orderer_daily_cap_usd / per_auction_cap_usdmainnet-target
The Floor's Board across two contracts (§4-I item 3, 2026-09-28)Board is meant to carry both ORDER-source rows (from the AuctionHouse indexer) and VAULT-source rows (from the vault flows indexer / BondedSupplyAuction events, already indexed for that vault's own page) in one list -- suppliers bid on VAULT rows through the existing vault-auction bid API (unified 4-tier), and on ORDER rows through AuctionHouse. TODAY: Board shows ORDER-source rows only; merging in VAULT-source rows is a near-term integration step, not yet built. No single orderer's AuctionHouse limits are shared across vaults under this design -- each vault's own on-chain mandate gates its own legs independentlyserverdocs/guide/32-the-auction-house.md "Where it lives"mainnet-target
P0-Rounds: what a round is, and who opens the next one (docs/pm/AUCTION_HOUSE_ROUNDS_ORDER.md)contracts are unchanged -- an order the orderer experiences as one continuous thing is, on-chain, a group of SEPARATE AuctionHouse orders the server tracks by an order_group_id, one full open->award/expire/fallback->settle cycle per round (120s bid window + 30s award grace, reference re-anchored at award, live gate <= 30s old). openAuction makes its OWN caller the orderer and pulls escrow from that same wallet, so the ORDERER'S OWN WALLET is what opens every round, including round 2 and beyond -- neither the house nor any keeper can open a round on someone else's behalf, though the desk now signs and sends that transaction for the orderer with one click (round 1 and every later round). Escrow refunds at the end of a round; opening another one means signing and escrowing again -- except a round with zero awards, which in v1.1 can be reopened on the SAME order with reopen(id, expiry, screening, authSig), keeping its escrow (only once its 150 seconds are over; the server says so with next_action: "reopen" and reopen.order_id, every other remainder is next_action: "open", a fresh openAuction). An agent orderer signs ReopenAuth(id, expiry, currentOpenedAt) and posts it to POST /market/auction/{order_id}/reopen-auth; the house keeper relays it. v1.1 also gives the signed Screening attestation an extra one-time-use nonce. A vault leg is different: the vault's OWN keeper re-tries it next cycle, through the vault's own existing path (chapter 27), never this round system at all. The remainder is read from a single on-chain snapshot (qty - filledQty - qtyResolved) PLUS the sum of every AwardExpired.remainingQty the indexer has already applied for that order -- if a winning bid is awarded and then simply never delivered (it expires past its own 30-minute window), that portion is refunded straight back to the orderer on-chain and filledQty alone would still count it as awarded, but the indexer adds the expired amount back on top of the live read so the remainder matches what was actually refunded. Remaining known limitation (fails safe): a mode-3 empty-route DEX refund and a plain refundRemainder both mark that qty 'resolved' in the contract's own bookkeeping, which the indexer's remainder read cannot distinguish from qty that's genuinely still chaseable -- can only ever UNDER-state the remainder, never over-state it, so this never over-offers a next roundserver (bookkeeping) + contract (each round itself) + the orderer's own walletsvc/common/auction_house_flows.py:nw_auction_house_order_groups / nw_auction_house_order_group_roundsmainnet-target
P0-Rounds: "Sign round n+1" and the round/time capan orderer sets a round cap (1-10 rounds, 5 by default) and an overall time cap (bounded, positive) when opening round 1 (Advanced on the order form); both are fixed for the whole group from then on. My desk shows "Sign round n+1" once a round has resolved with a remainder that is still at or above the token's own remainder floor, the round cap isn't used up, and the time cap hasn't elapsed -- clicking it now does the whole thing with one click: signs the same AttestationRequest step round 1 used (pre-filled with the group's own token, side, order_group_id and round_number+1, and the remainder as qty), POSTs it, then sends openAuction from the same wallet with the screening it returns -- or, when the reply says next_action: "reopen" (v1.1: a zero-award round whose 150 seconds are over), reopen on that same order reusing its escrow, for exactly the order's own qty; the same round cap, time cap and remainder floor apply. Round 1's own "Place order" does the identical flow. A no-bid round reaches a next round by the orderer cancelling it first (cancel() only succeeds pre-award, so filledQty/qtyResolved are still both 0 and the full unfilled qty becomes that round's own remainder; the next round is a fresh openAuction) or, in v1.1, by reopen keeping the escrow (the indexer links the new round to the group by the same order id). The server re-checks the floor/cap/time-cap trio live the moment a next round is actually requested (never just when it decided to show the badge), and also checks the requested round's own token/side match the group and its qty does not exceed the previous round's remainder; an attested-but-never-opened round expires after 24h so it cannot block the group forever. There is no automatic DEX fill between rounds and no automatic re-open -- both would need a contract change (v1.1). A vault's own AUTO/MANUAL legs never use this round system -- they run on the vault's own keeper cycle instead (chapter 27)server (eligibility check) + the orderer's own wallet (the actual re-open)svc/common/auction_house_rounds.py:should_offer_next_round / svc/api/routes/market.py:request_open_auction_attestationmainnet-target
P0-Rounds: the remainder floor gates opening and re-opening onlyfloor = max($50, 20x the cheapest known delivery route's own withdrawal fee, that exchange's own minimum order size for the token), computed per token (its on-chain address resolved to a real symbol first) across every known route -- a route with an unknown (0/null) withdrawal fee is excluded from the max(), never treated as free, and the floor itself is never $0. The third term (an exchange's own minimum order size) now reads a real number per route from token_metadata (the same cache GET /metadata serves), resolved from the token's own address to the CEX pair symbol first -- a route with no cached minimum still contributes 0 for that term (never a guess), so omitting it can still only ever make the floor LOWER, never higher, and never causes a false refusal. An ORDER sized below its own floor is refused at placement, checked against the token and quantity the caller actually SIGNED (both are part of the AttestationRequest digest) -- that signature is not yet bound to the specific on-chain openAuction call itself, so it cannot stop someone from opening a smaller on-chain order than what they signed (binding the signature hash into openAuction's own calldata needs a contract change, v1.1). A round whose remainder drops below the floor simply is not offered a next round. The floor NEVER touches an award -- it does not grow, shrink, or drop a winning bid; the contract awards exactly what the signed bids and each bidder's own limits allow, full stop. A vault's own MANUAL rebalance leg below the floor is evaluated every cycle (nw_spot_vault_keeper.py:run_manual_legs_step_for_vault, after settle_vault) -- actually EXECUTING a leg at or above the floor is v1.1; a deposit's own buy legs and a withdrawal's own sell legs are never skipped this wayserver (placement/re-open checks only)svc/common/auction_house_rounds.py:compute_token_floor_usd / should_refuse_order_entry / should_skip_vault_legmainnet-target
Delivery is a contract call, not a transfer (Robin clarification)deliver() must be sent from the awarded bidder's OWN wallet (msg.sender == bidder) and pulls the token with transferFrom from that same wallet -- a supplier who bought on several exchanges must withdraw to that bidder wallet, approve, and call deliver from it, inside the 30-minute window. Tokens sent straight from an exchange, or from any other wallet, are stray: they never count as delivered and are swept to treasury on the vault side. One-click Deliver from The Floor's own screen (signing approve+deliver for the supplier) is v1.1 -- today, a row's own Deliver button opens these instructions rather than sending anything. A future deliverFor(id, awardIndex, qty) callable from any wallet (payment still to the bidder) is also v1.1 -- exchange-direct transfers still would not count even thencontractdocs/pm/AUCTION_HOUSE_ROUNDS_ORDER.md "Delivery rule clarification"mainnet-target
What a supplier pays to deliver (cost preview, Robin addendum)The Floor's real Bid panel fetches and shows this before a bid is signed: the exchange withdrawal fee for the supplier's own delivery route (nw_exchange_contracts.wd_fee, resolved via the token's own symbol; "fee unknown -- route excluded" if 0/unknown), a live BSC gas estimate for approve (once per token) + deliver (per call) in USD -- the BNB price for that USD conversion is read from the house reference oracle's own live fairPrice(WBNB) first, falling back to the last indexed WBNB AuctionHouse order only if that live read fails (never the other way around, so it doesn't stay null just because no WBNB order was ever placed), and net = (bid price - that route's own exchange price) x qty - withdrawal fee - gas, with a plain warning when net <= 0. The route's own exchange price is, for now, approximated from the same cross-exchange reference the order itself uses (a live per-route feed is v1.1). The orderer's own 15% price-improvement fee is NEVER subtracted here -- suppliers pay no fee to bid or deliver (spec §4-C); an earlier draft of this formula did subtract it and priced the comparison from the orderer's own side, which was backwards for a number shown to the supplierserver (math + live inputs) + the supplier's own wallet (the actual bid signature)svc/common/auction_house_delivery_cost.py / GET /market/auction/{id}/cost-preview / frontend/app/(site)/market/BidPanel.jsmainnet-target

Fees

RuleValueEnforced bySourceStatus
Fee typePerformance fee only — no deposit or withdrawal feecontractSpotMandateVault.sollive-testnet
Hurdle (monthly)2% (200 bps) — no fee is charged below this hurdle for the monthcontract constantSpotMandateVault.sol:HURDLE_BPSlive-testnet
Passive-mandate fee rate2.5% (250 bps)contract constantSpotMandateVaultFactory.sol:PASSIVE_FEE_BPSlive-testnet
Active-mandate fee rate range10%-25% (1000-2500 bps)contract constantSpotMandateVaultFactory.sol:ACTIVE_FEE_MIN_BPS / ACTIVE_FEE_MAX_BPSlive-testnet
Platform share of every performance fee20% (2000 bps) to the NightWatch treasury; the maker keeps 80%contract constantSpotMandateVault.sol:PLATFORM_SHARE_BPSlive-testnet
Fee claim prioritySubordinate to the withdrawal queue; claimFees() is permissionless but blocked while the vault is pausedcontractSpotMandateVault.sol:claimFeeslive-testnet

Bonds

RuleValueEnforced bySourceStatus
Maker bond (at vault creation)25% of the proposed TVL cap, floored at $100 (pilot deploys used $250)serversvc/api/routes/forge_indices.py:BOND_RATE / BOND_FLOOR_USDlive-testnet
Maker bond nature (Robin 2026-09-29)Collateral only — never a share of the vault and it earns nothing while posted. Refunded to the maker in full, minus any penalty assessed for a violation.NightWatch policydocs/pm/FORGE_MAKER_ONBOARDING_FLOW.md §8 / docs/pm/MANDATE_VAULT_FEES_AND_REVENUE.md §4policy
Maker violation catalogue (Robin 2026-09-29)Three things count as a maker violation: plan falsification (what is published does not match what the vault actually does), a pilot-mandate breach (deviating from the approved pilot's own rules), and a retroactive edit of a published record (backtest, pilot performance, or plan text changed after publication). A keeper or NightWatch operational error is NightWatch's own responsibility, not the maker's, and is never grounds for a bond penalty.NightWatch policydocs/pm/FORGE_MAKER_ONBOARDING_FLOW.md §8policy
Maker bond adjudication (Robin 2026-09-29)The NightWatch operator adjudicates every case publicly, with a written reason attached to the decision. Restitution to affected depositors is paid first, before any remaining penalty is kept or forwarded to the treasury.NightWatch policy (operator decision, not a contract function)docs/pm/FORGE_MAKER_ONBOARDING_FLOW.md §8policy
Cherry conversion rateServices priced in USD are listed in Cherry at the published list rate (price_table.usd_to_cherry); Cherry has no guaranteed cash valueserversvc/common/price_table.py:usd_to_cherrylive-testnet
Supplier minimum first bond$100server policysvc/common/auction_tiers.py:MIN_FIRST_BOND_USDlive-testnet
Supplier bond ratio by tier100% (T-1/T0) down to 25% (T3), bounded on-chain to [25%, 100%]server, bounded by contractsvc/common/auction_tiers.py:_TIER_LIMITS + BondedSupplyAuction.sol:MIN_BOND_RATIO_BPS/MAX_BOND_RATIO_BPSlive-testnet
Cherry-denominated bond (v1.1)Not available today: bonds are posted in on-chain USDT. Cherry bonds are planned for v1.1 (they need a treasury deposit-on-behalf function and a bond ledger, designed together with the P1 share-collateral contract)NightWatch policy (Robin 2026-10-01)docs/pm/P1_SHARE_COLLATERAL_ORDER.mdpolicy

Roles & keys

RuleValueEnforced bySourceStatus
DepositorNo deposit or withdrawal fee, ever (performance fee only). If the keeper/settler goes silent, nothing is stuck: a stale pending deposit resolves after 24 hours (refund or mint at that moment's NAV), and an unpaid withdrawal request can self-settle (settleOwnRequest) after 3 days unresolved, or redeem in kind pro-rata at the live NAVcontractSpotMandateVault.sol: PendingDeposit/resolveStale, settleOwnRequest, redeemInKindlive-testnet
Maker / vault ownerCannot touch depositor money directly — deposits sit as PendingDeposit (cash, outside NAV) until the settler deploys them, and only the withdrawal queue/emergency paths pay depositors out. The mandate (basket, fee rate) is frozen at vault creation: the maker cannot change the fee rate or mandate after the vault opens, and cannot withdraw depositor fundscontract (immutable at construction) + role separationSpotMandateVault.sol / SpotMandateVaultFactory.sol (fee rate set at createVault, no setter)live-testnet
Settler (keeper)Publishes EIP-712 snapshots, calls deployPending/rebalanceSwap/settleWithdrawals/crystalliseFees/claimFees/openAuction/award; rotated by the factory. If it stops running, nothing is stuck — deposits still resolve via the 24-hour stuck-pending-deposit path and withdrawals still exit via the 3-day emergency in-kind path, both permissionless and not dependent on the settlercontract roleSpotMandateVaultFactory.sol:SETTLER_ADMIN_ROLElive-testnet
Snapshot signerA separate server key — deliberately NOT the CDP wallet, because CDP cannot produce an EIP-712 signatureNightWatch ops policydocs/pm/MANDATE_VAULT_V0_SPEC.md §0-Clive-testnet
Factory admin / PAUSERCDP admin key — intentional design choice. CAN: pause/unpause/retire a vault, mark a stale-priced asset impaired (and clear it once price returns), sweep stray unwhitelisted tokens to the treasury. CANNOT: move vault or depositor funds to an arbitrary or third-party address, change fees, or upgrade the vault's mandate — it never holds a key that can redirect depositor funds anywhere outside the contract's own withdrawal/treasury pathscontract roleSpotMandateVaultFactory.sol:PAUSER_ROLElive-testnet
Whitelister (auction only)A dedicated hot ops key (since R9 S5), holding only setWhitelisted — never the settler's or admin's keycontract roleBondedSupplyAuction.sol:WHITELISTER_ROLElive-testnet
Auction contract topologyOne BondedSupplyAuction per chain, shared by every vault deployed on that chaincontract deployment topologyBondedSupplyAuction.sol / contracts/deploy_spot_vault.pylive-testnet
Role-separation enforcement on BSCThe deploy script refuses to deploy if settler, treasury, admin, or snapshot signer would collapse onto one keyNightWatch deploy toolingcontracts/deploy_spot_vault.py (R8 F1 fix)live-testnet
TreasuryA fixed payout address with no on-chain authority; must differ from the admin, settler, and signer keyscontract (fixed address) + deploy toolingSpotMandateVaultFactory.sol / contracts/deploy_spot_vault.pylive-testnet

Emergency exits

RuleValueEnforced bySourceStatus
Emergency self-settleIf a withdrawal request sits unresolved for 3 days, the requester (only their own request) may call settleOwnRequestcontract constantSpotMandateVault.sol:SETTLEMENT_EMERGENCY_TIMEOUTlive-testnet
In-kind redemptionredeemInKind pays your pro-rata share of the vault's underlying tokens; it requires a live NAV (current price) to resolve — there is no fallback formula for a price outage. If a live NAV isn't available yet, it waits for D2 recovery rather than blocking permanentlycontractSpotMandateVault.sol:redeemInKindlive-testnet
Stuck pending depositAfter 24 hours unused, the depositor chooses refund or mint-at-current-NAV via resolveStalePendingcontract constantSpotIssuanceLib.sol:PENDING_STALE_AFTERlive-testnet
Impairment escape for a dead price feedThe factory PAUSER can mark a stale-priced asset impaired (NAV=0); it self-heals the moment a valid price returnscontract call, policy preconditionSpotMandateVault.sol:markImpaired/clearImpairedpolicy
Vault pauseThe factory PAUSER can pause/unpause/retire a vault; claimFees is blocked while paused, but the withdrawal queue keeps moving toward the emergency exits abovecontract roleSpotMandateVaultFactory.sollive-testnet

Pilot limits

RuleValueEnforced bySourceStatus
Public availability todayNot open to the general public. Deployed and tested end to end on BSC testnet (chain id 97); the mainnet pilot is whitelist-only, small real deposits, not a public launchNightWatch policy + deploy statusdocs/guide/27-the-mandate-vault.mdlive-testnet
Per-depositor cap$100NightWatch policy (whitelist + deposit discipline, not an on-chain per-depositor limit)docs/pm/MANDATE_VAULT_SPOT_MAINNET_ORDER.mdmainnet-target
Vault cap (maxTotalAssets)$1,000contract (immutable, set at vault creation)contracts/deploy_spot_vault.py:MAINNET_MAX_TOTAL_ASSETS_18DECmainnet-target
Per-leg rebalance cap (maxLegAssets)$200contract (immutable, set at vault creation)contracts/deploy_spot_vault.py:MAINNET_MAX_LEG_ASSETS_18DECmainnet-target
Maker bond, pilot deploys$250NightWatch policydocs/pm/MANDATE_VAULT_V0_SPEC.md §0-Cmainnet-target
Listed basket assetsTier 1 only (BTCB, ETH, SOL, XRP, WBNB) — Tier 2 alts (ADA, DOGE, LTC, DOT, LINK, UNI, AAVE, AVAX) cannot list on mainnet yet because their Chainlink oracle is not configured in the deploy scriptdeploy configurationdocs/pm/MANDATE_VAULT_V0_SPEC.md §0 / §3.3mainnet-target

Agent access & pricing

RuleValueEnforced bySourceStatus
Free metered reads per day100 per registered agent accountserversvc/common/paid_read.py:NW_FREE_READS_PER_DAYlive-testnet
Metering order for a priced readfree tier -> Cherry balance -> x402 (USDC) -> HTTP 402 payment requiredserversvc/common/paid_read.py:decide()live-testnet
Payment kinds recordedfree, cherry, x402, subscriptionserversvc/common/paid_read.py:PAYMENT_KINDSlive-testnet
Live Forge maker signal readAlways a paid read (10🍒 or $0.10 in USDC); no registered-agent free-tier exemptionserversvc/api/routes/forge_signals.py (paid_read(free_tier=False))live-testnet
Per-key daily request ceiling (all calls, separate from the free-read count)2,000/day on the free tierserverfrontend/public/llms.txt §4live-testnet
x402 settlementUSDC via a facilitator verify+settle round-trip; revenue is recorded and split with the paid resource's ownerserversvc/common/x402.py / svc/common/revenue.pylive-testnet
Cherry purchase limitsClosed 2026-10-03: Cherry is no longer sold for money. Buy Obsidian Cherry and convert it.serversvc/api/routes/cherry_buy.py (vending closed, R104)closed

Promotion Pools

No decided rule rows yet — see "Under review" below.

Maker onboarding

RuleValueEnforced bySourceStatus
Index application reviewAn operator approves or rejects a 'submitted' application (POST /admin/forge/index-applications/{id}/approve|reject); rejection requires a reasonserversvc/api/routes/forge_indices.py:admin_approve_application/admin_reject_applicationlive-testnet
Spot pilot book on approvalApproving a submitted application creates exactly one nw_pilot_vaults row on the BSC spot venue (dex='bsc_spot', hl_address NULL — no Hyperliquid account exists for a spot book); no charge, no on-chain call — the application's own bond already recorded the money-side intentserversvc/api/routes/forge_indices.py:admin_approve_applicationlive-testnet
Pilot day 1The approval timestamp, not a first snapshot — a spot book has no Hyperliquid account to snapshot fromserversvc/api/routes/forge_indices.py:admin_approve_application (record_start=NOW() at approval)live-testnet
Minimum pilot record before a vault-creation request30 daysserversvc/api/routes/forge_vault_requests.py:MIN_PILOT_DAYSlive-testnet
Perp books cannot become deposit vaultsA vault-creation request is refused (409) for any book on dex main/xyz (a real Hyperliquid perp account) — it can run as a signal book with mirroring today; a deposit vault for a perp book is v1.2serversvc/api/routes/forge_vault_requests.py:request_vault_creation (_PERP_DEXES check)live-testnet

Under review — not yet decided

NightWatch has not yet decided the items below. They are listed here explicitly, rather than left for a reader to assume an answer that hasn't been made yet.

  • OperatorIdentity ERC-721 transferability as final policy (Roles & keys). NightWatch is deciding: the OperatorIdentity token is coded as a transferable ERC-721 (the reputation-carrying SBT it never moves with stays soulbound) — whether that transferability is the final mainnet policy, or will be restricted, is still open.
  • SBT revocation policy (Roles & keys). NightWatch is deciding: there is no revocation path for a minted Soulbound Token today — whether, and under what circumstances, a minted SBT could ever be revoked or its standing reset is not yet decided.
  • Per-agent x402 spend caps (Agent access & pricing). NightWatch is deciding: whether an individual agent account will have a maximum daily or per-transaction x402 (USDC) spend cap, separate from the existing free-read and per-key request-rate limits.
  • Cherry-per-atom rate and miner/verifier split (Promotion Pools). NightWatch is deciding: the exact Cherry-per-atom minting rate and the miner/verifier split for Promotion Pools are not yet decided — this is an open decision, not a published rate (docs/guide/22-what-is-superseded.md: "the exact distribution method for the new pools is still not finalized").

FAQ

Is the maker bond part of the vault's capital?

No. The maker bond (25% of the proposed TVL cap, floored at $100 (pilot deploys used $250)) is separate collateral, not a deposit counted in the vault's NAV or basket. Cherry bonds: Not available today: bonds are posted in on-chain USDT. Cherry bonds are planned for v1.1 (they need a treasury deposit-on-behalf function and a bond ledger, designed together with the P1 share-collateral contract).

Collateral only — never a share of the vault and it earns nothing while posted. Refunded to the maker in full, minus any penalty assessed for a violation.

What can a maker be penalised for, and who decides?

Supply-auction suppliers have a precisely defined penalty (an undelivered award is slashed on expiry — see the Supply auction table above). For a maker, the violation catalogue is published: Three things count as a maker violation: plan falsification (what is published does not match what the vault actually does), a pilot-mandate breach (deviating from the approved pilot's own rules), and a retroactive edit of a published record (backtest, pilot performance, or plan text changed after publication). A keeper or NightWatch operational error is NightWatch's own responsibility, not the maker's, and is never grounds for a bond penalty. The NightWatch operator adjudicates every case publicly, with a written reason attached to the decision. Restitution to affected depositors is paid first, before any remaining penalty is kept or forwarded to the treasury.

Are assets bought on PancakeSwap?

Yes — PancakeSwap v3 only (exactInputSingle/exactOutputSingle), enforced by the contract itself (SpotMandateVault.sol:rebalanceSwap).

What are the minimum/maximum deposits, and are they final?

On today's pilot: per-depositor cap $100 (mainnet-target), vault-wide cap $1,000 (mainnet-target). Neither is final — both are pilot-stage numbers NightWatch can raise as the vault proves itself, enforced by NightWatch policy (whitelist + deposit discipline, not an on-chain per-depositor limit) rather than a permanent contract limit. On-chain minimum deposit: None — deposit() accepts any positive amount, on-chain or in the contract. The deposit screen recommends $10 as a practical floor; that recommendation is UI copy only, not an enforced limit.