Self-hosted server that enforces hard limits on AI agent spend, risk, and tool actions before execution. Reference implementation of the Cycles Protocol (v0.1.25) — a reservation-based control plane for multi-tenant AI agent runtimes.
Drop-in budget governance for OpenAI, Anthropic, MCP servers, OpenAI Agents SDK, LangChain, and custom agent frameworks. Reserve cost up front, commit on success, release on failure — with concurrency-safe enforcement across thousands of agents and tenants.

Cycles enforcing a hard budget cap on a runaway agent loop — see the runaway demo for the full walkthrough.

Action authority — Cycles blocking an unauthorized tool call before the side effect lands. Action-authority demo →
Starts the full stack (Redis + Cycles Server + Admin Server), creates a tenant, API key, and budget, and verifies the full reserve/commit lifecycle:
./quickstart.shPrerequisites: Docker and Docker Compose v2+. No Java or Maven required.
# Build from source and start (no local Java/Maven required)
docker compose up --buildServer starts on port 7878. API docs are available when SpringDoc is
enabled and require X-Admin-API-Key.
# Download docker-compose.prod.yml, then:
docker compose -f docker-compose.prod.yml upThis pulls the latest image from ghcr.io/runcycles/cycles-server.
Prerequisites: Java 21+, Maven, Docker (for Redis)
# 1. Start Redis
docker run -d -p 6379:6379 redis:7-alpine
# 2. Build
cd cycles-protocol-service
./build-all.sh
# 3. Seed a sample budget
./init-budgets.sh
# 4. Run
REDIS_HOST=localhost REDIS_PORT=6379 \
java -jar cycles-protocol-service-api/target/cycles-protocol-service-api-*.jarServer starts on port 7878. API docs are available when SpringDoc is
enabled and require X-Admin-API-Key.
HTTP client
│ X-Cycles-API-Key
▼
Spring Boot 3.5 (port 7878)
│ ApiKeyAuthenticationFilter
│ Controllers → Repository → Lua scripts (atomic)
▼
Redis 7+
│ event:{id}, delivery:{id}, LPUSH dispatch:pending
▼
cycles-server-events (port 7980)
│ BRPOP → HTTP POST with HMAC-SHA256 signature
▼
Webhook receivers
Event emission: Runtime operations emit events to the shared Redis dispatch queue. The events delivery service (cycles-server-events) picks them up and delivers via HTTP POST with HMAC-SHA256 signing.
Modules (under cycles-protocol-service/):
| Module | Purpose |
|---|---|
cycles-protocol-service-model |
Shared request/response POJOs |
cycles-protocol-service-data |
Redis repository + Lua scripts |
cycles-protocol-service-api |
Spring Boot controllers + auth |
All protocol endpoints require X-Cycles-API-Key header authentication — except GET /v1/evidence/{id}, a public capability URL (the unguessable evidence_id is the bearer secret), and GET /v1/.well-known/cycles-jwks.json, which publishes public verification keys for CyclesEvidence signer resolution (see CyclesEvidence). Operational endpoints are separate: liveness/readiness are public for probes, while aggregate actuator, Prometheus, API docs, and Swagger require X-Admin-API-Key when enabled.
| Endpoint | Method | Description |
|---|---|---|
/v1/decide |
POST | Evaluate budget decision without reserving |
/v1/reservations |
POST | Create budget reservation |
/v1/reservations |
GET | List reservations (with pagination/filters) |
/v1/reservations/{id} |
GET | Fetch a single reservation |
/v1/reservations/{id}/commit |
POST | Record actual spend |
/v1/reservations/{id}/release |
POST | Return reserved budget |
/v1/reservations/{id}/extend |
POST | Extend reservation TTL |
/v1/events |
POST | Direct debit without prior reservation (returns 201) |
/v1/balances |
GET | Query budget balances for scopes |
/v1/evidence/{id} |
GET | Fetch a signed CyclesEvidence envelope by id (public capability URL — no API key) |
/v1/.well-known/cycles-jwks.json |
GET | Fetch the public CyclesEvidence signer JWK Set for authority resolution and rotation history (no API key) |
Every budget decision can be backed by a tamper-evident, signed audit envelope. On decide / reserve / commit / release — and on budget/lifecycle denials (e.g. a 409 BUDGET_EXCEEDED error) — the response carries an optional cycles_evidence ref ({ evidence_id, cycles_evidence_url }). cycles-server computes the content-addressed evidence_id synchronously and returns it on the wire; the cycles-server-events tier asynchronously Ed25519-signs the envelope and stores it, served back verbatim at the public GET /v1/evidence/{id} capability URL. A consumer (e.g. an APS gateway) records the evidence_id to bind its own receipt to the decision, then fetches + verifies the envelope offline — no live ledger access needed.
Evidence is off until configured: set the shared identity env vars (below). The private signing key lives only in cycles-server-events — see its identity enablement runbook. Wire shape: cycles-protocol-v0.yaml (CyclesEvidenceRef, getEvidence, getEvidenceJwks) + cycles-evidence-v0.2.yaml, whose signer-key resolution/JWKS layer is additive to the raw-hex envelope shape.
All commands run from the cycles-protocol-service/ directory.
cd cycles-protocol-service
# Full build (compile + unit tests + package)
mvn clean install
# Or use the wrapper script
./build-all.shThe fat JAR is produced at cycles-protocol-service-api/target/cycles-protocol-service-api-<version>.jar (where <version> is the revision property in cycles-protocol-service/pom.xml — e.g. 0.1.25.14).
Two Docker Compose files are provided for different use cases:
| File | Use case | Command |
|---|---|---|
docker-compose.yml |
Development — builds from source inside Docker (multi-stage build, no local Java/Maven needed) | docker compose up --build |
docker-compose.prod.yml |
Production / end-user — pulls pre-built image from GHCR | docker compose -f docker-compose.prod.yml up |
Both start Redis 7 and the cycles-server on port 7878.
Pre-built images are published to GitHub Container Registry on each release:
ghcr.io/runcycles/cycles-server:latest
ghcr.io/runcycles/cycles-server:<version> # e.g. 0.1.25.14
The Cycles Server listens on HTTP at port 7878 by design — TLS termination is the responsibility of the ingress layer in front of it. The recommended deployment topology:
client → [TLS-terminating reverse proxy] → cycles-server:7878 (HTTP)
↓
redis:6379 (TCP, password-protected)
Use any TLS-terminating proxy in front of cycles-server. Common choices:
- nginx with Let's Encrypt — minimal config, well-documented
- Caddy — automatic Let's Encrypt + HTTP/3
- Traefik — natural fit for Docker / Kubernetes
- AWS ALB / GCP Load Balancer / Cloudflare — managed TLS at the edge
The cycles-server itself doesn't need to know it's behind a proxy. Standard X-Forwarded-For / X-Forwarded-Proto headers are honored by Spring Boot's defaults.
Why HTTP at the app layer: keeps the container minimal (no cert renewal logic in the app), lets ops centralize TLS posture (cipher suites, HSTS, mTLS) at the proxy, and matches how production Spring Boot services are typically deployed.
| Path | Recommendation |
|---|---|
| Public → reverse proxy | TLS 1.2+ only, modern cipher suites, HSTS enabled |
| Reverse proxy → cycles-server | Same private network or VPC; not exposed publicly |
| cycles-server → Redis | Same private network; Redis password set via REDIS_PASSWORD env var; for highest assurance, also enable Redis TLS (Redis 6+ supports it natively) |
| Admin server (port 7979) | Internal network only — never expose publicly. The admin server is the management plane (tenants, budgets, API keys); compromise here is total. |
REDIS_PASSWORD— never run Redis without one in productionADMIN_API_KEY— the bootstrap admin key for the admin server and cycles-server operational endpoints (32+ random bytes recommended)WEBHOOK_SECRET_ENCRYPTION_KEY— encrypts webhook subscriber secrets at rest in Redis (32 bytes, base64)EVIDENCE_SERVER_ID+EVIDENCE_SIGNING_SIGNER_DID— (optional) the public CyclesEvidence identity; set both to enable evidence, byte-identical to thecycles-server-eventsvalues.cycles-serverholds only the public half — the private signing key lives incycles-server-events. Leave unset to run without evidence. See the enablement runbook.EVIDENCE_SIGNING_KID— (optional) public JWKkidlabel forGET /v1/.well-known/cycles-jwks.json; defaults to the first 16 hex chars ofEVIDENCE_SIGNING_SIGNER_DID. Not a signing key.EVIDENCE_SIGNING_NBF_MS— (optional) active key validity start in epoch milliseconds; default0is appropriate for a never-rotated key.EVIDENCE_SIGNING_RETIRED_KEYS— (optional) JSON array of retired public keys for JWKS rotation history (signer_did,kid,nbf_ms,exp_ms).
See Configuration below for the full env-var matrix.
The test suite is split into four categories, each gated by a surefire tag or Maven profile so PR CI stays fast while heavier suites run on demand or on a schedule.
cd cycles-protocol-service
# Unit tests only (no Docker required) — default PR CI feedback loop
mvn test
# Unit + integration tests (requires Docker for Testcontainers Redis)
mvn clean install -Pintegration-tests
# Concurrent load benchmarks (measures throughput + latency percentiles)
mvn test -Pbenchmark
# Property-based concurrent invariant checks (jqwik)
mvn test -pl cycles-protocol-service-api -am -Pproperty-tests*IntegrationTest.java classes use Testcontainers to spin up a Redis instance automatically. Excluded from the default build; enabled via the -Pintegration-tests Maven profile.
jqwik-driven property tests that force concurrent interleavings and assert system-wide invariants. Four property tests ship today:
| Test | Invariants |
|---|---|
BudgetExhaustionConcurrentPropertyTest |
Under REJECT overage policy: no overdraw, no dual-terminal states, no leaked ACTIVE reservations after sweep. |
OverdraftConcurrentPropertyTest |
ALLOW_IF_AVAILABLE never creates debt; ALLOW_WITH_OVERDRAFT respects overdraft_limit; ledger invariant (allocated = remaining + spent + reserved + debt) holds under contention. |
ScopeAttributionConcurrentPropertyTest |
Multi-scope spend attribution: spent[level] equals Σ charged_amount at every level for scope chains of depth 1–6. |
AuditLogCompletenessPropertyTest |
1:1 mutation↔audit-entry on admin-driven releases; dual-index consistency (audit:logs:_all + audit:logs:{tenant}); required fields including metadata.actor_type=admin_on_behalf_of. |
Tagged @Tag("property-tests") and excluded from default PR CI. Run locally with -Pproperty-tests; a nightly GitHub Actions workflow (.github/workflows/nightly-property-tests.yml) runs at 06:00 UTC with deeper coverage.
Try count is configurable:
| Mode | Command | Try count | Runtime |
|---|---|---|---|
PR-speed default (from jqwik.properties) |
mvn test -Pproperty-tests |
20 | ~20 s |
| Nightly CI (5× coverage) | mvn test -Pproperty-tests -Djqwik.defaultTries=100 |
100 | ~2 min |
| Manual deep run | mvn test -Pproperty-tests -Djqwik.defaultTries=500 |
500 | ~10 min |
The property annotation deliberately does not set tries — the count comes from cycles-protocol-service-api/src/test/resources/jqwik.properties (defaults to 20) and can be overridden with -Djqwik.defaultTries=<N>. An annotation literal would win over the system property and silently ignore the override.
Reproducing a failure: jqwik prints a seed = <number> line on failure. Pass it back via -Djqwik.seeds.tries.default=<number> to replay the exact same interleaving against the fixed code.
| Variable | Default | Description |
|---|---|---|
REDIS_HOST |
localhost |
Redis hostname |
REDIS_PORT |
6379 |
Redis port |
REDIS_PASSWORD |
(empty) | Redis password |
cycles.expiry.interval-ms |
5000 |
Background expiry sweep interval (ms) |
JAVA_OPTS |
(empty) | JVM options (e.g. -XX:MaxRAMPercentage=75 -XX:+UseG1GC) |
LOGGING_STRUCTURED_FORMAT_CONSOLE |
(unset) | Set to ecs or logstash for JSON logging in production |
ADMIN_API_KEY |
(empty) | Admin key for admin-on-behalf-of runtime paths and operational endpoints (/actuator/prometheus, aggregate actuator, docs). Production Compose requires it. |
CYCLES_METRICS_TENANT_TAG_ENABLED |
true |
Include tenant labels on custom domain metrics. Production Compose sets this to false to avoid tenant-id disclosure and high-cardinality series. |
CYCLES_EVENTS_EMIT_THREADS |
0 |
Worker count for non-blocking runtime event emission. 0 uses a CPU-derived default. |
CYCLES_EVENTS_EMIT_QUEUE_CAPACITY |
10000 |
Bounded in-process queue for non-blocking runtime event emission. When full, only the event side effect is dropped and logged; API ledger mutations are unchanged. |
SPRINGDOC_API_DOCS_ENABLED |
true |
Expose /api-docs. Production Compose sets this to false; when enabled, access requires X-Admin-API-Key. |
SPRINGDOC_SWAGGER_UI_ENABLED |
true |
Expose /swagger-ui.html. Production Compose sets this to false; when enabled, access requires X-Admin-API-Key. |
redis.pool.max-total |
128 |
Max Redis connections |
redis.pool.max-idle |
32 |
Max idle Redis connections |
redis.pool.min-idle |
16 |
Min idle Redis connections |
WEBHOOK_SECRET_ENCRYPTION_KEY |
(empty) | AES-256-GCM key for webhook signing secret encryption at rest (base64, 32 bytes). Must match admin + events services. Generate: openssl rand -base64 32 |
EVENT_TTL_DAYS |
90 |
Redis TTL for event:{id} keys (days) |
DELIVERY_TTL_DAYS |
14 |
Redis TTL for delivery:{id} keys (days) |
EVIDENCE_SERVER_ID |
(empty) | Public CyclesEvidence issuer base including /v1; set with EVIDENCE_SIGNING_SIGNER_DID to emit cycles_evidence refs. Must match cycles-server-events. |
EVIDENCE_SIGNING_SIGNER_DID |
(empty) | Public raw Ed25519 key (64 hex) stamped as signer_did; must match the events worker signing key public half. |
EVIDENCE_SIGNING_KID |
(empty) | Optional public JWK kid label for the signer JWKS; defaults to the first 16 hex chars of EVIDENCE_SIGNING_SIGNER_DID. |
EVIDENCE_SIGNING_NBF_MS |
0 |
Active JWKS key validity start, epoch ms inclusive; set to the rotation time when rotating keys. |
EVIDENCE_SIGNING_RETIRED_KEYS |
(empty) | Optional JSON array of retired public signing keys with bounded validity windows for evidence rotation history. |
The runtime server emits events to the shared Redis dispatch queue for:
reservation.denied— reserve or decide returned DENYreservation.commit_overage— commit actual exceeded reservation estimatereservation.expired— reservation TTL expired without commit/release (via background sweeper)budget.exhausted— remaining budget reached 0 after an operationbudget.over_limit_entered— scope entered over-limit state (debt > overdraft_limit or ALLOW_IF_AVAILABLE cap)budget.debt_incurred— commit/event created debt via ALLOW_WITH_OVERDRAFT
These events are delivered by cycles-server-events to webhook subscribers via HTTP POST with HMAC-SHA256 signing. Event emission is non-blocking — failures are logged but never affect the API response.
If the events service is down: Events and deliveries accumulate in Redis with TTL (90d/14d). When the events service restarts, deliveries older than 24h are auto-failed. The admin and runtime servers continue operating normally.
In the full-stack production Compose deployment, WEBHOOK_SECRET_ENCRYPTION_KEY
is required because admin stores webhook secrets and events decrypts them for
delivery signing. The events worker exposes management endpoints on port 9980;
its internal worker port 7980 is not published by the production full-stack
file.
GET /actuator/health/readiness
Reports application readiness plus the Redis ledger dependency. If Redis is
unreachable, the endpoint reports DOWN so container and orchestrator
healthchecks stop treating the process as ready for traffic. Liveness remains
available at GET /actuator/health/liveness and does not include Redis.
GET /actuator/prometheus
Exposes JVM, HTTP, and Spring Boot metrics in Prometheus format. Prometheus is
protected with X-Admin-API-Key; configure Prometheus to send that header or
inject it at trusted ingress. Production Compose also disables tenant labels on
custom domain metrics by setting CYCLES_METRICS_TENANT_TAG_ENABLED=false.
In addition to Spring Boot's auto-emitted http_server_requests_seconds, the service exposes seven domain-level counters under the cycles_* namespace (reserve / commit / release / extend / expired / events / overdraft). Operators can alert on denial rates, overdraft incidence, and per-tenant activity without reverse-engineering it from HTTP status codes.
Full metric inventory, tag semantics, ready-to-paste Prometheus alert rules, SLO definitions, and an incident playbook live in OPERATIONS.md.
CHANGELOG.md— release notes for downstream consumers (Docker / JAR)OPERATIONS.md— operator runbook: metrics inventory, alert recipes, SLOs, incident playbookAUDIT.md— engineering history and rationale for each release- Cycles Documentation — full docs site
- Deploy the Full Stack — deployment guide with server setup
- Server Configuration Reference — all server configuration options
cycles-protocol-service/README.md— core concepts, authentication, error codes, and the Redis data model