The agent09

The operator

The company is run by an agent that can only choose among actions code has already allowed. Every tick it reads a snapshot of the books, code enumerates the candidate actions and why the rest are not allowed, the model picks and explains, code re-checks right before executing, and one signed ledger entry records all of it.

Snapshot → candidates → guards → one entry

  1. Snapshot. companySnapshot() reads revenue (the payments table), reserves (revenue wallet USDC and Circle Earn vault positions on Arc, Circle wallets), obligations (treasury invoices and daily headroom), screening volume and p95 latency, current prices and modes. Every section degrades to null with an error string; the function never throws.
  2. Candidates. candidateActions() is deterministic. It lists what is allowed now with a reason, and lists every rejected action with its reason. HOLD is always a candidate.
  3. Choice. The model (same client as the analyst) must call submit_plan with candidate ids, a rationale and the next check interval. Unknown ids are dropped, HOLD is never mixed with actions, an empty choice falls back to the deterministic plan, the interval is clamped to 15 min–6 h, the rationale is cut at 160 words. Without an LLM provider (LLM_PROVIDER=0g + ZEROG_API_KEY, or ANTHROPIC_API_KEY) the deterministic plan runs with meta.fallback = true.
  4. Execution. In the order payouts → prices → liquidity, validateAction() re-derives the candidates and rejects anything no longer allowed, then each executor honours its own mode (dry-run or live).
  5. Record. One OPERATOR entry with the snapshot summary, candidates, rejected, chosen with results, rationale and nextCheckAt. The company page renders the last ten.
json
// kind: "OPERATOR" - one per tick, hash-chained like every other decision
{
  "type": "OPERATOR",
  "snapshot":   { "revenue": { … }, "reserves": { … }, "obligations": { … }, "screening": { … }, "pricing": { … }, "mode": { … }, "errors": { … } },
  "candidates": [{ "id": "c1", "type": "RUN_PAYOUTS", "params": { "pendingInvoices": 2, "amountUsdc": 35 }, "reason": "…" }, { "id": "c2", "type": "HOLD", … }],
  "rejected":   [{ "type": "SWEEP_TO_YIELD", "reason": "balance 2.02 below float 5 but no vault to redeem (effective float 5 = 5 cushion + 0 due 48 h)" }, { "type": "ADJUST_PRICE", "reason": "x402 disabled …" }],
  "chosen":     [{ "id": "c1", "type": "RUN_PAYOUTS", "ok": true, "result": { "mode": "dry-run", "paid": 1, "limited": 1, "held": 0, … } }],
  "rationale":  "…plain English, ≤ 160 words, cites the numbers in the snapshot…",
  "nextCheckInSeconds": 1800, "nextCheckAt": 1760001800,
  "meta": { "model": "0g:claude-opus-4-8", "fallback": false, "latencyMs": 2140, "guardNotes": [], "paused": false, "chainId": 5042 }
}

What it may decide

ActionAllowed whenExecutor
SWEEP_TO_YIELDfloat and obligations are both known, usdc − (YIELD_FLOAT_USDC + obligations due in 48 h) ≥ min sweep (1 USDC), and a Circle Earn vault passes the rules (params carry the vault)Circle Earn tick from the Circle revenue wallet (dry-run unless YIELD_MODE=live)
REDEEM_FROM_YIELDusdc is below the effective float, a vault position is held, and that vault has the liquidity to pay itsame
RUN_PAYOUTSpending invoices exist, daily cap has headroom, and a live treasury is not at zero reservesGuarded Payouts tick (dry-run until TREASURY_MODE=live)
ADJUST_PRICEx402 is enabled; raise when paid calls in 24 h > 50 and p95 ≤ 8 s, lower when outside paid calls in 48 h = 0; inside bounds, step and cooldownoperator_pricing table, read per request by the paid routes
HOLDalwaysno-op

A sweep is never planned blind: unknown reserves, unknown obligations or missing Earn vault metadata reject it. A paused service (COMPLIR_PAUSED=1) rejects everything but HOLD. Ledger entries written before 2026-10-14 call these actions SWEEP_TO_USYC / REDEEM_FROM_USYC.

Where idle revenue goes

The yield backend is Circle Earn: Morpho USDC vaults on Arc, discovered through Circle's Earn API (the service behind @circle-fin/earn-kit) and used as plain ERC-4626 from the Circle revenue wallet (USDC.approve → vault.deposit(assets, wallet); vault.redeem(shares, wallet, wallet)).

RuleWhy
refuse vaults Circle flags low_liquiditythe revenue has to come back on demand; Circle's illiquid flag is the first gate
available liquidity ≥ 2× (deposit + position)we could redeem the whole position twice over right now
no RED warning, not unlisted, Circle Sentinel, TVL ≥ 100k, APY > 0skip timelock-less test vaults and unreviewed curators
best current APY among the resttoday: Steakhouse Prime USDC 0xbeef0016…7298
float = YIELD_FLOAT_USDC (5) + obligations due 48 h; ≤ 25 USDC per depositpayouts and refunds never wait on a redeem; stays under the wallet's per-tx policy

Each executed move is its own ledger entry (OPERATOR · YIELD_SWEEP / YIELD_REDEEM with tx hashes); a plan that moves nothing is NOOP · YIELD_PLAN. The /company Reserves card shows the position per vault (name, address, shares, assets, APY, liquidity). Details: docs/YIELD.md.

Pricing bounds

screen
0.005 – 0.05 USDC (config default 0.01)
watch
0.02 – 0.2 USDC / 30 days (config default 0.05)
Step
at most ±20 % per change; the operator proposes 10 % steps
Cooldown
at least 6 h between two changes of the same price
Raise trigger
OPERATOR_RAISE_CALLS_24H (50) paid calls in 24 h and p95 latency ≤ OPERATOR_MAX_P95_MS (8,000 ms)
Lower trigger
zero outside paid calls in 48 h and price above the floor

validatePriceChange() is a pure guard (bounds → step → cooldown) and runs again inside setPrice(). The current price is what the 402 advertises; a client that signed for less is rejected before anything runs.

Schedule and visibility

Cron
hourly at :30 (/api/internal/operator, bearer CRON_SECRET) or worker/operator.ts
Next check
the agent's own interval (900–21,600 s) is recorded; the worker honours it, the Vercel cron is the floor
Read
GET /api/v1/company - snapshot + last ten decisions, cached 30 s · /company · /ledger kind OPERATOR
Model
LLM_MODEL (today claude-opus-4-8 on the 0G Compute Router; ANALYST_MODEL for Anthropic), temperature 0.2, tool-forced structured output