Developers
Decision loop

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/vdl

The 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.