Sessions and runs
A session ties one agent to one wallet and its virtual allocation. Circuit supplies a fresh AgentContext to eachrun invocation. Implement run(agent) for the strategy; SDK methods execute the operations. Use Memory to persist strings between invocations; session memory ends with the session.
Only one run executes per session. Start admits its initial run. Top-up allocates the amount and admits another run atomically, even while busy. Stop ends the session, cancels queued work and releases its allocation; an executing run finishes and records its own outcome. It runs no code: holdings, positions and resting orders stay open for the user to close by asking Circuit. Account requirements inferred from the starting asset must be ready before agent code executes. Hosted execution requires runtime admission: Stop winning that admission race prevents execution, while an already-admitted run may continue.
An agent has one active version. Saving code moves every session, including other users’ sessions of a shared agent, to it from its next run; queued and executing runs keep their admitted revision. Setting overrides affect later admissions, and an override the current setting definitions reject no longer applies. Run status reports the codeHash each run admitted; null means no historical revision was recorded.
Agent context
The runtime passesAgentContext to run on each trigger; agents never instantiate it, and run returns no value. It exposes sessionId, walletAddress, resolved settings, trigger, fixed allocation, and the SDK resources: log, memory, data, operations, transactions and wallet, transfers, swaps, fomo, solana and watchedWallets, hyperliquid, lighter and lighterRobinhood, papertrade, polymarket and kraken. trigger.kind is schedule, manual, price, resolution, allocation (deposit or withdraw), position, fill, news, walletTrade or webhook. Price-trigger values are decimal strings; venue triggers identify the Hyperliquid coin or Polymarket outcome token; news triggers carry the matched headline, summary, source, url and emittedAt; webhook triggers carry the provider, the provider’s event id, and the payload exactly as the provider sent it, typed unknown: check its fields before using them, and pass its numbers to the decimal helpers as strings; walletTrade triggers carry the watched wallet (lowercased on EVM) and one trade it made, whose venue is chain (network, transaction, sold and bought assets with whole-token amounts, and where the bought token arrived, boughtOn and recipient), hyperliquid, lighter, lighterRobinhood or derive (one fill’s asset, side, size, price, at and id, Hyperliquid adding positionAfter) or papertrade (a position’s action, positionId, asset, side, size, entryPrice and leverage); a Papertrade change also names its positionId, and its before or after adds margin and leverage. Methods return data or throw structured errors.
Every amount the SDK takes or returns is a decimal string of whole tokens; the engine rounds spends down and minimums up to each token’s smallest unit. Do money math with the circuit:sdk helpers decimalAdd, decimalSub, decimalMultiply, decimalDivide / decimalDivideCeil(numerator, denominator, scale), decimalMin, decimalMax, decimalIsZero and decimalCompare: they are exact, never emit exponents, and need no configuration. Floats, Number(), parseFloat and default-precision decimal.js round money silently, and BigInt() of a scaled product throws once its decimals exceed the scale: products grow decimals, so truncate with decimalDivide(x, "1", scale) first.
Methods accept named requests, such as operations.retrieve({ operationId }); no-input methods take no arguments. Money writes return completed operations with kind-specific result and typed receipts. See error handling for durable references, failure stages and cross-run observation.
Memory
agent.memory stores strings within a session. Use get({ key }), set({ key, value }), delete({ key }) and list({ prefix? }); keys are nonempty and deletion is idempotent. Missing entries return null; entries contain key, value and ISO updatedAt. Keep a session under 1,000 keys: writes past that still succeed but are flagged, and list fails with INVALID_INPUT once a session holds more than 10,000; rewrite or delete keys instead of adding one per run.
list returns all matching keys and timestamps in UTF-8 key order. Omit prefix for every key. An empty array means no matches; failed reads throw ApiError, never partial results. Writes are last-write-wins and enumeration is not a concurrent snapshot.
Persist strategy state and admitted operation references, never a second execution status or holdings ledger. agent.allocation owns current virtual inventory. Memory is session-only; scope arguments are invalid. Stopping a local session clears its keys.
Triggers
Declare triggers in circuit.toml. Rolling schedules count from the schedule’s last firing, which manual, top-up and other trigger runs never move; resuming a session restarts the count; fixed schedules align to clock boundaries (hourly at:00, or every fifteen minutes at :00, :15, :30, :45). Hosted sessions fire at a stable second within the scheduled minute, chosen per session, so agents never start in lockstep. Every invocation receives a fresh agent.allocation, refreshed after each money write. A trigger that fires while a run executes waits its turn: a session keeps the newest 200 fired triggers, handles them in order, and drops the oldest when triggers arrive faster than the agent handles them, which the session log says at most once a minute.
Continuous agents
Setcontinuous = true in circuit.toml for one long-lived process instead of one run per trigger. run(agent) loops on await agent.next({ until }), which returns the next fired trigger or null at until (epoch milliseconds), and refreshes agent.allocation before returning, except after a walletTrade of another wallet, which leaves it unchanged. Continuous agents declare no schedule triggers; their cadence is the until they pass. In a run that is not continuous, agent.next() throws a BAD_REQUEST error that names the fix instead of waiting. Memory in variables lives while the process does; keep anything you need across restarts in Memory.
Circuit restarts the process at its next agent.next() when the agent’s code or the session’s settings change, the session is topped up or run manually, or the runtime is updated: that agent.next() never returns, the process exits once its pending SDK calls finish, and a new run starts with agent.trigger.kind === "restart" (or the queued run’s trigger). No exception reaches your code and money calls are never interrupted; a process that does not reach agent.next() within five minutes of a restart request is stopped and its run fails. agent.next() delivers each fired trigger once: a run that fails after receiving one does not see it again, and the session log names the last trigger that run received. A run that ends for any other reason restarts at most once a minute. The two-hour run deadline does not apply; on Circuit, five minutes without a Circuit call still stops the run, and waiting in agent.next() counts as calls. circuit backtest replays a continuous agent on its replay clock: agent.next() returns the triggers that fire minute by minute and null at until. circuit run runs it until you stop it, and agent.next() returns its price, fill and position triggers as they fire on live data; price triggers need market data, so they fire under circuit run --simulated and are refused by a plain wallet run, and other trigger kinds fire only for agents Circuit hosts.
Execution limits and failures
On Circuit, five minutes without a Circuit call stops a run. SDK calls, including logging and data reads, reset the silence clock; readingagent.allocation, local sleep, hangs and computation do not. Return and continue on a later trigger when work requires a longer wait. A run fails two hours after it is due, including time spent queued behind the session’s previous run; Circuit, circuit run and circuit backtest stop it at that deadline. Uncaught exceptions fail the execution and are logged automatically.
Hosted runs reach outside services only over TCP ports 443 (HTTPS) and 80 (HTTP), each run from a public IPv4 address of its own (an AWS us-east-1 Elastic IP) that no other active run shares. Other ports, private addresses, and UDP other than DNS are refused. Local circuit run has your machine’s network.
Each money operation has its own lifecycle deadline: three minutes for a cross-chain swap and thirty seconds for everything else, counted from when its turn begins, so operations queued behind a conflicting one on the same wallet don’t spend it waiting. High-level SDK calls may sequence multiple operations, such as approval followed by a newly quoted swap; each is separately admitted, and failure stops the sequence without retrying. Earlier successful transactions remain recorded. Stopping cannot undo provider-accepted effects. A deadline ends the wait, not the outcome: a submitted operation stays pending until evidence settles it, past its settlement bound it becomes unproven while Circuit keeps watching, and nothing is ever resubmitted.