Using ACPpotamia.
ACPpotamia sits between your agents and the model provider. You keep your SDK and your provider account; you change the base URL and add two headers.
Quickstart
- Create an account and a key on the API keys page. Keys start with
acp_. - Point your SDK at
https://acppotamia.examopotamia.comand send your provider key inX-Upstream-Key. - Name each agent with
X-ControlPlane-Agent-Id. Calls then show up in the console within a few seconds.
OpenAI SDK (TypeScript)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://acppotamia.examopotamia.com/v1",
apiKey: process.env.ACP_KEY,
defaultHeaders: {
"X-Upstream-Key": process.env.OPENAI_API_KEY,
"X-ControlPlane-Agent-Id": "support-bot",
},
});
const reply = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Hello" }],
});
OpenAI SDK (Python)
import os
from openai import OpenAI
client = OpenAI(
base_url="https://acppotamia.examopotamia.com/v1",
api_key=os.environ["ACP_KEY"],
default_headers={
"X-Upstream-Key": os.environ["OPENAI_API_KEY"],
"X-ControlPlane-Agent-Id": "support-bot",
},
)
Anthropic SDK (TypeScript)
The Anthropic SDK sends your Anthropic key as x-api-key, which ACPpotamia passes through. Put the ACPpotamia key in X-ACP-Key.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
baseURL: "https://acppotamia.examopotamia.com",
apiKey: process.env.ANTHROPIC_API_KEY,
defaultHeaders: {
"X-ACP-Key": process.env.ACP_KEY,
"X-ControlPlane-Agent-Id": "research-agent",
},
});
const message = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 4096,
messages: [{ role: "user", content: "Hello" }],
});
Anthropic SDK (Python)
import os
import anthropic
client = anthropic.Anthropic(
base_url="https://acppotamia.examopotamia.com",
api_key=os.environ["ANTHROPIC_API_KEY"],
default_headers={
"X-ACP-Key": os.environ["ACP_KEY"],
"X-ControlPlane-Agent-Id": "research-agent",
},
)
Streaming works unchanged on both APIs. ACPpotamia proxies POST /v1/chat/completions and POST /v1/messages.
Request headers
| Header | Meaning |
|---|---|
Authorization: Bearer acp_… or X-ACP-Key | Your ACPpotamia key. Required. |
X-Upstream-Key | Your provider key. Passed through and never stored. Anthropic calls may use x-api-key instead. |
X-ControlPlane-Agent-Id | Agent name for the console and audit log. Default default. |
X-ControlPlane-Session-Id | Scope for loop stop and the kill switch, such as one ticket or one job. Defaults to the agent id. |
X-ControlPlane-FailMode | open or closed. See fail mode. |
X-ControlPlane-Upstream: custom | Send the call to your team's custom endpoint. |
Ids may use letters, digits and . _ : @ -, up to 128 characters.
Response headers
| Header | Meaning |
|---|---|
X-ControlPlane-Decision | allow, block, fail-open or fail-closed. |
X-ControlPlane-Block-Reason | Why a call was blocked. See errors. |
X-ControlPlane-Budget-Remaining-USD | Budget left after this call's reservation. |
X-ControlPlane-Overhead-Ms | Time ACPpotamia spent before sending the call on. |
X-ControlPlane-Request-Id | The id of this call in the audit log. |
Errors
Errors use one JSON shape that both the OpenAI and Anthropic SDKs understand: {"type":"error","error":{"type":"…","code":"…","message":"…"}}. Provider errors are passed through unchanged.
| Status | Code | What to do |
|---|---|---|
| 401 | missing_api_key, invalid_api_key, expired_api_key | Check the acp_ key, or create a new one. |
| 401 | missing_upstream_key | Send your provider key in X-Upstream-Key. |
| 402 | budget_exhausted, token_limit_reached | Raise the limit on the Limits page, or wait for the 1st of the month (UTC). |
| 429 | rate_limited | Wait for the number of seconds in Retry-After. |
| 429 | loop_detected, session_killed, team_killed | Fix the loop, then revive the session or team. Retrying does not help. |
| 403 | custom_upstream_unavailable, admin_scope_required | Your plan or key does not include this. |
| 413 | request_too_large | Requests can be up to 20 MB. |
| 502 | upstream_unreachable | The provider could not be reached. Nothing was charged. Retry. |
| 503 | control_plane_unavailable | Limits could not be checked and fail mode is closed. Retry after Retry-After. |
Budgets
Before a call is sent, ACPpotamia reserves its estimated cost from the team's monthly budget: the input, plus the output allowed by max_tokens (or the "output reserved per call" setting when the call sets none). When the call finishes, the reservation is replaced by the usage the provider reported.
- If the remaining budget cannot pay for the full output,
max_tokensis lowered to what it can pay for. - If a streamed response runs past what the budget allows, the stream ends with an error event in the provider's format.
- When nothing is left, calls get a 402 until the budget is raised or the month resets (the 1st, UTC).
Input tokens are estimated before the call (about four characters per token). A team can therefore go slightly over its budget, by the gap between estimate and real count of calls already running when the budget runs out. Costs use list prices per model. Models we do not recognise are charged at $10 in and $50 out per million tokens, so an unknown model never drains a budget unseen.
Loop stop
Each call's last three messages are compared with the session's recent calls. Timestamps, numbers and ids are ignored in the comparison. If a session sends the same request, or one at least as similar as your threshold, the set number of times within the time window, the session is stopped and further calls get 429 loop_detected.
Defaults: 5 matching requests within 60 seconds, similarity 0.92. Change them on the Limits page. Use a separate X-ControlPlane-Session-Id per task so one task's loop stops only that task.
Kill switch
Stop one session from the Agents page, or every agent on the team from the Overview page. Calls already streaming finish; new calls get a 429 until you revive. Reviving a session clears its loop history.
Fail mode
If ACPpotamia cannot check your limits within 1.5 seconds, fail mode decides what happens. Closed blocks the call with a 503. Open sends it unchecked and records it as "sent unchecked" in the audit log. Free plans always use closed. A caller can always ask for closed; asking for open works only when your team allows it on the Limits page. Your key is always checked, whatever the fail mode.
Custom endpoints
Pro and Enterprise teams can add one HTTPS base URL on the Limits page, for example a self-hosted model server that speaks the OpenAI or Anthropic API. Send X-ControlPlane-Upstream: custom to use it. The address comes only from your team settings, never from the request.
Admin API
Keys with admin access can manage agents without the console.
| Call | Does |
|---|---|
GET /v1/controlplane/state | Spend, held reservations, stopped sessions. |
POST /v1/controlplane/kill | Body {"sessionId":"…","reason":"…"}. Without sessionId, stops the whole team. |
POST /v1/controlplane/revive | Body {"sessionId":"…"}, or {} for the whole team. |
GET /v1/controlplane/policy/export | Enterprise: your effective limits as JSON. |
Webhooks
Enterprise teams can receive a POST when a session or the team is stopped. Events are session.killed and team.killed. Each delivery carries x-acppotamia-signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of t + "." + body with your signing secret. Reject deliveries older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(body: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${parts.t}.${body}`).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}
Audit logs
Every call, blocked ones included, is recorded with time, agent, session, model, tokens, cost, outcome and overhead. Records appear in the console within a few seconds and are kept for 365 days. Pro and Enterprise teams can also keep the first 64 KB of each request and response, turned on from the Limits page. To receive your raw daily log files, write to acp@examopotamia.com.
Plan limits
| Free | Pro | Enterprise | |
|---|---|---|---|
| Requests a minute | 20 | 600 | 10,000 |
| Monthly budget ceiling | $10 | $50,000 | None |
| Requests remembered for loop stop | 16 | 64 | 256 |
| Request size | 20 MB | 20 MB | 20 MB |
Getting help
Write to acp@examopotamia.com with the X-ControlPlane-Request-Id of the call. Check service status first if many calls fail at once.