architecture

How ebb-ai is built.

A small, deliberately scoped system. One scheduler. One MCP surface. Grid feeds in front, provider adapters behind, carbon receipts in between.

System diagram

Where each component sits and how the data flows.

AGENT HOSTS Claude Desktop Claude Code OpenClaw Codex CLI / custom MCP SERVER (stdio) @ebb-ai/mcp 4 tools — forecast · recommend · schedule · status SCHEDULER CORE @ebb-ai/core · ebb_ai (py) Queue · Scorer · Dispatcher · Tracer cleanest in-deadline window · carbon-budget guard SQLite durable queue · per-task receipts GRID FEEDS Electricity Maps · mock fallback (planned) WattTime marginal (planned) GridStatus.io ISO data PROVIDER ADAPTERS Anthropic · OpenAI sync API + Batch API (50% off) (planned) Gemini, Ollama (local) RECEIPTS / AUDIT Per-task carbon receipts SQLite — when, where, gCO₂e (planned) SEC ESG export

The MCP server is a thin protocol surface in front of the scheduler. The scheduler core does the work and is the same implementation used by the in-process TypeScript library and the Python port (same algorithms, same receipt schema, two host languages).

Data sources

Grid intensity feeds come from government / TSO authorities — the same sources that ISO/RTO grid operators and DOE energy-usage studies cite. ebb-ai does no data collection of its own; it routes dispatch decisions through public-sector telemetry.

Every per-task carbon receipt carries the exact intensity value (and source identifier) used at scoring time, so the audit trail is reproducible — operators can later verify any individual dispatch against the public data feed it relied on.

Data flow, end to end

  1. Ingest. A task arrives via defer(task, deadline) (library) or schedule_task(prompt, deadline) (MCP). The task body is captured (closure, JSON-serialized request, or a provider-adapter call descriptor) and persisted to SQLite with a UUID task_id. For non-committal planning — when the agent wants to see when the cleanest window falls before deciding whether to commit — there's recommend_window(deadline, region), which returns the recommended window, top-3 alternatives, and a savings-vs-now number without ever touching the queue.
  2. Validate. Deadline must parse as ISO-8601 and be in the future. If not, the call fails fast with InvalidDeadlineError. A taskId, if caller-supplied, must be unique.
  3. Forecast. The scheduler asks the configured grid feed for the next N hours of carbon intensity for the task's region. With an Electricity Maps API key, this hits the live API (5-second timeout). Without one, the feed falls back to a deterministic UTC-based sinusoidal mock so the whole stack still runs end-to-end.
  4. Score. Each hour inside the deadline window is scored. The cheapest-carbon window wins. If a carbonBudgetG was supplied, windows that exceed the budget are dropped first; if no window survives, the task fails with CarbonBudgetExceededError.
  5. Wait. A timer is set for the chosen window. Long horizons are chained to avoid Node's 32-bit-millisecond setTimeout overflow.
  6. Dispatch. When the timer fires, the scheduler calls the task body. If the body is a provider-adapter descriptor (Anthropic / OpenAI), and the deadline is more than 24 hours out, the adapter routes through the provider's Batch API for the 50% discount. Otherwise it uses the standard sync endpoint.
  7. Receipt. The dispatch writes a carbon receipt: timestamp, region, intensity (g CO₂/kWh) from the forecast entry used to score the window (not a freshly-fetched value — the audit trail is honest about what was scored), tokens in/out, duration, dollar cost saved versus peak.
  8. Return. The result resolves the caller's promise (library) or becomes visible via check_queue_status(task_id) (MCP). The receipt persists.

What's deliberately not in v0.8

See docs →