Overview
A1 Token is an OpenAI-compatible AI gateway. Point any OpenAI client at it, keep your own model names, and pay from one balance. Every key carries its own spend caps, its own rate limit and its own usage history — so a company issues one key per person or per service rather than sharing a single credential.
Base URL
Inference and every key-authenticated read are served at
https://token.gme-ai.com/v1. The same API also answers at
https://gme-ai.com/v1, so clients already pointed there keep working.
What is in this API
- POST /v1/chat/completions
- OpenAI-compatible chat, buffered or streamed, with tool calls.
- GET /v1/models
- The models this key may call — already narrowed by the company and key allowlists.
- GET /v1/key
- What the key is: caps, rate limit, allowlist.
- GET /v1/credits
- Remaining balance and lifetime usage, in USD.
- GET /v1/usage
- Spend and tokens by model and by day, week or month.
- GET /v1/documents
- Read and append to the account's documents — opt-in per key.
- POST /api/mcp/a1
- A read-only MCP server so an IDE can answer “what have I spent this week”.
Balances are in USD and belong to the company, not to the key. Several keys draw on one balance; caps decide how much of it each key may reach.
https://token.gme-ai.com/v1
Authorization: Bearer gme-<your key>
Content-Type: application/json
Quickstart
Three steps from nothing to a completion.
-
Create a key
Sign in to the portal and open Developer. Create a key; the full value is shown once. It starts
gme-. After that the portal only ever shows a masked hint. -
Point your client at the gateway
Set the base URL to
https://token.gme-ai.com/v1and send the key asAuthorization: Bearer. Nothing else in an OpenAI client changes. -
Send a completion
Use a model name from GET /v1/models, or one of the familiar aliases such as
gpt-4o. The response is the OpenAIchat.completionobject.
A key spends real money from the company balance. Keep it server-side: never ship it in a browser bundle, a mobile app or a public repository. Revoke and re-mint rather than rotating by hand.
curl https://token.gme-ai.com/v1/chat/completions \
-H "Authorization: Bearer $A1_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.4",
"messages": [
{"role": "user", "content": "Say hello in five words."}
]
}'
import os
from openai import OpenAI
client = OpenAI(
base_url="https://token.gme-ai.com/v1",
api_key=os.environ["A1_KEY"],
)
r = client.chat.completions.create(
model="gpt-5.4",
messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(r.choices[0].message.content)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://token.gme-ai.com/v1",
apiKey: process.env.A1_KEY,
});
const r = await client.chat.completions.create({
model: "gpt-5.4",
messages: [{ role: "user", content: "Say hello in five words." }],
});
console.log(r.choices[0].message.content);
{
"id": "chatcmpl-7c1f9a0b52d84e6fa31b7c02",
"object": "chat.completion",
"created": 1774224000,
"model": "atg-gpt-5.4",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello there, good to meet you."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 14,
"completion_tokens": 8,
"total_tokens": 22
}
}
Authentication
Every endpoint on this page takes the same credential: an API key in the
Authorization header, as
Bearer gme-…. There is no session, no cookie and no
second token type to manage — the key you paste into Cursor is the same key the
MCP server authenticates.
One key per person or per service
A key is a credential you hand to a client, a contractor or a CI job. It is not an account. A company with ten thousand staff issues ten thousand keys, or one per service — not one shared key — because caps, rate limits, allowlists and usage history are all attributed per key. The usage endpoint defaults to key scope for exactly this reason: a key handed to a contractor must not become a window onto everything else the company spends.
A member may hold up to 25 live keys at once (server-configurable);
minting the twenty-sixth returns 409 key_limit_reached
until an unused one is revoked.
Key prefixes
Inference keys begin gme-. Merchant
management keys begin gmadm- and are rejected
at the door with 401 management_key_cannot_infer —
they are a different credential class and can never reach a model.
That refusal is the only thing a management key cannot do. It is the credential a company uses to create its members, issue and rotate their keys, fund them and take funding back, and read what they spent — see Managing your members. Keeping it away from inference is deliberate: a credential that can move money should never be pasted into application code.
Why a key can stop working
Authentication returns 401 with a specific reason, or 403 in one case:
| Reason | Status | Meaning |
|---|---|---|
missing_api_key | 401 | No Authorization: Bearer header, or an empty one. |
invalid_api_key | 401 | No key with that value exists. |
api_key_revoked | 401 | The key was revoked. Revocation is immediate and permanent. |
api_key_expired | 401 | The key had an expiry and it has passed. |
account_inactive | 401 | The owning account is not active. |
management_key_cannot_infer | 401 | A gmadm- management key was sent to an inference route. |
merchant_suspended | 403 | The key belongs to a suspended merchant. |
/v1/chat/completions, /v1/models
and /v1/documents report every one of these reasons under
the single error code invalid_api_key, with the specific
reason in error.message.
/v1/key, /v1/credits and
/v1/usage put the specific reason in
both error.message and
error.code. Branch on the HTTP status; read the reason
for a log line, not for control flow.
Authorization: Bearer gme-xK3q8Lm2Vn9pRtYw4ZbD7eF1
{
"error": {
"message": "api_key_revoked",
"type": "invalid_request_error",
"code": "invalid_api_key"
}
}
{
"error": {
"message": "api_key_revoked",
"type": "invalid_request_error",
"code": "api_key_revoked"
}
}
OpenAI SDK drop-in
The gateway speaks the OpenAI Chat Completions wire format, so the official SDKs work unmodified. One line changes: the base URL.
Python uses base_url. Node uses
baseURL. Include the /v1
suffix — both SDKs append paths to whatever you give them, so
https://gme-ai.com without it would request
/chat/completions and 404.
What carries over, and what does not
Chat completions, streaming, tool calling and JSON mode carry over. Endpoints this gateway does not implement — embeddings, images, audio, assistants, batch, responses, fine-tuning — are not proxied, so an SDK call to any of them will not find a route. Chat completions lists exactly which request fields are read.
from openai import OpenAI
client = OpenAI(
base_url="https://token.gme-ai.com/v1", # <- this line
api_key=os.environ["A1_KEY"],
)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://token.gme-ai.com/v1", // <- this line
apiKey: process.env.A1_KEY,
});
Chat completions
POST/v1/chat/completions
Auth: Bearer gme-…. Body limit 4 MB.
OPTIONS is answered with permissive CORS headers, so the
route is reachable from a browser — which does not make it safe to put a key in one.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
messages | array | required | OpenAI message objects. Must be a non-empty array or the call returns 400 invalid_request. |
model | string | optional | A model id or alias. Omitted, empty or "auto" selects the gateway default. See Model resolution below. |
stream | boolean | optional | Exactly true turns on SSE framing. Any other value is treated as off. See Streaming. |
tools | array | optional | OpenAI tool definitions, passed to the model. |
tool_choice | string / object | optional | Passed through unchanged. |
temperature | number | optional | Passed through unchanged. |
top_p | number | optional | Passed through unchanged. |
max_tokens | integer | optional | Defaults to 2048 when absent or zero. Raise it for long answers. |
response_format | object | optional | Only {"type":"json_object"} is read; it turns on JSON mode. Other shapes, including JSON schema, are ignored. |
Fields not in this table are ignored, not rejected.
n, stop,
seed, logprobs,
presence_penalty,
frequency_penalty and
user are accepted by the parser and then dropped. Do not
rely on them having taken effect.
Model resolution
The name you send is resolved before dispatch, in this order: a remapped legacy id,
then an exact catalogue id, then an alias, then an alias after stripping an
openai/ or anthropic/
prefix.
A name that matches none of those is refused with 404
(model_not_found), so a typo is never billed as another model.
Only an absent model, or auto, picks the
gateway default. model in the response always names the model
that actually ran; list /v1/models rather than hard-coding a
name you have not verified.
Response
A standard chat.completion object.
id is a freshly generated
chatcmpl- identifier.
choices[0].message is the assistant message as the model
returned it, so it carries tool_calls when tools were
used — in which case finish_reason is
"tool_calls".
The completion id is generated for the
response and is not written to the billing ledger, so it cannot be used to look a
single generation's cost up afterwards. Use /v1/usage for spend.
curl https://token.gme-ai.com/v1/chat/completions \
-H "Authorization: Bearer $A1_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.4",
"max_tokens": 1024,
"temperature": 0.2,
"messages": [
{"role": "system", "content": "You are terse."},
{"role": "user", "content": "Weather in Kuala Lumpur?"}
],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}]
}'
{
"id": "chatcmpl-40b6e2d91af35c780c44be61",
"object": "chat.completion",
"created": 1774224117,
"model": "atg-gpt-5.4",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_9d2f",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Kuala Lumpur\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 96,
"completion_tokens": 21,
"total_tokens": 117
}
}
{
"error": {
"message": "messages is required",
"type": "invalid_request_error",
"code": "invalid_request"
}
}
{
"error": {
"message": "This key is not permitted to use the requested model.",
"type": "invalid_request_error",
"code": "model_not_allowed"
}
}
Streaming
Send "stream": true and the response is
text/event-stream: a sequence of
data: frames carrying
chat.completion.chunk objects, terminated by
data: [DONE]. Any OpenAI streaming client parses it
without changes.
Be honest about what streams
There are two paths behind that flag, and by default you get the second:
- Real token streaming — tokens are forwarded as the upstream
produces them. It is off unless the server is configured for it, and it is used
only when the request carries no
tools. Its final chunk carries ausageobject. - Synthesized streaming — the default. The completion is fetched in
full, then re-framed into chunks of about 90 characters. The framing is identical
and clients cannot tell the difference, but time-to-first-token is not
improved: the first frame arrives only once the whole answer exists. Its
chunks carry no
usage.
If your product promises a typewriter effect to reduce perceived latency, know that on the default path you are animating an answer that already finished.
Tool calls while streaming
A streamed request with tools always uses the synthesized path, and the tool call
arrives as a single delta containing the complete
tool_calls array — not as incrementally assembled
argument fragments. Clients that accumulate partial
function.arguments strings still work; they simply
receive it all at once.
Errors mid-stream
If dispatch fails before any bytes are written, you get a normal JSON error with the
right status. If it fails after the stream has opened, the headers are already sent —
so the stream is closed with data: [DONE] and
no error frame. A stream that ends early with a short or empty answer
is the failure signal. There is no re-dispatch on a failed stream, deliberately:
re-calling could bill a completion the upstream had already produced twice.
curl -N https://token.gme-ai.com/v1/chat/completions \
-H "Authorization: Bearer $A1_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.4",
"stream": true,
"messages": [{"role": "user", "content": "Count to three."}]
}'
data: {"id":"chatcmpl-8a1c","object":"chat.completion.chunk","created":1774224200,"model":"atg-gpt-5.4","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-8a1c","object":"chat.completion.chunk","created":1774224200,"model":"atg-gpt-5.4","choices":[{"index":0,"delta":{"content":"One, two, three."},"finish_reason":null}]}
data: {"id":"chatcmpl-8a1c","object":"chat.completion.chunk","created":1774224200,"model":"atg-gpt-5.4","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
{
"id": "chatcmpl-8a1c",
"object": "chat.completion.chunk",
"created": 1774224200,
"model": "atg-gpt-5.4",
"choices": [
{ "index": 0, "delta": {}, "finish_reason": "stop" }
],
"usage": {
"prompt_tokens": 11,
"completion_tokens": 6,
"total_tokens": 17
}
}
Models
GET/v1/models
Auth: Bearer gme-…. No parameters. Returns the OpenAI
list envelope.
This is a per-key list, not a catalogue
The list is already narrowed to what this key can actually call. Two filters apply, in this order:
- The company allowlist. If an operator has chosen a set of models for the company that owns the key, anything outside it is removed. A company with no selection means “everything in the offer”, not “nothing”.
- The key allowlist. If the key carries its own list, everything else is removed. An alias survives when its resolved target is on the list.
Not every model in the commercial offer is callable here. The
gateway resolves every request to a model that can be billed to the company
balance; a model that sits in the offer without such a route is never listed and
cannot be called. A key whose allowlist contains only such models will
receive an empty list and be unable to call anything. Treat an empty
data array as a configuration fault to report, not as
an outage.
Two spellings, both accepted
The list contains catalogue ids (for example
atg-gpt-5.4 — the prefix is a legacy internal registry
marker that shows through in the id) and familiar aliases such as
gpt-4o, gpt-5.4 and
claude-sonnet. Both spellings are accepted by
/v1/chat/completions.
Aliases are convenience names for a catalogue entry.
claude-opus and claude-sonnet reach
Anthropic's Claude Opus 5.5 and Sonnet 5.5 through WDCloud, which needs a WDCloud allowance
on the account; claude-haiku still maps to GPT-5.4 mini. If the
exact model matters, send a catalogue id and check model in the
response.
If a key has an allowlist
Allowlist entries are matched against the resolved catalogue id,
after aliasing. An allowlist containing only alias spellings (for example
gpt-4o) will therefore block every request, because the
resolved id (atg-gpt-5.4) is not in it. Put catalogue
ids in allowlists.
curl https://token.gme-ai.com/v1/models \
-H "Authorization: Bearer $A1_KEY"
{
"object": "list",
"data": [
{ "id": "atg-gpt-5.5", "object": "model", "created": 0, "owned_by": "atg" },
{ "id": "atg-gpt-5.4", "object": "model", "created": 0, "owned_by": "atg" },
{ "id": "atg-gpt-5.4-mini", "object": "model", "created": 0, "owned_by": "atg" },
{ "id": "atg-gpt-5.3-codex", "object": "model", "created": 0, "owned_by": "atg" },
{ "id": "gpt-4o", "object": "model", "created": 0, "owned_by": "gme-ai" },
{ "id": "gpt-5.4", "object": "model", "created": 0, "owned_by": "gme-ai" },
{ "id": "claude-sonnet", "object": "model", "created": 0, "owned_by": "gme-ai" }
]
}
id catalogue id or alias — send either as "model"
object always "model"
created always 0 — this gateway does not date models
owned_by the catalogue vendor, or "gme-ai" for an alias row
Key introspection
GET/v1/key
Auth: Bearer gme-…. No parameters. Tells a client what
the credential it is holding may do, without it needing portal access.
| Field | Type | Meaning |
|---|---|---|
label | string | key_ plus the key's numeric id. Not the name you gave it at creation. |
usage | number | Lifetime charged spend attributed to this key, in USD. Not a period figure. |
limit | number | null | The daily spend cap, or null for no cap. |
limit_remaining | number | null | limit minus spend in the rolling last 24 hours, floored at 0. null when there is no cap. |
limit_enforcement | string | Always "soft". See Rate limits & spend caps. |
monthly_limit | number | null | The calendar-month-to-date cap, or null. |
is_free_tier | boolean | Always false. There is no free tier. |
is_provisioning_key | boolean | Always false. Keys cannot mint other keys. |
rate_limit.requests | number | Requests permitted per interval, already clamped to the enforced ceiling of 60. |
rate_limit.interval | string | Always "60s". |
model_allowlist | array | null | Catalogue ids this key is restricted to, or null for no restriction. |
limit is a courtesy budget, not the wall. The
hard ceiling is the company balance reported by
/v1/credits. Building a quota UI on
limit alone shows a customer headroom they may not
have.
curl https://token.gme-ai.com/v1/key \
-H "Authorization: Bearer $A1_KEY"
{
"data": {
"label": "key_4812",
"usage": 18.420712,
"limit": 25,
"limit_remaining": 21.95,
"limit_enforcement": "soft",
"monthly_limit": 400,
"is_free_tier": false,
"is_provisioning_key": false,
"rate_limit": {
"requests": 60,
"interval": "60s"
},
"model_allowlist": null
}
}
{
"data": {
"label": "key_5190",
"usage": 0.9134,
"limit": null,
"limit_remaining": null,
"limit_enforcement": "soft",
"monthly_limit": null,
"is_free_tier": false,
"is_provisioning_key": false,
"rate_limit": { "requests": 60, "interval": "60s" },
"model_allowlist": ["atg-gpt-5.4-mini"]
}
}
Credits
GET/v1/credits
Auth: Bearer gme-…. No parameters. Figures are USD.
The balance reported is the one belonging to the member this key was issued to — not a company-wide pool. Two keys of the same company read two different numbers, deliberately: that is what makes per-member budgeting possible. A company funds its members from its pool through the member-management API.
| Field | Type | Meaning |
|---|---|---|
balance | number | null | The authoritative figure: money remaining. null when it could not be read. |
total_usage | number | Lifetime charged spend by this key’s member, across every key they hold and the portal. Not a company total — summing it across members is the only way to get one. |
total_credits | number | null | Derived as balance + total_usage, so that remaining = total_credits − total_usage holds. null when the balance is unavailable. |
balance_status | string | "reported" or "unavailable". |
balance: null means unknown, not
zero. The upstream did not answer. Check
balance_status before you render a number — showing
“$0.00” for an unreadable balance tells a customer their account is
empty when it may be full, and both derived fields go null
with it precisely so that you can tell the two apart.
The balance is read through an upstream service and cached briefly, so a top-up can take a few seconds to appear here. Spend within a billing window is not debited from this figure instantaneously either — for spend, use /v1/usage, which reads the ledger.
curl https://token.gme-ai.com/v1/credits \
-H "Authorization: Bearer $A1_KEY"
{
"data": {
"total_credits": 512.834112,
"total_usage": 87.165912,
"balance": 425.6682,
"balance_status": "reported"
}
}
{
"data": {
"total_credits": null,
"total_usage": 87.165912,
"balance": null,
"balance_status": "unavailable"
}
}
Usage reporting
GET/v1/usage
Auth: Bearer gme-…. Self-service spend and token
reporting, bucketed daily, weekly or monthly and broken down
per model. This is the endpoint to build a customer-facing usage dashboard on.
Query parameters
| Parameter | Type | Required | Notes |
|---|---|---|---|
granularity | string | optional | day (default), week or month. Anything else returns 400 bad_granularity. |
days | integer | optional | How far back to look. Default 30, clamped to 1–365. A non-numeric value falls back to 30 rather than erroring. |
scope | string | optional | key (default) or account. Anything else returns 400 bad_scope. |
Scope is the key by default
scope=key counts only traffic carrying this key's id.
scope=account widens it to everything the owning account
spent, including the web portal and other keys. There is no parameter that can
reach another account — the account is taken from the authenticated key, never
from the query string. The note field in every response
says which of the two you are looking at; render it.
Buckets
Weeks are Monday-anchored and are the same weeks on every connection.
Months start on the first. Buckets with no traffic are returned as zeroes,
not omitted — so a chart shows a quiet Sunday as a quiet Sunday rather than
as missing history. from is the first bucket returned and
to is today.
With granularity=week or
month, the window still starts days
days back and is then widened backwards to the start of that week or month — so the
first bucket may cover days before the window and read low. Ask for whole weeks
(days=28) or whole months if that matters.
Response
| Field | Type | Meaning |
|---|---|---|
scope, granularity, days | string, string, number | The resolved parameters, echoed after clamping. |
from, to | string | null | YYYY-MM-DD. from is null only if no bucket was produced. |
totals | object | calls, tokens_in, tokens_out, tokens_cached, cost. |
by_model[] | array | model plus the same five counters, ordered by cost descending. |
by_period[] | array | period, calls, tokens_in, tokens_out, cost. No tokens_cached here — it exists in totals and by_model only. |
note | string | Plain-English statement of what the figures cover. |
Counted calls, charged cost
calls counts every logged call in the window.
cost sums only the calls that were charged. An
unbilled call therefore raises the call count without raising the cost — which is
correct, and means you cannot derive a per-call price by dividing one by the other.
Model names here are upstream ids, not the ids
/v1/models returns. The ledger records the model
the provider ran, so usage reports gpt-5.4 where
/v1/models lists atg-gpt-5.4
and a completion response returns atg-gpt-5.4. Strip the
prefix before joining the two, and expect (unknown) for
rows whose model was not recorded.
curl "https://token.gme-ai.com/v1/usage?granularity=day&days=7" \
-H "Authorization: Bearer $A1_KEY"
curl "https://token.gme-ai.com/v1/usage?granularity=week&days=84" \
-H "Authorization: Bearer $A1_KEY"
curl "https://token.gme-ai.com/v1/usage?granularity=month&days=365&scope=account" \
-H "Authorization: Bearer $A1_KEY"
{
"data": {
"scope": "key",
"granularity": "day",
"days": 7,
"from": "2026-09-17",
"to": "2026-09-23",
"totals": {
"calls": 1842,
"tokens_in": 2914330,
"tokens_out": 418077,
"tokens_cached": 1204800,
"cost": 9.842131
},
"by_model": [
{
"model": "gpt-5.4",
"calls": 1502,
"tokens_in": 2610411,
"tokens_out": 361900,
"tokens_cached": 1204800,
"cost": 8.901141
},
{
"model": "gpt-5.4-mini",
"calls": 340,
"tokens_in": 303919,
"tokens_out": 56177,
"tokens_cached": 0,
"cost": 0.94099
}
],
"by_period": [
{ "period": "2026-09-17", "calls": 241, "tokens_in": 402118, "tokens_out": 55310, "cost": 1.304412 },
{ "period": "2026-09-18", "calls": 318, "tokens_in": 501774, "tokens_out": 71902, "cost": 1.702887 },
{ "period": "2026-09-19", "calls": 0, "tokens_in": 0, "tokens_out": 0, "cost": 0 },
{ "period": "2026-09-20", "calls": 290, "tokens_in": 455001, "tokens_out": 66040, "cost": 1.551203 },
{ "period": "2026-09-21", "calls": 301, "tokens_in": 470336, "tokens_out": 68115, "cost": 1.602884 },
{ "period": "2026-09-22", "calls": 344, "tokens_in": 540210, "tokens_out": 77414, "cost": 1.841002 },
{ "period": "2026-09-23", "calls": 348, "tokens_in": 544891, "tokens_out": 79296, "cost": 1.839743 }
],
"note": "Scoped to this API key. Usage made through the portal or another key is not included."
}
}
{
"error": {
"message": "granularity must be day, week or month",
"type": "invalid_request_error",
"code": "bad_granularity"
}
}
Documents
The same key can read and append to the account's documents — but only if it was created with that reach. This is opt-in per key, at creation time: a key minted for inference cannot touch documents, and no deploy will silently grant it that. A merchant-issued key is refused outright.
| Endpoint | Does |
|---|---|
GET /v1/documents | List visible documents. Query: q (search, 80 chars), kind (doc or note), limit (1–100, default 25). |
GET /v1/documents/:id | Read one, as plain text, with truncated and chars so you know if you got all of it. |
POST /v1/documents | Create. Body: title (required), text (Markdown) or html, kind. |
POST /v1/documents/:id/append | Append text or html to the end. The only write. |
There is deliberately no PUT and no
DELETE: the API can add to a document but cannot
overwrite or destroy one. Appends take a row lock, so two concurrent appends both
land rather than one erasing the other.
Errors specific to documents
| Status | Code | Meaning |
|---|---|---|
| 503 | feature_disabled | The documents API is not enabled on this server. It is off by default. |
| 403 | merchant_key_no_documents | A merchant-issued key. It can never reach documents. |
| 403 | key_has_no_document_access | The key was created without document reach. Mint a new one with it. |
| 403 | read_only | The document was shared to read, not to change. Nothing was written. |
| 404 | not_found | No document with that id is visible to this account. |
| 409 | doc_full | The document is at its maximum size. Nothing was written. |
| 413 | too_long | A single append may add at most 100,000 characters. Send it in parts. |
| 400 | empty | Neither text nor html carried content. |
curl "https://token.gme-ai.com/v1/documents?q=runbook&limit=5" \
-H "Authorization: Bearer $A1_KEY"
{
"object": "list",
"data": [
{
"id": 3312,
"title": "Deploy runbook",
"kind": "doc",
"created_at": "2026-08-02T09:14:22.000Z",
"updated_at": "2026-09-21T16:40:05.000Z",
"chars": 8412,
"is_mine": 1
}
]
}
curl https://token.gme-ai.com/v1/documents/3312/append \
-H "Authorization: Bearer $A1_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "## 2026-09-23\n\nRolled back to build 812."}'
{
"object": "document.append",
"id": 3312,
"title": "Deploy runbook",
"tab": "Notes",
"added_chars": 42,
"updated_at": "2026-09-23T11:02:48.000Z"
}
MCP for Cursor & Cline
POST/api/mcp/a1
Protocol version 2025-06-18. Server name
a1-token-mcp, version
1.0.0. Transport is JSON-RPC 2.0 over HTTP POST.
Auth is the same gme- key you already use for
inference — there is no second credential to manage.
MCP is not how your IDE reaches the models. Cursor and Cline consume A1 as an OpenAI-compatible provider: base URL plus key, configured under the editor's model settings. MCP is for tools. This server lets you ask about your account without leaving the editor — “what have I spent this week”, “which model is costing me most”, “what can this key reach”. Both are worth configuring; they do different jobs.
The tools
Four, all read-only, all scoped to the authenticated key. Nothing here can spend, move or refund anything, and none of them can see another account.
| Tool | Arguments | Returns |
|---|---|---|
a1_balance | none | Text plus { balance, status }. A null balance means unreadable, not zero. |
a1_usage | granularity (day | week | month), days (1–365), scope (key | account) | A readable summary plus the exact same object GET /v1/usage returns. |
a1_key | none | Caps, rate limit, allowlist, plus rate_enforcement and cap_enforcement. |
a1_models | none | { models: [{ model, context, input_per_1m, output_per_1m, capabilities }] }. |
a1_usage and
GET /v1/usage run the same implementation, so the answer
an IDE gets and the answer your dashboard gets can never disagree. Note that
a1_models reports model names with the catalogue prefix
removed (gpt-5.4), unlike
/v1/models.
Transport details
- Request body limit 256 KB. Non-
2.0jsonrpcis rejected-32600. - Notifications (
notifications/*with noid) are answered202with an empty body. GETon the endpoint returns405withAllow: POST. That is deliberate: the server offers no optional server-to-client SSE stream, which the protocol permits.- Tool calls are rate-limited on the key's own per-minute budget, on a counter
separate from inference. Exceeding it returns a tool result with
isError: true, not a JSON-RPC error. /api/gme-ai/a1-mcpis an equivalent alias for the same handler.
{
"mcpServers": {
"a1-token": {
"url": "https://gme-ai.com/api/mcp/a1",
"headers": {
"Authorization": "Bearer gme-YOUR_KEY"
}
}
}
}
{
"mcpServers": {
"a1-token": {
"type": "streamableHttp",
"url": "https://gme-ai.com/api/mcp/a1",
"headers": {
"Authorization": "Bearer gme-YOUR_KEY"
},
"disabled": false,
"autoApprove": ["a1_balance", "a1_usage", "a1_key", "a1_models"]
}
}
}
Cursor -> Settings -> Models -> OpenAI API Key
Override Base URL : https://token.gme-ai.com/v1
API Key : gme-YOUR_KEY
Add model : atg-gpt-5.4 (or any id from /v1/models)
curl https://gme-ai.com/api/mcp/a1 \
-H "Authorization: Bearer $A1_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": {} },
"serverInfo": {
"name": "a1-token-mcp",
"version": "1.0.0"
},
"instructions": "These tools report on the A1 Token account behind the API key in use. They are READ-ONLY: nothing here can spend, move or refund anything, and none of them can see another account. They do not provide model access - for that, point the client at the A1 gateway as an OpenAI-compatible provider."
}
}
curl https://gme-ai.com/api/mcp/a1 \
-H "Authorization: Bearer $A1_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "a1_usage",
"arguments": { "granularity": "week", "days": 84, "scope": "account" }
}
}'
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "A1 Token usage - account scope, last 84 days ..."
}
],
"structuredContent": {
"scope": "account",
"granularity": "week",
"days": 84,
"totals": { "calls": 18420, "tokens_in": 29143300, "tokens_out": 4180770, "tokens_cached": 12048000, "cost": 98.4213 },
"by_model": [],
"by_period": [],
"note": "Scoped to the account that owns this key."
}
}
}
-32600 invalid_request jsonrpc was not "2.0"
-32601 method_not_found not initialize / tools/list / tools/call
-32602 unknown_tool params.name is not one of the four tools
-32603 internal_error the handler threw
-32001 <auth reason> key rejected; HTTP status is 401 or 403
-32000 <method guidance> returned on GET, with HTTP 405
Rate limits & spend caps
Three different mechanisms protect three different things, and only one of them is a hard wall. Knowing which is which is the difference between a quota UI that is right and one that lies to a customer.
Rate limit — enforced, in-process
A sliding 60-second window per key. The limit is the key's configured
rate_per_min, clamped to a hard ceiling of 60
requests per minute: configuring a key for 200/min still yields 60.
GET /v1/key reports the clamped figure, so what it tells you is
what you get. Exceeding it returns 429 rate_limit_exceeded.
Inference and documents share one counter per key. MCP tool calls use a separate counter with the same ceiling.
The counter lives in the API process's memory, not in the database. It therefore resets when the process restarts, and if the service is ever run as more than one process each would carry its own counter. Treat 60/min as a reliable ceiling on sustained traffic and not as a cryptographic guarantee about any single minute.
Spend caps — best-effort, soft
A key may carry a daily cap (a rolling 24 hours) and a monthly cap (calendar
month-to-date). Before each completion the gateway sums charged spend for the key and
refuses with 402 spend_cap_reached if the cap is already
met.
These are courtesy budgets, and they can be overshot. Two reasons, both deliberate:
- The check reads the ledger, and the ledger is written after the model answers. Requests already in flight are not yet visible, so a burst can cross the cap by a few calls.
- If the ledger read itself fails, the check fails open — it treats spend as zero and lets the call through. A budget must not become an outage.
This is why /v1/key reports
limit_enforcement: "soft" in the response body rather
than leaving you to discover it.
The balance — the hard ceiling
The real wall is the company's balance. When it empties, the call is refused with
402 and there is no fallback to another
model — a gateway that silently failed over would be spending somebody else's
money. Monitor /v1/credits, not the caps, if you want warning
before traffic stops.
Model restrictions
Two allowlists can narrow what a key may call: one set by the operator for the whole
company, one on the key itself. Both are enforced on the call itself, after alias
resolution, so an alias can never be used to slip past a block. They combine by
narrowing: a key can only ever restrict what its company already permits, never widen it.
A blocked model returns 403 model_not_allowed, and the same
restriction applies to GET /v1/models, so the list you are shown
is the list you can call. If the permissions cannot be read at all the call is refused
with 503 allowlist_unavailable and is safe to retry — an
unreadable restriction is never treated as no restriction.
See Models.
Model routes
Some model names are routes: a name your operator defined, backed by an
ordered list of real models. You call one exactly like any other model — it appears
in GET /v1/models if your company may use it.
A route is tried in order. If a step fails upstream before any of the response has reached you, the next one is tried; you receive one answer and are billed once, for the model that actually served. Nothing is retried once the response has started, and a refusal — an empty balance, an exhausted quota, a model that does not exist — is never retried, because a second supplier returns the same answer more slowly.
This is why model in the response can differ from the
model you sent: it always names what answered. Read it if you
need to know which model produced a given completion — the
usage feed records the same thing.
Rate limit 60 req/min per key HARD (in-process)
Daily cap rolling 24h, per key SOFT (may overshoot)
Monthly cap calendar MTD, per key SOFT (may overshoot)
Balance per company, USD HARD (no fallback)
Model allowlist company + per key HARD
{
"error": {
"message": "Rate limit exceeded — slow down.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded"
}
}
{
"error": {
"message": "Daily spend cap reached ($25).",
"type": "insufficient_quota",
"code": "spend_cap_reached"
}
}
# the courtesy budget
curl -s https://token.gme-ai.com/v1/key \
-H "Authorization: Bearer $A1_KEY"
# the actual wall
curl -s https://token.gme-ai.com/v1/credits \
-H "Authorization: Bearer $A1_KEY"
Errors
Every error is the OpenAI error envelope: an error
object with message,
type and code.
Branch on the HTTP status and code. Never parse
message — it is written for a human reading a
log and is free to change.
Status codes this API returns
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | messages missing or empty. |
| 400 | bad_granularity / bad_scope | Bad /v1/usage parameter. |
| 401 | invalid_api_key | Any authentication failure on an inference route. Reason in message. |
| 402 | spend_cap_reached | The key's daily or monthly cap is met. |
| 402 | insufficient_quota | The company balance is exhausted. There is no fallback model. |
| 402 | quota_exhausted | A usage quota upstream of the balance is exhausted. |
| 403 | model_not_allowed | The key's allowlist does not include the resolved model. |
| 403 | merchant_key_no_documents / key_has_no_document_access | Document reach was not granted to this key. |
| 404 | model_not_found | The resolved model is unavailable at dispatch time. |
| 404 | not_found | No document with that id is visible. |
| 409 | doc_full | The document is at maximum size. |
| 413 | too_long | A document append exceeded 100,000 characters. |
| 429 | rate_limit_exceeded | More than 60 requests in a rolling minute for this key. |
| 500 | internal_error / server_error | A bug on our side. Safe to retry once with backoff. |
| 502 | upstream_error | The model provider failed. See the warning below. |
| 503 | feature_disabled | The gateway, or the documents API, is switched off on this server. |
Retrying
Retry 429 after a pause, and
500 or 502 once with
backoff. Do not retry 400,
401, 402,
403 or 404: nothing about
the next attempt will differ. A 402 retry loop against a
soft cap is the one pattern that can still cost money, because the cap fails open
when the ledger cannot be read.
A 502 may not arrive as JSON. This host sits behind a CDN that
replaces the body of a 502 or 504 with its own HTML error page, so on
upstream_error your client can receive markup where it
expected an error envelope. Guard the parse: treat any
5xx whose body does not parse as a transient upstream
failure rather than crashing on it.
This is also why nothing on this API answers
424. Elsewhere on the platform, endpoints that move
money answer 424 rather than
502 or 504 precisely
because that body must survive: a result whose money outcome is uncertain
is useless to a client if the CDN has turned it into unparseable HTML. The
inference API takes no such uncertain outcome — a completion either billed or did
not — so it keeps the plain 502 an OpenAI client
already understands.
{
"error": {
"message": "human-readable, do not parse",
"type": "invalid_request_error",
"code": "invalid_request"
}
}
invalid_request_error 400, 401, 403, 404, and every /v1/key,
/v1/credits and /v1/usage error
insufficient_quota 402 — cap reached or balance exhausted
rate_limit_error 429
api_error 500, 502, 503
import time, httpx
def call(payload, tries=3):
for i in range(tries):
r = httpx.post(
"https://token.gme-ai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {KEY}"},
json=payload, timeout=120,
)
if r.status_code == 200:
return r.json()
if r.status_code in (429, 500, 502, 503):
time.sleep(2 ** i) # transient: back off
continue
# 400/401/402/403/404 will not change on a retry
try:
err = r.json()["error"]
except ValueError:
err = {"code": "unparseable", "message": r.text[:200]}
raise RuntimeError(f"{r.status_code} {err['code']}: {err['message']}")
raise RuntimeError("gave up after retries")
Managing your members
Everything above is written for one person holding one
gme- key. This section is for a company that
resells A1 Token to its own people — creating them, issuing their keys, funding
them, taking funding back, and reading what they spent. All of it is one credential and
plain HTTP; none of it needs a browser.
Two different credentials, and they are not interchangeable. A
gme- key calls models and can read only its
own balance and usage. A gmadm- management key
manages members and cannot call a model — sending one to an
inference route returns management_key_cannot_infer. That
refusal is deliberate: a credential that can move money must not also be pasted into
application code.
The management key
Ask your A1 Token operator to issue one. It arrives once, begins
gmadm-, and is stored only as a hash — it cannot be read
back. It carries an optional expiry and can be revoked at any time. Send it the same way
as any other key:
curl https://token.gme-ai.com/api/gme-ai/merchant/customers \
-H "Authorization: Bearer gmadm-…"
Every route below is scoped to your company by the key itself. A member id
belonging to another company returns customer_not_found, not a
permission error — you cannot discover that it exists.
Members
| Endpoint | What it does |
|---|---|
POST /api/gme-ai/merchant/customers | Creates a member under your company and provisions their own balance account. |
GET /api/gme-ai/merchant/customers | Lists your members with their current balances. |
GET /api/gme-ai/merchant/usage | Usage across your company, broken down by member. |
Their keys
| Endpoint | What it does |
|---|---|
POST /api/gme-ai/merchant/keys | Mints a gme- key bound to one of your members. The full key is returned once. |
GET /api/gme-ai/merchant/keys | Lists issued keys by prefix. The stored material is never readable. |
PATCH /api/gme-ai/merchant/keys/:id | Adjusts a key’s spend caps and rate. |
POST /api/gme-ai/merchant/keys/:id/rotate | Issues a replacement and retires the old one. |
POST /api/gme-ai/merchant/keys/:id/revoke | Kills a key immediately. Not reversible. |
Money
POST/api/gme-ai/merchant/customers/:id/topup
Moves funds from your company pool to one member. The pool is the wallet you fund; members draw from what you give them, not from each other.
curl https://token.gme-ai.com/api/gme-ai/merchant/customers/482/topup \
-H "Authorization: Bearer gmadm-…" \
-H "Idempotency-Key: payroll-2026-09-24-482" \
-H "Content-Type: application/json" \
-d '{"amount_usd": 25.00}'
POST/api/gme-ai/merchant/customers/:id/clawback
The reverse: returns unspent funds from a member to the pool. Use it when someone leaves, or when an allocation was wrong.
Always send an Idempotency-Key. A network
timeout does not tell you whether the money moved. With the header, replaying the
identical request returns the original result instead of moving the money a
second time; a replay still in flight returns
request_in_progress (409). Without it, a retry is a second
transfer.
| Status | Meaning | What to do |
|---|---|---|
200 | Moved. | Nothing. |
402 | Your pool does not hold enough. The response carries pool_balance. | Fund the pool, then retry with the same idempotency key. |
409 | request_in_progress — the same idempotency key is still running. | Wait, then retry the same key. Do not mint a new one. |
424 | The outcome is unknown. The transfer was dispatched and the upstream did not confirm. | Do not retry blind. Read the member’s balance, or retry the identical idempotency key, which will replay the original outcome rather than repeat the transfer. |
404 | customer_not_found — no such member under your company. | Check the id. This is also what another company’s member looks like. |
424 is not a failure and not a success. It means the money
may or may not have moved and the system will not guess on your behalf. Treating it as
a failure and retrying without the original idempotency key is how a member gets funded
twice.
Telling your members apart in a shared key
If you route your members’ traffic through one key rather than issuing each of them their own, stamp every request with your own identifier for the person:
{
"model": "atg-gpt-5.4-mini",
"messages": [{"role": "user", "content": "…"}],
"user": "acct_8812"
}
The value is recorded against the call and returned in your usage reports. It is opaque to A1 Token — never parsed, never matched to an account, never shown to anyone else. Send a stable internal id rather than an email address: it is stored, and an identifier you can rotate is safer than one a person carries for life.
Managing keys
Key management is a different surface with different auth. These
routes are authenticated by a signed-in portal session, not by a
gme- key, precisely so that a leaked key cannot mint more
keys or revoke its siblings. A key can never create another key —
/v1/key reports
is_provisioning_key: false for exactly this reason.
| Endpoint | Does |
|---|---|
POST /api/gme-ai/gateway-keys | Mint a key. Body: name, spend_cap_usd, scopes. |
GET /api/gme-ai/gateway-keys | List your keys, masked. |
POST /api/gme-ai/gateway-keys/:id/revoke | Revoke one. Immediate and permanent. |
GET /api/gme-ai/gateway-keys/usage | Account-wide API traffic, split by key. days must be 7, 28, 90 or 365. |
Minting
scopes is an array, and today
"docs" is the only grantable value — any other word is
dropped rather than stored, so a scope this build does not enforce can never sit in a
row waiting to be honoured later. Omit it and the key can call models and nothing
else.
The full key comes back once, as
token.raw_token_shown_once. After that the list endpoint
returns only token_hint — the prefix, an ellipsis and the
last four characters. There is no endpoint that reveals a key again. Lose it and you
revoke and re-mint.
Revoking
Revocation sets a timestamp and takes effect on the next request; the key returns
401 api_key_revoked from then on. It cannot be undone,
and revoking an already-revoked key returns
404 key_not_found. Revoke freely — it only ever reduces
spend.
Account-level usage
GET /api/gme-ai/gateway-keys/usage is the operator's
view that /v1/usage is the customer's view of. It counts only
traffic that came through the API — portal usage is excluded — and adds a
by_key breakdown so you can see which key or service is
responsible for the spend. That breakdown is the reason to issue a key per service
rather than sharing one.
curl https://gme-ai.com/api/gme-ai/gateway-keys \
-H "Content-Type: application/json" \
-b "$PORTAL_SESSION_COOKIE" \
-d '{
"name": "ci-build-runner",
"spend_cap_usd": 25,
"scopes": []
}'
{
"ok": true,
"token": {
"id": 4812,
"name": "ci-build-runner",
"token_prefix": "gme-xK3q8Lm2",
"raw_token_shown_once": "gme-xK3q8Lm2Vn9pRtYw4ZbD7eF1",
"created_at": "2026-09-23T10:41:07.000Z",
"scopes": []
},
"config": {
"base_url": "https://token.gme-ai.com/v1",
"api_key": "gme-xK3q8Lm2Vn9pRtYw4ZbD7eF1",
"cursor": "Cursor → Settings → Models → OpenAI API Key: Override Base URL = https://token.gme-ai.com/v1 , API Key = ... , add model e.g. atg-gpt-5.4",
"curl": "curl https://token.gme-ai.com/v1/chat/completions ...",
"openai_sdk": "client = OpenAI(base_url=\"https://token.gme-ai.com/v1\", api_key=\"...\")"
},
"warning": "Copy this key now. Treat it like a password ..."
}
{
"ok": false,
"error": "key_limit_reached",
"detail": "You can have at most 25 active API keys. Revoke an unused one first."
}
{
"ok": true,
"days": 28,
"totals": { "calls": 7412, "tokens_in": 11840221, "tokens_out": 1702004, "cost_usd": 39.8814 },
"by_model": [
{ "model_id": "gpt-5.4", "calls": 6011, "tokens_in": 10420118, "tokens_out": 1480221, "cost_usd": 36.1102 }
],
"by_day": [
{ "d": "2026-09-22", "calls": 344, "cost_usd": 1.8410 }
],
"by_key": [
{ "gateway_key_id": 4812, "name": "ci-build-runner", "token_prefix": "gme-xK3q8Lm2", "calls": 5120, "cost_usd": 28.4411 }
]
}