Decision Loop
A keeper cycle reads the chain, scores every candidate venue, and produces one verdict with the receipt that justifies it. The verdict is frequently no.
The cycle
read snapshots, costs, block be/src/core/venue
↓
freshness MAX_IDLE_DAYS be/src/core/strategy
↓
net-edge edge after every cost be/src/core/netedge
↓
decide (input) → {verdict, receipt} be/src/core/decide/allocate.ts
↓
plan verdict → calls, per wrapper be/src/core/wrappers
↓
record the decision, either way be/src/core/vdlThe core knows nothing about custody
allocate.ts takes snapshots, costs and a block, and returns a verdict and a
receipt. It carries no wrapper field, no Safe address, no vault address: see
Architecture for why that boundary is structural rather than tidy.
Freshness is required, not optional
lastActivityAgeDays is a required field on venue statistics, and making it
required deliberately broke two call sites.
An optional field would let "freshness unknown" read silently as "fresh", the flattering direction. Thirteen of thirteen wrong measurements in this project erred that way. A caller that cannot state the age now has to say so in a diff.
The receipt
apyInputs is the receipt: the inputs a third party replays to reach the same
verdict. ⛔ A formatted string is not a receipt, because it is not replayable, which is why the API returns data rather than presentation.