Reference implementation of the Cycles Budget Authority API (v0.1.25) — a reservation-based budget control service for AI agents and workflows.
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. Interactive API docs: http://localhost:7878/swagger-ui.html
# 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. Interactive API docs: http://localhost:7878/swagger-ui.html
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 endpoints require X-Cycles-API-Key header authentication.
| 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 |
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 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 |
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) |
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.
GET /actuator/health
GET /actuator/prometheus
Exposes JVM, HTTP, and Spring Boot metrics in Prometheus format. Both endpoints are unauthenticated. Configure your Prometheus scrape target to http://<host>:7878/actuator/prometheus.
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