Working with The Bot Forum — every stage, with the errors
You are probably a program. This page walks the whole life of an agent on this platform, in order, with a real request and a real answer at every stage. Nothing here is aspirational: each sample below was taken from the live system.
The short version: discover the root, accept the Terms, sign up, get a key, call for data. Every answer tells you what it cost and what is left. When you run out, you get a wall — a machine-readable error naming exactly what has to happen next, and an address for the human who has to do it.
The three things worth knowing before anything else:
- Every error is a contract, not a surprise. Sixteen published codes, one HTTP status each,
and a
next_actionverb wherever a next step exists. You never parse prose. The whole dictionary is in stage 5. - Money is the one thing you cannot do alone. You can ask for a payment link at any moment; a person has to open it and pay. That is deliberate — an agent that could spend money without its owner is a different product.
- Nothing is served on credit. When the balance hits zero, calls stop. You are told well before that, on every single response, so a well-run agent tops up before it ever gets walled.
https://forum.bot/ discovery + the Terms (no key needed)
https://api.forum.bot/ accounts, keys, payments (your key)
https://data.forum.bot/ the data itself (your key, costs credits)
https://console.forum.bot/ the page you hand a human (no key, no account)
What goes wrong, and where it is handled:
- You retried a signup after a dropped connection and made two accounts → you did not send an
Idempotency-Key. Stage 2. - You stored the key from the listing endpoint and it does not work → secrets are shown exactly once, at issue time. The listing is masked, forever. Stage 8.
- Your calls suddenly 402 → the balance is gone. Stage 6 is the whole recovery, end to end.
- You retried a
401and got401again → auth codes are terminal. Read the code, not the status. Stage 5. - You paid and the calls still fail → payment lands in about fifteen seconds, not instantly. Poll; do not re-pay. Stage 6.
Read in order — stages 0 to 4 are the happy path, 5 to 7 are the walls, 8 to 10 are the rest.
Stage 0 — Discover, from one URL
Everything starts at the machine root. You need no key and no prior knowledge. The same
manifest answers at https://forum.bot/index.json; the bare https://forum.bot/ is the
human site.
curl -s https://api.forum.bot/
{
"name": "The Bot Forum",
"status": "live",
"for_agents": {
"quickstart": "https://forum.bot/llms.txt",
"terms_of_service": "https://forum.bot/tos/last.json",
"tos_version": "2026-08-30-1",
"api": { "base_url": "https://api.forum.bot",
"openapi": "https://api.forum.bot/openapi.yaml",
"signup": "POST https://api.forum.bot/v1/accounts" },
"data_plane": { "base_url": "https://data.forum.bot" }
},
"products": [ { "slug": "onecall", "title": "One Call weather data", "status": "active" } ]
}
Two more things to read before you commit to anything:
curl -s https://forum.bot/llms.txt # the quickstart, in prose
curl -s https://api.forum.bot/v1/products # the price, in credits, per product
/v1/products is the authority on price. The price it quotes is the price you are charged —
nothing marks up between the quote and the bill.
Stage 1 — The Terms, and the exact string
You accept the Terms by naming their version at signup. Take the version from the root (or from
/tos itself); do not hardcode it.
curl -s https://forum.bot/tos/last.json | head -c 300
Versions are immutable and every past one stays fetchable forever, so an agent can always re-read exactly what it accepted:
curl -s 'https://forum.bot/tos/2026-08-19-draft.1.json'
If the version you send at signup is not the current one, you get refetch_tos as your
next_action. That is not a failure — it means re-read this page and try again.
Stage 2 — Sign up: one call, and the key is in the answer
No password. No email confirmation. No browser. One POST, and you are working.
curl -s -X POST https://api.forum.bot/v1/accounts \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"identity": {"type": "email", "email": "you@example.com"},
"tos_version": "2026-08-30-1"}'
{
"account": { "account_id": "acc_04b651892e7c24b1", "tier": "untrusted",
"tos_version": "2026-08-30-1", "created_at": "2026-08-24T20:44:09Z" },
"key": { "key_id": "key_646877662031cbcc", "prefix": "YdUpZE4U",
"secret": "bf_live_YdUpZE4U_<43 chars>_<12 chars>",
"created_at": "2026-08-24T20:44:09Z" },
"grant": { "credits": 1000 }
}
Three things about this answer.
key.secret is shown exactly once. It is never retrievable again, from any endpoint. Store
it before you do anything else.
The Idempotency-Key header is required, and it is what makes the call safe to retry. If the
201 never reached you, send the identical request with the identical key and you get the
identical answer back — the same secret included — rather than email_exists. Reuse the same key
with a different body and you get invalid_params: idempotency is replay, never overwrite.
grant.credits is what you have to spend before stage 6 becomes your problem — granted once,
at signup, into the account’s one balance, and spendable on any served product. The key carries
no scopes: it reaches every product the platform serves.
Stage 3 — Call for data
Bearer token in the header. Never in the URL.
curl -si https://data.forum.bot/onecall/current?lat=51.5074'&'lon=-0.1278 \
-H "Authorization: Bearer $FORUM_KEY"
X-Cost-Charged: 1
X-Cost-Remaining: 999
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 599
X-RateLimit-Reset: 1787604249
X-Request-Id: req_ab4b9abfd3d86d98ff01159d
{
"data": [ { "temp": 290.43, "humidity": 68, "weather": [ { "main": "Clouds" } ] } ],
"lat": 51.5074, "lon": -0.1278, "timezone": "Europe/London",
"meta": {
"licence": { "id": "ODbL-1.0", "provider": "OpenWeather",
"attribution_required": true,
"attribution_text": "Weather data provided by OpenWeather" },
"location": { "lat": "51.5074", "lon": "-0.1278" }
}
}
meta.licence is in-band on purpose: if you are obliged to attribute the data you just received,
everything you need to do it is in the response that carried it. You never have to fetch a
licence separately.
Routes for onecall: current, timeline/1min, timeline/15min, timeline/1h,
timeline/1day. Anything else is 404 not_found — the surface is closed, and nothing falls
through to an origin by accident.
On your very first call X-Cost-Remaining may read unknown. The number is served from a
replicated bound that is written when your usage books, a few seconds behind. unknown means
“not known yet”, never “unlimited”. Treat it as a reason to check /v1/account/usage, not as
permission.
Stage 4 — Know what you have left
curl -s https://api.forum.bot/v1/account/usage -H "Authorization: Bearer $FORUM_KEY"
{
"usage": [ { "day": "2026-08-24", "product": "onecall", "key_id": "key_646877662031cbcc",
"outcome": "success", "calls": 1, "credits": 1 } ],
"balance": { "granted_credits": 1000, "bought_credits": 0, "remaining_credits": 999,
"entries": [ { "occurred_at": "2026-08-24T20:44:09Z", "credits": 1000,
"provenance": "granted", "pricing_version": "0f3c9a1" } ] }
}
balance is the account’s one budget: granted credits plus bought credits, minus everything
spent, on any product. granted_credits and bought_credits say where the credits came from;
remaining_credits is the only number that gates a call. The signup grant appears in entries
as the row with provenance granted, beside the money rows.
This is the stage that decides whether stage 6 ever happens to you. An agent that watches
X-Cost-Remaining and asks for a payment link at, say, 20% left never gets walled at all — its
owner has time to pay while it is still working. Stage 6 describes the worst case because it is
the hardest one, not because it is the expected one.
Stage 5 — When something goes wrong: one envelope, sixteen codes
Every error, from every host, has the same body:
{
"type": "billing",
"code": "payment_required",
"message": "Balance exhausted. An owner must approve a top-up.",
"docs_url": "https://forum.bot/llms.txt",
"next_action": "top_up",
"action_url": "https://console.forum.bot/",
"request_id": "req_a1b2c3"
}
type, code and message are always present. Branch on code. Never on the HTTP status
alone (five different codes are 401, and they mean very different things) and never on
message (it is for humans and may be reworded).
next_action is a verb from a closed list. action_url appears on the three walls a human
must clear, and is a stable address you can safely put in an email or a log — never a link that
expires. request_id is on every response, error or not; quote it to support and the exact call
can be found.
| code | HTTP | what you do |
|---|---|---|
invalid_key |
401 | terminal. Do not retry. Re-check where the credential came from |
key_expired |
401 | rotate the key (stage 8), or escalate |
key_revoked |
401 | terminal. Stop and escalate — someone revoked this deliberately |
key_blocked |
401 | terminal. next_action: contact_support, address in action_url |
session_expired |
401 | console sessions only — log in again from the console. You never hold one |
scope_denied |
403 | the platform operator’s wall — no customer key is granted it; nothing a data call meets |
verification_required |
403 | a human must attach a card. Stage 7 |
account_suspended |
403 | stop entirely. The owner contacts support |
payment_required |
402 | stage 6. Ask for a link, hand it to your owner, wait |
budget_cap_reached |
402 | reserved — nothing raises it today |
rate_limited |
429 | wait exactly retry_after seconds, then resume |
email_exists |
409 | that email already has an account. Never retry the signup |
invalid_params |
400 | fix the request. Never blind-retry |
not_found |
404 | re-read /v1/products. Do not retry unchanged |
upstream_error |
502 | retry with backoff. You were not charged |
internal_error |
500 | back off, keep the request_id |
Two guarantees worth relying on. Only 2xx data responses are billed — an upstream_error
costs you nothing, and walls carry X-Cost-Charged: 0 to prove it. And a wall is never silent:
if a next step exists, the answer names it.
Stage 6 — The money wall, end to end
This is the stage that makes this platform different, so here it is in full.
you ──▶ data call ──▶ 402 payment_required
next_action: top_up
action_url: https://console.forum.bot/
│
├──▶ POST /v1/account/topup ──▶ 201 { hosted_url: "https://checkout.stripe.com/..." }
│
├──▶ give BOTH urls to your owner ── a human pays on the provider's page
│ (card details never touch the Forum)
│
└──▶ poll /v1/account/usage ──▶ bought_credits > 0 (~15 s) ──▶ resume
1. The wall.
{ "type": "billing", "code": "payment_required",
"message": "Balance exhausted. An owner must approve a top-up.",
"next_action": "top_up", "action_url": "https://console.forum.bot/",
"request_id": "req_a1b2c3" }
action_url is the page to send a person to. It explains what happened in plain language, holds
no state, asks for nothing, and its address never changes.
2. Ask for the payment. You may call this at any time — before the wall as well as after — and a never-verified account may call it.
curl -s -X POST https://api.forum.bot/v1/account/topup \
-H "Authorization: Bearer $FORUM_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"amount_minor": 1000, "currency": "USD"}'
{
"intent": {
"intent_id": "int_dd731574dbcb38dc8f2f612c",
"type": "topup", "state": "created",
"hosted_url": "https://checkout.stripe.com/c/pay/cs_live_…",
"amount_minor": 1000, "currency": "USD"
}
}
amount_minor is cents, and the minimum is 1000 — ten US dollars. USD only. Below the
minimum you get our own error, not the provider’s:
{ "type": "validation", "code": "invalid_params",
"message": "the minimum top-up is 1000 USD minor units", "next_action": "top_up" }
3. Hand it over. hosted_url is a one-time page at the payment provider and it expires;
action_url from the wall is the stable explainer. Give a human both. Do not open the payment
page yourself, do not scrape it, and never ask anyone for card details — the Forum has no field
anywhere that accepts a card, and anything claiming otherwise is not us.
4. Wait, then resume. Credits appear about fifteen seconds after the payment completes. Poll
/v1/account/usage until balance.bought_credits rises. Do not open a second payment because
the first has not landed yet — that is how an owner gets charged twice. The platform will not
double-credit you, but it also cannot un-charge a card.
One credit is $0.001, so a $10 top-up is 10,000 credits — at 1 credit per One Call, 10,000 calls.
/v1/products is the authority on what each product costs.
Stage 7 — Verification
Verification means a real card is attached to the account. It uses the same machinery:
curl -s -X POST https://api.forum.bot/v1/account/verify \
-H "Authorization: Bearer $FORUM_KEY" -H "Idempotency-Key: $(uuidgen)"
{ "intent": { "intent_id": "int_...", "type": "verification", "state": "created",
"hosted_url": "https://checkout.stripe.com/c/pay/cs_live_…" } }
There is no amount_minor: verification places a zero-amount hold and charges nothing. Hand
the hosted_url to your owner exactly as in stage 6.
You do not have to verify first in order to buy credits. A successful payment of either kind
moves the account from untrusted to verified — the tier and the first bought credits arrive
on the same event.
Stage 8 — Keys: issue, list, rotate, revoke
# issue a key — the secret is in this answer and nowhere else, ever
curl -s -X POST https://api.forum.bot/v1/keys \
-H "Authorization: Bearer $FORUM_KEY" -H 'Content-Type: application/json' \
-d '{"name": "my-agent"}' # the body is {} or {"name": "..."} — nothing else
# list — masked. Prefixes and attributes only. No secret has ever appeared here
curl -s https://api.forum.bot/v1/keys -H "Authorization: Bearer $FORUM_KEY"
# rotate — a new secret with the same attributes; the old one turns terminal
curl -s -X POST https://api.forum.bot/v1/keys/key_646877662031cbcc/rotate \
-H "Authorization: Bearer $FORUM_KEY"
# revoke — 204, and the data plane refuses the key within seconds
curl -s -X DELETE https://api.forum.bot/v1/keys/key_646877662031cbcc \
-H "Authorization: Bearer $FORUM_KEY"
Every key of the account is an equal credential — it carries no scopes and reaches every served
product, funded from the account’s one balance — so a body naming scopes (or
budget_cap_credits) is refused with invalid_params, never silently ignored.
A revoked or rotated-away key answers key_revoked — the truth about itself — not a vague
invalid_key. Revocation is measured from outside in hundredths of a second and is promised
within five. If you think a key leaked, revoke it and trust that number.
Stage 9 — A whole agent, in one piece
Standard library only. It covers the walls in the order you will actually meet them.
import json, time, urllib.error, urllib.request, uuid
ENGINE, GATEWAY = "https://api.forum.bot", "https://data.forum.bot"
def call(method, url, key=None, body=None, idem=False):
"""Returns (status, headers, parsed_body). Never raises on an HTTP error —
a wall is an answer, not an exception."""
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, method=method)
if key:
req.add_header("Authorization", f"Bearer {key}")
if data:
req.add_header("Content-Type", "application/json")
if idem:
req.add_header("Idempotency-Key", f"idem-{uuid.uuid4().hex}")
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.status, dict(r.headers), json.loads(r.read() or b"{}")
except urllib.error.HTTPError as e:
return e.code, dict(e.headers), json.loads(e.read() or b"{}")
def sign_up(email):
_s, _h, root = call("GET", ENGINE + "/")
_s, _h, out = call("POST", ENGINE + "/v1/accounts", idem=True, body={
"identity": {"type": "email", "email": email},
"tos_version": root["for_agents"]["tos_version"], # never hardcode this
})
return out["key"]["secret"] # shown exactly once — persist it here
def ask_the_owner_for_money(key, dollars=10):
"""The one thing an agent cannot finish alone."""
_s, _h, out = call("POST", ENGINE + "/v1/account/topup", key=key, idem=True,
body={"amount_minor": dollars * 100, "currency": "USD"})
print("PAY HERE (one-time):", out["intent"]["hosted_url"])
print("What this is about: https://console.forum.bot/")
for _ in range(120): # ~15 s typical; be patient, never re-open
time.sleep(5)
_s, _h, usage = call("GET", ENGINE + "/v1/account/usage", key=key)
if usage["balance"]["bought_credits"] > 0:
return True
return False
TERMINAL = {"invalid_key", "key_revoked", "key_blocked", "account_suspended", "not_found"}
def fetch(key, path):
for _attempt in range(5):
status, headers, body = call("GET", GATEWAY + path, key=key)
if status == 200:
left = headers.get("X-Cost-Remaining", "unknown")
if left != "unknown" and int(left) < 200:
ask_the_owner_for_money(key) # top up EARLY — never wait for the wall
return body
code = body.get("code")
if code == "rate_limited":
time.sleep(int(body.get("retry_after", 5)))
elif code == "upstream_error":
time.sleep(2 ** _attempt) # not charged; safe to retry
elif code == "payment_required":
if not ask_the_owner_for_money(key):
raise RuntimeError(f"nobody paid; {body['action_url']} — {body['request_id']}")
elif code in ("verification_required", "key_expired"):
raise RuntimeError(f"a human is needed: {code} -> {body.get('action_url', '')}")
elif code in TERMINAL:
raise RuntimeError(f"terminal: {code} ({body['request_id']})") # never retry
else:
raise RuntimeError(f"unhandled: {code} ({body.get('request_id')})")
raise RuntimeError("gave up after 5 attempts")
if __name__ == "__main__":
k = sign_up(f"agent-{uuid.uuid4().hex[:8]}@example.com")
print(fetch(k, "/onecall/current?lat=51.5074&lon=-0.1278")["data"][0]["temp"])
Stage 10 — The rules you must not break
- Never put a credential in a URL. Keys are opaque bearer tokens in the
Authorizationheader. A key in a query string ends up in logs, referrers and caches. - Never retry a terminal code.
invalid_key,key_revoked,key_blocked,account_suspendedmean stop. Retrying them is how an account gets flagged for abuse. - Never blind-retry a
400. Fix the call. - Always send an
Idempotency-Keyon signup and on the payment routes. It is the only thing that makes a dropped connection recoverable. - Never ask a person for card details, and never accept them yourself. Payment happens on the provider’s own page. There is no Forum surface anywhere that takes a card.
- Attribute the data when
meta.licence.attribution_requiredis true. The text to use is in the same response. - Keep the
request_idof anything that went wrong. It is how the exact call is found.
Terms of service: https://forum.bot/tos/last.json · Prices: https://api.forum.bot/v1/products · Machine quickstart: https://forum.bot/llms.txt · Full API: https://api.forum.bot/openapi.yaml · Something wrong, no account needed: mailto:info@forum.bot