Documentation Index
Fetch the complete documentation index at: https://docs.usecompassai.com/llms.txt
Use this file to discover all available pages before exploring further.
Compass’s hot path is a pure-function Rust evaluator. It runs per account, only when something changes, and produces a structured EvaluatorThought that can be replayed bit-for-bit. The LLM is not in this loop.
This is the page where the central design claim of Compass — “LLM out of
the hot loop” — becomes concrete. Below is what actually runs every time the
system makes a routing decision.
Why a deterministic loop at all
A naive AI yield agent calls the LLM every time it considers rebalancing. That has three costs:- Money. LLM inference is paid per token, per account, per tick. At any meaningful scale, this eats yield.
- Reproducibility. LLM outputs vary between runs. A decision can’t be re-audited by re-running it; you have to trust the after-the-fact explanation.
- Auditability. A “why did the agent do X” answer from the LLM is a rationalization, not a trace. There’s no way to know whether the same input would produce the same output tomorrow.
Two parts: scheduler + evaluator
The loop is split into a global scheduler and a per-account evaluator. They have different jobs and different properties.| Component | Scope | Role |
|---|---|---|
| Scheduler | Global | Decides when and for which account to run the evaluator. |
| Evaluator | Per account | Decides what to do for one account, given its state. |
Scheduler
The scheduler is event-driven, not cadence-driven. There is no fixed “every N seconds.” Instead, the scheduler triggers an account’s evaluator when:- A whitelisted yield source on a whitelisted chain publishes a new rate.
- A user’s account state changes (deposit, withdrawal, rule update).
- An in-flight cross-chain intent settles or fails.
- A retry condition from a previous tick is met (e.g. indexer lag recovery).
Evaluator
The evaluator is a pure Rust function. Given an account’s state and the current world state, it produces anEvaluatorThought. It has no side
effects of its own — execution and audit-writing happen outside it, against
the thought it returned.
Because the evaluator is pure, every tick is reproducible. Re-running an
evaluator with the same recorded inputs produces the same recorded output.
The five steps of an EvaluatorThought
Every tick produces a structuredEvaluatorThought with five fields, one per
step. The thought is what gets written to the audit trail — including the
ticks that decide to do nothing.
1. load_state
Snapshot the account at this moment: current positions across all chains,
USDC balances, the user’s risk band, protocol whitelist, chain whitelist, and
caps. Also snapshot any in-flight intents from previous ticks.
This is the “what’s true about this account right now” record.
2. fetch_yields
Read current rates for every whitelisted (protocol, chain) pair. Yields come
from venue-specific adapters — each adapter is small, audited code that
returns a normalized rate.
This is the “what does the world look like right now” record.
3. propose
A deterministic function over the previous two steps: given the current
positions and current rates, what is the best route? The answer can be:
- A new route — e.g. exit a lending position on one L2 and open one on another where rates are higher.
- Stay put — current allocation is already optimal under the rules.
- No valid route — nothing satisfies the rules right now.
propose does not call the policy engine yet. It just produces a candidate.
4. check_policy
Run the candidate through the policy engine.
Every rule attached to the account — whitelists, risk band, per-route cap,
daily cap — is checked. The output is either:
- Approved — the candidate becomes a signed call.
- Rejected — with a structured reason field naming the rule that failed.
5. emit
Two possible emissions:
- A session-key-signed call if
check_policyapproved. - A no-op record if it didn’t.
EvaluatorThought — all five fields — is written to the
audit trail. See Audit trail.
What’s in the loop, what’s not
The loop is what runs every tick. The LLM is what runs when the user opens chat. These are different code paths.| In the deterministic loop | Not in the loop |
|---|---|
| Scheduler event handling | LLM inference |
| Yield-source adapters | Natural-language parsing |
propose function | Plan generation from prompts |
| Policy checks | Conversational explanations |
| Session key signing | User-facing chat responses |
check_policy and emit before anything moves. A tick triggered
by a yield change never calls the LLM at all. See
Chat agent for how the LLM hands off to the loop.
Reproducibility in practice
Because the evaluator is pure and the inputs are recorded, any past decision can be replayed:- Pull the
EvaluatorThoughtfrom the audit trail. - Feed its
load_stateandfetch_yieldssnapshots back into the same evaluator binary version. - The output matches bit-for-bit.
Retries and indexer lag
Cross-chain settlement via Circle Gateway usesBurnIntent signed messages
rather than broadcast transactions. This matters for the loop:
- If an intent settles but the indexer hasn’t caught up, the next tick’s
load_statemay still show the old position. The scheduler holds a pending-intent guard so duplicate proposals don’t fire. - If an intent fails or expires, the scheduler enqueues a retry with the same parameters. Because the underlying signature is reusable, no re-signing or re-prompting is needed.
- Compass uses a 60-second window for intent retries before falling back to a paused state and surfacing the issue in the dashboard.
Next steps
Policy engine
The gate that every candidate plan passes through.
Audit trail
Every EvaluatorThought, including no-ops, recorded and replayable.
Chat agent
The LLM path, and where it hands off to the loop.
System overview
Back to the full three-layer picture.