Skip to main content

Overview

The AI agent — Synapse — is a two-speed nervous system. It connects to the sidecar via Server-Sent Events and wakes on three triggers. Each wake first passes through a deterministic gate (decideCycle) that routes the cycle to one of two tiers: a Reflex fast-path that submits with no model call, or a Cortex ReAct loop (the LLM) for anything that needs judgment.

tx_enqueued

A new user transaction arrived in the queue.

bundle_settled(failed)

A bundle terminated with a failure class that requires a retry decision.

30s watchdog

Safety net for SSE reconnect gaps — agent checks for any missed work.

Two-tier router (Reflex / Cortex)

Most submissions need no judgment: a stable floor with a fresh transaction has one correct move — bid the known clearing floor — and paying for LLM inference on that path is latency for nothing. So every wake first hits a gate that routes it to the cheapest tier that can decide it correctly.

Reflex — System 1

Deterministic fast-path, no model call (~0 ms). Fires only when every trivial-case check holds (fresh tx, stable floor, Jito leader, no unrecovered failure, oracle fresh) and submits at the recent clearing floor.

Cortex — System 2

The LLM ReAct loop (~4.2 s decision). Any ambiguity escalates here — a retry, a moving floor, a near-TTL transaction, a cold start, an unrecovered failure.
A misjudged Reflex bid is cheap by construction: it fails at the auction, pays no tip (Invalid), and the resulting bundle_settled(failed) routes its retry to the Cortex. The cost of an optimistic fast-path is bounded by one free rejection — never a lamport. The tier is a runtime toggle, not a fork. cortex is the default (a bare deployment reasons on every cycle); flip to reflex live with no restart:
await sinker.setMode('reflex');   // fast-path
await sinker.setMode('cortex');   // full reasoning
const { decision_mode } = await sinker.getMode();
Or seed the startup tier with AGENT_DECISION_MODE=reflex. Each landing records which tier produced it on the lifecycle entry (decision_mode, and escalated in reflex mode).

The Cortex cycle (System 2)

When the gate routes a wake to the Cortex, this is the loop that runs (the Reflex fast-path skips all of it). Data gathering is parallelized out of the LLM hot path. Before the model is invoked, the outer loop fetches getState, getTip, getLifecycle, and getPendingTxs in a single Promise.all and injects the results directly into the cycle prompt. The agent reads pre-loaded context and goes straight to the decision — typically two tool calls total.
trigger fires

preflight: queue depth > 0? inflight guard clear?

Promise.all([getState, getTip, getLifecycle, getPendingTxs])  ← parallel, ~50ms

inject into prompt as PRE-LOADED CONTEXT block

generateText (LLM call with tool set)

  [THINK] read pre-loaded slot, tip percentiles, queue items, history
  [THINK] select percentile, form reason string
  [CALL]  submit({ tip_lamports, reason, tx_ids })   ← step 1
  [CALL]  writeTrace({ id, trace })                  ← step 2

terminal: submitted / held / error
This cuts the typical Cortex cycle from 6+ sequential LLM steps to 2. The remaining inference still dominates — a measured ~4.2 s for the Cortex decision (the Reflex fast-path skips it entirely, ~0 ms). The agent still reads the same data; it just doesn’t burn a round-trip per field to fetch it.

Tip selection

The agent reads five percentiles on the live Jito tip oracle (p25p99) and opens at the lowest tip recent history shows landing — often the observed minimum, not a high percentile — because a fee_too_low rejection pays no tip and is free information. It escalates only on that free failure, climbing one tier per consecutive fee_too_low:
Consecutive fee_too_low failuresTip selected
0lowest recent clearing tip (probe from below)
1p75
2p95
≥ 3p99
This is not hardcoded logic — the agent reads the lifecycle history and reaches this conclusion through its reasoning trace. The trace is written to the lifecycle entry so every decision is auditable. Example reasoning trace from a real cycle:
“fee_too_low at p75=5000. Pattern: 2 consecutive fee_too_low. Oracle p99=1,000,000. Tx age=34/50 slots near TTL. Escalating to p95=100,000. If this fails: p99.”

The clean boundary

The architectural invariant that makes the system independently upgradeable:

Rust never calls an LLM

All execution — signing, serialization, Jito submission — happens in the Rust sidecar. The agent is an external observer that issues commands.

Agent never touches a keypair

The agent calls POST /internal/submit with a tip amount and list of tx_ids. The sidecar does the rest.
This means you can swap the AI provider — GPT-4o, Claude, Grok, or a local Ollama model — without any change to the execution layer.

Configuring the model

The agent model is configured via environment variables in the agent process:
# Use Anthropic Claude
ANTHROPIC_API_KEY=sk-ant-...
MODEL_PROVIDER=anthropic
MODEL_NAME=claude-sonnet-4-5

# Use OpenAI
OPENAI_API_KEY=sk-...
MODEL_PROVIDER=openai
MODEL_NAME=gpt-4o

# Use a local Ollama model (no API key needed)
MODEL_PROVIDER=ollama
MODEL_NAME=llama3.1:8b
OLLAMA_BASE_URL=http://localhost:11434
The startup decision tier is set by AGENT_DECISION_MODE (cortex is the default — a bare deployment reasons on every cycle; set reflex for the fast-path). It’s runtime-flippable via sinker.setMode() or POST /internal/mode — no restart.

Retry race condition

A non-obvious concurrency hazard: the bundle_settled(failed) SSE event fires while the original submit cycle is still executing. The agent handles this with a pendingRetry field:
if (state.runningCycle && retryContext) {
  state.pendingRetry = { ...retryContext, slot };
  return; // don't start a new cycle yet
}

// ... cycle runs ...

} finally {
  state.runningCycle = false;
  if (state.pendingRetry) {
    const pending = state.pendingRetry;
    state.pendingRetry = null;
    await maybeRunCycle(pending.slot, state, pending); // drain
  }
}
This guarantees the retry cycle fires immediately after the current cycle’s finally block with zero additional delay.