Architecture Overview: How Cycles Fits Together
Cycles is a runtime authority for autonomous agents. It sits between your application and the actions that cost money or carry risk.
This page describes the components, how they interact, and where each piece runs.
Prerequisites
This is a reference page. If you haven't set up Cycles yet, start with the End-to-End Tutorial or Deploy the Full Stack.
System overview
Your application talks to the Cycles Server (port 7878) at runtime. The Cycles Admin Server (port 7979) is the management plane where you create tenants, generate API keys, and configure budget ledgers. The Cycles Events Service (port 7980) delivers webhook notifications asynchronously. All three services share the same Redis instance.
Independent release cadences
Runtime, admin, and events services ship patch releases independently. Current versions as of 2026-04-17: cycles-server 0.1.25.13, cycles-server-admin 0.1.25.26, cycles-server-events 0.1.25.6, cycles-dashboard 0.1.25.28. Older admin servers that predate newer query parameters (e.g., sort_by, search) ignore them rather than erroring — the APIs follow an additive-parameter guarantee. See the changelog for the full matrix of minimum versions per feature.
Components
Cycles Protocol
The protocol specification defines the API contract. It is a language-agnostic OpenAPI 3.1 spec that any client or server can implement.
The protocol defines:
- Nine HTTP endpoints for reservations, decisions, balances, and events
- The Subject hierarchy (tenant, workspace, app, workflow, agent, toolset)
- The reserve → execute → commit lifecycle
- Error codes and their semantics
- Idempotency guarantees
- Scope derivation rules
The spec lives at cycles-protocol.
Cycles Server
The reference server implementation. It is a Spring Boot 3.5 application backed by Redis 7+.
What it does:
- Accepts HTTP requests from clients
- Validates API keys and enforces tenant isolation
- Executes atomic budget operations via Redis Lua scripts
- Maintains budget state (allocated, spent, reserved, debt)
- Runs a background expiry sweep to clean up abandoned reservations
Modules:
| Module | Purpose |
|---|---|
cycles-protocol-service-api | REST controllers, security filters, exception handling |
cycles-protocol-service-data | Redis repository, Lua scripts, scope derivation, expiry service |
cycles-protocol-service-model | Shared DTOs and enums |
Why Redis and Lua:
Budget enforcement under concurrency requires atomicity. A reservation must check and update multiple scope counters in a single operation. Redis Lua scripts execute atomically on the server, ensuring no race conditions between concurrent reservations.
Six Lua scripts handle the core operations:
| Script | Operation |
|---|---|
reserve.lua | Check budgets across all scopes, reserve atomically |
commit.lua | Record actual spend, release remainder, handle overage |
release.lua | Return reserved budget to pool |
extend.lua | Extend reservation TTL |
event.lua | Record direct debit without reservation |
expire.lua | Mark expired reservations and release their budget |
Cycles Admin Server
The management plane for Cycles. It runs as a separate Spring Boot 3.5 service on port 7979 and shares the same Redis instance as the Cycles Server.
The optional Cycles Admin Dashboard (Vue 3 SPA) sits in front of this server and exposes its operations as a web UI — useful for day-two ops without crafting curl commands.
What it does:
- Manages tenants (create, list, update, suspend, close)
- Creates and revokes API keys with granular permissions
- Creates budget ledgers and handles funding operations (credit, debit, reset, reset_spent, repay debt)
- Defines policies (caps, rate limits, TTL overrides) matched by scope patterns — stored for future runtime enforcement; not yet evaluated by the Cycles Server in v0
- Validates API keys (used by the Cycles Server for authentication)
- Maintains an audit log of all administrative operations
Modules:
| Module | Purpose |
|---|---|
cycles-admin-service-api | REST controllers, auth interceptor, Spring Boot app |
cycles-admin-service-data | Redis repositories, key service |
cycles-admin-service-model | Shared domain models and DTOs |
Authentication: The admin server uses two auth schemes depending on the endpoint. The split reflects a bootstrap ordering: X-Admin-API-Key handles operations that must exist before any tenant-scoped key (tenants, keys, audit), while X-Cycles-API-Key handles everything else (budgets, policies, runtime).
X-Admin-API-Key — bootstrap / system administration
Set via the ADMIN_API_KEY environment variable. Not scoped to any tenant.
| Endpoint | Method | Purpose |
|---|---|---|
/v1/admin/tenants/* | POST, GET, PATCH | Create, list, update, suspend tenants |
/v1/admin/api-keys/* | POST, GET, DELETE | Create, list, revoke API keys |
/v1/auth/validate | POST | Validate an API key |
/v1/admin/audit/logs | GET | Query audit logs |
X-Cycles-API-Key — tenant-scoped operations
Requires a key created via the admin API with the appropriate permissions.
| Endpoint | Method | Required Permission |
|---|---|---|
/v1/admin/budgets | POST, PATCH | admin:write |
/v1/admin/budgets | GET | admin:read |
/v1/admin/budgets/fund | POST | admin:write |
/v1/admin/policies | POST, PATCH | admin:write |
/v1/admin/policies | GET | admin:read |
/v1/balances | GET | balances:read |
/v1/reservations | GET | reservations:list |
/v1/reservations | POST | reservations:create |
/v1/reservations/{id}/commit | POST | reservations:commit |
/v1/reservations/{id}/release | POST | reservations:release |
/v1/reservations/{id}/extend | POST | reservations:extend |
/v1/decide | POST | (valid key only) |
/v1/events | POST | (valid key only) |
Which header do I use?
If the endpoint manages identity (tenants, API keys, audit) → X-Admin-API-Key. If the endpoint manages money (budgets, policies) or runtime (reservations, balances) → X-Cycles-API-Key.
Key provisioning
The two headers represent different keys with different lifecycles:
X-Admin-API-Keyis a static secret you choose at deploy time. Set it as theADMIN_API_KEYenvironment variable when starting the admin server. There is no API to create or rotate it — you manage it like any infrastructure secret (secrets manager, env vars, etc.).X-Cycles-API-Keykeys are created dynamically viaPOST /v1/admin/api-keys(authenticated with the admin key). Each key is scoped to one tenant and carries explicit permissions. The key secret (e.g.,cyc_live_abc123...) is returned once at creation time.
Bootstrap order: deploy server with admin key → create tenant → create API key → use API key for budgets and runtime operations. See Deploying the Full Cycles Stack for the step-by-step walkthrough.
Why a separate server:
Separating the management plane from the runtime enforcement plane lets you:
- Run the admin server in a restricted network (internal only) while the Cycles Server is accessible to applications
- Scale the enforcement server independently from the admin server
- Apply different access controls to management vs runtime operations
See the Admin API reference for the full API, or the governance spec for the authoritative OpenAPI definition.
Cycles Events Service
The async webhook delivery service. It runs as a separate Spring Boot 3.5 service on port 7980 and shares the same Redis instance.
What it does:
- Consumes delivery jobs from a Redis queue (
dispatch:pending) via BRPOP - Delivers events to webhook endpoints via HTTP POST with HMAC-SHA256 signatures
- Retries failed deliveries with exponential backoff (configurable: default 5 retries, 1s–60s delay)
- Auto-disables subscriptions after consecutive failures (default threshold: 10)
- Expires stale deliveries after configurable max age (default: 24h)
- Cleans up expired ZSET index entries hourly
Why a separate service:
| Concern | Admin Server | Events Service |
|---|---|---|
| Workload | Synchronous CRUD, operator-facing | Asynchronous delivery, variable latency |
| Scaling | Scale with admin traffic | Scale with webhook volume |
| Failure isolation | Admin stays responsive during delivery backlog | Delivery retries don't block admin API |
| Concurrency | Single instance | Multiple instances safe (BRPOP is atomic) |
Optional: If the events service is not deployed, admin and runtime servers operate normally. Events and deliveries accumulate in Redis (bounded by TTL) and are processed when the events service starts.
See Deploying the Events Service for setup and Webhook Event Delivery Protocol for the delivery specification.
Cycles MCP Server
A Model Context Protocol server that exposes Cycles runtime authority as MCP tools. MCP-compatible AI hosts (Claude Desktop, Claude Code, Cursor, Windsurf) discover and call these tools automatically.
What it does:
- Exposes 9 MCP tools covering the full Cycles protocol (reserve, commit, release, extend, decide, balance, events, reservations)
- Ships 3 built-in prompts for integration code generation, budget debugging, and strategy design
- Provides resources for inspecting balances and reservation state
- Wraps the
runcyclesTypeScript client internally — talks to the Cycles Server via HTTP
When to use it:
Use the MCP server when your agent host supports MCP. No SDK integration is needed in the agent's own code — adding the server to the agent's tool configuration is the only setup required. See Getting Started with the MCP Server.
Cycles Spring Boot Starter
A client library that integrates Cycles into Spring Boot applications. It provides two usage modes:
- Declarative — The
@Cyclesannotation wraps methods in a reserve → execute → commit lifecycle automatically via Spring AOP - Programmatic — The
CyclesClientinterface can be injected and used directly for fine-grained control
Key components:
| Component | Purpose |
|---|---|
@Cycles annotation | Declarative budget enforcement on methods |
CyclesAspect | AOP interceptor that drives the lifecycle |
CyclesLifecycleService | Orchestrates reserve/execute/commit/release |
CyclesClient / DefaultCyclesClient | HTTP client using Spring WebClient |
CyclesContextHolder | ThreadLocal access to reservation state mid-execution |
CyclesExpressionEvaluator | SpEL evaluation for dynamic estimates and actuals |
CyclesFieldResolver | Interface for dynamic Subject field resolution |
CommitRetryEngine | Retry engine for transient commit failures |
CyclesProperties | Spring Boot configuration properties |
Request flow
Here is what happens when an @Cycles-annotated method is called:
1. Estimate evaluation
The SpEL expression in the annotation is evaluated against method parameters to produce a numeric estimate.
2. Reservation request
The starter sends POST /v1/reservations to the Cycles server with the Subject, Action, estimate, TTL, and overage policy.
3. Atomic budget check (server side)
The server derives all affected scopes from the Subject, then executes reserve.lua. The Lua script:
- Checks each scope has sufficient remaining budget (
allocated - spent - reserved - debt >= estimate) - Checks no scope has outstanding debt or is over-limit
- If all checks pass, atomically increments the
reservedcounter on every scope - Stores the reservation record with its TTL
4. Decision returned
The server returns one of three decisions: ALLOW, ALLOW_WITH_CAPS, or DENY.
5. Method execution
If allowed, the starter runs the annotated method. During execution:
- A heartbeat thread periodically extends the reservation TTL
- The method can access
CyclesContextHolderto read caps or set metrics
6. Commit
After the method returns, the starter evaluates the actual expression and sends POST /v1/reservations/{id}/commit. The server executes commit.lua to record actual spend and release the unused remainder.
7. Error path
If the method throws, the starter sends POST /v1/reservations/{id}/release to return all reserved budget to the pool.
Data model
All budget state lives in Redis. The key concepts:
Scopes
A scope is a budgeting boundary derived from the Subject hierarchy. A single reservation may affect multiple scopes. For example, a reservation with tenant=acme, workspace=prod, app=chatbot affects three scopes:
tenant:acmetenant:acme/workspace:prodtenant:acme/workspace:prod/app:chatbot
Balances
Each scope tracks:
| Field | Meaning |
|---|---|
allocated | Total budget assigned to this scope |
spent | Committed actual usage |
reserved | Currently held by active reservations |
remaining | allocated - spent - reserved - debt |
debt | Negative balance from overdraft commits |
overdraft_limit | Maximum allowed debt |
is_over_limit | Whether debt > overdraft_limit |
Reservations
Each reservation is stored with:
- Unique ID
- Subject and action metadata
- Reserved amount and unit
- Status (ACTIVE, COMMITTED, RELEASED, EXPIRED)
- TTL and grace period timestamps
- Idempotency key and payload hash
Authentication
The server authenticates every request via the X-Cycles-API-Key header. Each API key is associated with a tenant. The server enforces that subject.tenant matches the key's tenant — a key for tenant A cannot create reservations for tenant B.
Deployment topology
A typical deployment:
Multiple Cycles server instances can run behind a load balancer. All state is in Redis, so the server is stateless. The admin server is typically on an internal network, accessible only to operators and CI/CD pipelines. The events service is optional — if deployed, it consumes delivery jobs from Redis and delivers webhooks with HMAC-SHA256 signatures.
Non-Spring clients (Python, TypeScript/Node.js, Go) can use the protocol directly via HTTP — the client libraries are convenience layers, not a requirement. MCP-compatible agents (Claude Desktop, Claude Code, Cursor, Windsurf) can use the Cycles MCP Server for a zero-code integration path.
Next steps
- Tenants, Scopes, and Budgets — how tenants, scopes, and budgets work together as a unified model
- Deploying the Full Cycles Stack — zero to working deployment with all components
- Self-Hosting the Cycles Server — server-specific configuration and deployment
- API Reference — interactive endpoint documentation
- Getting Started with the MCP Server — add runtime authority to Claude Desktop, Claude Code, Cursor, or Windsurf
- Getting Started with the Python Client — integrate with your Python app
- Getting Started with the TypeScript Client — integrate with your TypeScript/Node.js app
- Getting Started with the Spring Boot Starter — integrate with your Spring app