Skip to main content

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.
Compass solves this by making the routing decision a pure function. Given the same inputs (user rules, current positions, current yields), it always produces the same output. The LLM never enters this path. It enters only when the user opens chat — under 5% of all evaluations on a typical account.

Two parts: scheduler + evaluator

The loop is split into a global scheduler and a per-account evaluator. They have different jobs and different properties.
ComponentScopeRole
SchedulerGlobalDecides when and for which account to run the evaluator.
EvaluatorPer accountDecides what to do for one account, given its state.
This split is what makes event-driven execution possible. The scheduler watches external signals (yield-source updates, price feeds, on-chain events). When something material changes, it enqueues an evaluation for every account that could be affected. The evaluator itself doesn’t poll — it only runs when the scheduler wakes it.

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).
If nothing relevant changes, no tick runs. Idle accounts cost nothing.

Evaluator

The evaluator is a pure Rust function. Given an account’s state and the current world state, it produces an EvaluatorThought. 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 structured EvaluatorThought with five fields, one per step. The thought is what gets written to the audit trail — including the ticks that decide to do nothing. deterministic loop diagram Both branches end at the audit trail. A no-op tick is just as much a record as an executed one — that’s how the system can answer “why didn’t the agent do anything?” with a deterministic trace.

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.
A rejection is a normal outcome, not an error. It just means this tick produces no on-chain action.

5. emit

Two possible emissions:
  • A session-key-signed call if check_policy approved.
  • A no-op record if it didn’t.
Either way, the full 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 loopNot in the loop
Scheduler event handlingLLM inference
Yield-source adaptersNatural-language parsing
propose functionPlan generation from prompts
Policy checksConversational explanations
Session key signingUser-facing chat responses
The boundary is strict. A plan that came from the chat agent still has to pass through 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:
  1. Pull the EvaluatorThought from the audit trail.
  2. Feed its load_state and fetch_yields snapshots back into the same evaluator binary version.
  3. The output matches bit-for-bit.
This is the property that lets users (and the team) answer “why did the agent do that?” with a deterministic trace, not a post-hoc explanation.

Retries and indexer lag

Cross-chain settlement via Circle Gateway uses BurnIntent 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_state may 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.
See Four-step pipeline.

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.