Your keys and your API

One key starts it. The port stays on your machine.

A fresh instance needs exactly one secret to be useful — an LLM provider key — and it listens on one port bound to loopback. Everything else here is the next key and the next call: where a second key goes so it does not widen what every agent can reach, every outside service the container can reach once you give it one, the token you deliberately do not set, and the handful of routes worth knowing before you learn the console.

Which key goes where — click a location

./.env — the hub, and the one required secret

Sits next to the compose file, copied from .env.example. Section 1 is the only required part: pick a provider and paste one key. This file is read as environment, never mounted into the artifact tree.

What to know

Shape


        

The blast radius is the point. Bundled agents share the hub .env. Every key you add there becomes reachable by every bundled agent you invoke — which is convenient on day one and too broad by week three. agent-env/<id>.env exists so a connector key can belong to one agent instead of all of them.

Every outside service this container can reach

One key is required. Everything below it is off until you add a key. This is the outbound surface the buyer template covers — you pay each provider directly, and nothing here is reachable just because it was bundled. Two further blocks sit beyond it, noted under the table.
Service What it unlocks Env keys Ships as
Reasoning — pick exactly one
OpenAI The default provider. Model and a reasoning-effort knob alongside it. DAL_LLM_PRIMARY=openai
OPENAI_API_KEY
OPENAI_MODEL
Required
pick one
Anthropic Same job, different provider. DAL_LLM_PRIMARY=anthropic
ANTHROPIC_API_KEY
ANTHROPIC_MODEL
Commented
KimiOpenAI-compatible Any OpenAI-shaped endpoint, which is why the endpoint is a variable. DAL_LLM_PRIMARY=kimi
DAL_AI_KEY
DAL_AI_ENDPOINT
Commented
Search
Brave Search Web search inside jobs and the tool loop. DAL_WEB_SEARCH_PROVIDER=brave
DAL_BRAVE_SEARCH_API_KEY
Off
SerpAPI The same slot, the other provider. One or the other, not both. DAL_WEB_SEARCH_PROVIDER=serpapi
DAL_SERPAPI_KEY
Off
Mail and the Google surfaces
GmailOAuth, desktop client Reading and drafting real mail. Needs a one-time consent step run against the container, not just a pasted key. GMAIL_CLIENT_ID
GMAIL_CLIENT_SECRET
EMAIL_PROVIDER=gmail
Off
consent required
Calendar · Sheets · Drive The coordinator calendar pane, spreadsheet import, kit export. No new key — extra scopes on the same consent, so granting them means consenting again. GMAIL_SCOPE
COO_OPS_CALENDAR_ID
KIT_SHEET_ID
Off
re-consent
IMAP / SMTP Mail without Google. App password rather than OAuth. EMAIL_PROVIDER=imap_smtp
EMAIL_USERNAME / EMAIL_PASSWORD
EMAIL_IMAP_HOST / EMAIL_SMTP_HOST
Off
Posting and notifications
X Drafting and posting. Four credentials, because it is OAuth 1.0a. X_API_KEY / X_API_SECRET
X_ACCESS_TOKEN / X_ACCESS_TOKEN_SECRET
DAL_COO_AUTOPOST=0
Off
draft-only switch
Telegram Where the coordinator reaches you when something needs a person. DAL_COO_P2A_TRANSPORT=telegram
TELEGRAM_BOT_TOKEN
DAL_COO_P2A_TELEGRAM_CHAT
Off
Discord Same notification slot, webhook instead of a bot. DAL_COO_P2A_TRANSPORT=discord
DISCORD_WEBHOOK_URL
Off
SDKs used by bundled agents
Cursor SDKthe engineer agent How the bundled engineer actually edits code. A good first candidate for per-agent isolation rather than the hub file. CURSOR_API_KEY
CINNAMON_DRY_RUN=1
agent-env/cinnamon_agent.env
Off
dry-run switch

Two defaults worth knowing before you add any of these. Notifications ship pointed at a file, not a network — DAL_COO_P2A_TRANSPORT=file writes to a local outbox, so an instance with no connectors still has somewhere to tell you things. And the switches above that read =0 or =1 — draft-only posting, dry-run code edits, email mutations — ship commented out in the template, which means the profile default applies rather than the value you see. If you want one of them guaranteed, uncomment it and set it yourself. The gate you can rely on without reading any of that: no key, no connector, and anything leaving the building still waits on Review.

Payments and live travel booking go further, and are deliberately not on this page. Both have real key blocks. Neither is a single credential you paste in: the payment path sits behind its own live-enable switch layered over amount ceilings, a currency allowlist, webhook verification, and an approval check before anything executes — and the travel skill draws its product boundary at research, leaving ticket purchase and merchant-of-record outside unless you deliberately turn a live connector on. Those blocks want real domain knowledge and caution to run, so the variables are not published here; they are documented in the kit beside the code that uses them — packages/payment/README.md and agents/saffron_agent/docs/providers.md.

Travel research, though, needs no new credential. Flight, hotel, and directions research all ride the search key you already set — they resolve through the same shared SerpAPI client as the rest of the kit. So the trip packet is reachable from the connector you configured for web search, and only live booking is a separate decision.

Two things about the door

The token you do not set

Leave DAL_COO_API_TOKEN unset. First start writes one into .dal-docker/ — so the instance has a token you never chose and never pasted.

If you do set it, it becomes a real requirement: POST /api/tasks/run-due wants it, and anything on your host that calls wake has to send it too. The shipped wake scripts already source .env and add Authorization: Bearer …, which is why a hand-rolled cron line is the thing that breaks.

A sudden 401 on a local call usually means a token got set and something calling in does not know.

The port is a decision you own

Compose publishes ${DAL_COO_DOCKER_BIND:-127.0.0.1}:${DAL_COO_DOCKER_PORT:-4040}:4040. Loopback is the default, not a promise the kit makes for you: the bind is a variable, so reachable-from-the-LAN is something you would be choosing.

Worth choosing carefully because this container holds your provider keys. The compose file makes the same judgement in the one place it could go wrong — the optional public-facing services are deliberately given no env_file, with the reason written in place: a front door that only matches paths "has no business holding the provider keys in .env."

If you change the host port, use the new one in every URL below.

What you can actually call — grouped by the spine

Prove

Is it alive, and are the keys good

  • GET/healthLiveness. The same check compose uses.
  • GET/api/statusReturns JSON — the "it installed" proof.
  • GET/api/debug-aiOpen this after fixing a key, when you see an AI error.
curl -fsS http://127.0.0.1:4040/api/status
Do

Say something, or queue work

  • POST/api/messageTalk to the coordinator.
  • POST/api/taskPut work in the queue instead of a chat turn.
curl -sS -X POST \
  http://127.0.0.1:4040/api/task \
  -H 'Content-Type: application/json' \
  -d '{"description":"…"}'
Keep

Tomorrow happens without you

  • POST/api/wakeDrains due work. Nothing inside the container calls this — your host must.
  • GET/api/autonomy/stateWake interval and last_work_at — how you confirm the clock is real.
  • POST/api/tasks/run-dueRuns due tasks. Token-gated when you set one.
curl -fsS -X POST \
  http://127.0.0.1:4040/api/wake \
  -H 'Content-Type: application/json' \
  -d '{"max_items":10}'
Day

What waits for you, what you can invoke

  • GET/api/review/pendingThe things holding for a person. The honest center of the day.
  • GET/api/targetsEvery capability you may invoke, bundled and your own.
  • POST/api/coordinator/invokeCall one by capability_id.
  • POST/api/operator/work/<job_id>/runRun one catalog job now.
curl -sS http://127.0.0.1:4040/api/review/pending

These are the Docker URLs. Every call above assumes the published loopback port, so if you changed DAL_COO_DOCKER_PORT, use yours. Command lists and version tags come from INSTALL.md; console surfaces from OPERATOR_GUIDE.md.