A1 Token Developer docs

Developer reference

A1 Token API

One OpenAI-compatible endpoint for Claude, GPT, DeepSeek, Kimi, GLM and Qwen. Every key carries its own spend caps, rate limit and usage history, and the company balance is the hard ceiling behind all of them.

https://token.gme-ai.com/v1 OpenAI-compatible Streaming and tool calls One key per person or service

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.

Base URL
https://token.gme-ai.com/v1
Every request
Authorization: Bearer gme-<your key>
Content-Type: application/json

Quickstart

Three steps from nothing to a completion.

  1. 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.

  2. Point your client at the gateway

    Set the base URL to https://token.gme-ai.com/v1 and send the key as Authorization: Bearer. Nothing else in an OpenAI client changes.

  3. 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 OpenAI chat.completion object.

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.

First request
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."}
    ]
  }'
First request
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)
First request
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);
Response
{
  "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:

Authentication failure reasons
ReasonStatusMeaning
missing_api_key401No Authorization: Bearer header, or an empty one.
invalid_api_key401No key with that value exists.
api_key_revoked401The key was revoked. Revocation is immediate and permanent.
api_key_expired401The key had an expiry and it has passed.
account_inactive401The owning account is not active.
management_key_cannot_infer401A gmadm- management key was sent to an inference route.
merchant_suspended403The 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.

Header
Authorization: Bearer gme-xK3q8Lm2Vn9pRtYw4ZbD7eF1
401 on an inference route
{
  "error": {
    "message": "api_key_revoked",
    "type": "invalid_request_error",
    "code": "invalid_api_key"
  }
}
401 on /v1/key, /v1/credits, /v1/usage
{
  "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.

Python — the whole change
from openai import OpenAI

client = OpenAI(
    base_url="https://token.gme-ai.com/v1",   # <- this line
    api_key=os.environ["A1_KEY"],
)
Node — the whole change
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

Chat completion request parameters
FieldTypeRequiredNotes
messagesarrayrequiredOpenAI message objects. Must be a non-empty array or the call returns 400 invalid_request.
modelstringoptionalA model id or alias. Omitted, empty or "auto" selects the gateway default. See Model resolution below.
streambooleanoptionalExactly true turns on SSE framing. Any other value is treated as off. See Streaming.
toolsarrayoptionalOpenAI tool definitions, passed to the model.
tool_choicestring / objectoptionalPassed through unchanged.
temperaturenumberoptionalPassed through unchanged.
top_pnumberoptionalPassed through unchanged.
max_tokensintegeroptionalDefaults to 2048 when absent or zero. Raise it for long answers.
response_formatobjectoptionalOnly {"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.

Request with tools
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"]
        }
      }
    }]
  }'
200 — tool call
{
  "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
  }
}
400 — no messages
{
  "error": {
    "message": "messages is required",
    "type": "invalid_request_error",
    "code": "invalid_request"
  }
}
403 — blocked by the key allowlist
{
  "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 a usage object.
  • 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.

Request
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."}]
  }'
Event stream
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]
Final chunk, real-streaming path only
{
  "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:

  1. 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”.
  2. 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.

Request
curl https://token.gme-ai.com/v1/models \
  -H "Authorization: Bearer $A1_KEY"
200 — abbreviated
{
  "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" }
  ]
}
Field notes
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.

Key introspection response fields
FieldTypeMeaning
labelstringkey_ plus the key's numeric id. Not the name you gave it at creation.
usagenumberLifetime charged spend attributed to this key, in USD. Not a period figure.
limitnumber | nullThe daily spend cap, or null for no cap.
limit_remainingnumber | nulllimit minus spend in the rolling last 24 hours, floored at 0. null when there is no cap.
limit_enforcementstringAlways "soft". See Rate limits & spend caps.
monthly_limitnumber | nullThe calendar-month-to-date cap, or null.
is_free_tierbooleanAlways false. There is no free tier.
is_provisioning_keybooleanAlways false. Keys cannot mint other keys.
rate_limit.requestsnumberRequests permitted per interval, already clamped to the enforced ceiling of 60.
rate_limit.intervalstringAlways "60s".
model_allowlistarray | nullCatalogue 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.

Request
curl https://token.gme-ai.com/v1/key \
  -H "Authorization: Bearer $A1_KEY"
200
{
  "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
  }
}
200 — restricted key, no caps
{
  "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.

Credits response fields
FieldTypeMeaning
balancenumber | nullThe authoritative figure: money remaining. null when it could not be read.
total_usagenumberLifetime 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_creditsnumber | nullDerived as balance + total_usage, so that remaining = total_credits − total_usage holds. null when the balance is unavailable.
balance_statusstring"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.

Request
curl https://token.gme-ai.com/v1/credits \
  -H "Authorization: Bearer $A1_KEY"
200
{
  "data": {
    "total_credits": 512.834112,
    "total_usage": 87.165912,
    "balance": 425.6682,
    "balance_status": "reported"
  }
}
200 — balance unreadable
{
  "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

Usage query parameters
ParameterTypeRequiredNotes
granularitystringoptionalday (default), week or month. Anything else returns 400 bad_granularity.
daysintegeroptionalHow far back to look. Default 30, clamped to 1–365. A non-numeric value falls back to 30 rather than erroring.
scopestringoptionalkey (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

Usage response fields
FieldTypeMeaning
scope, granularity, daysstring, string, numberThe resolved parameters, echoed after clamping.
from, tostring | nullYYYY-MM-DD. from is null only if no bucket was produced.
totalsobjectcalls, tokens_in, tokens_out, tokens_cached, cost.
by_model[]arraymodel plus the same five counters, ordered by cost descending.
by_period[]arrayperiod, calls, tokens_in, tokens_out, cost. No tokens_cached here — it exists in totals and by_model only.
notestringPlain-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.

Last 7 days, daily
curl "https://token.gme-ai.com/v1/usage?granularity=day&days=7" \
  -H "Authorization: Bearer $A1_KEY"
Last 12 weeks, weekly
curl "https://token.gme-ai.com/v1/usage?granularity=week&days=84" \
  -H "Authorization: Bearer $A1_KEY"
Last 12 months, whole account
curl "https://token.gme-ai.com/v1/usage?granularity=month&days=365&scope=account" \
  -H "Authorization: Bearer $A1_KEY"
200 — daily, abbreviated
{
  "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."
  }
}
400 — bad granularity
{
  "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.

Document endpoints
EndpointDoes
GET /v1/documentsList visible documents. Query: q (search, 80 chars), kind (doc or note), limit (1–100, default 25).
GET /v1/documents/:idRead one, as plain text, with truncated and chars so you know if you got all of it.
POST /v1/documentsCreate. Body: title (required), text (Markdown) or html, kind.
POST /v1/documents/:id/appendAppend 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

Document error codes
StatusCodeMeaning
503feature_disabledThe documents API is not enabled on this server. It is off by default.
403merchant_key_no_documentsA merchant-issued key. It can never reach documents.
403key_has_no_document_accessThe key was created without document reach. Mint a new one with it.
403read_onlyThe document was shared to read, not to change. Nothing was written.
404not_foundNo document with that id is visible to this account.
409doc_fullThe document is at its maximum size. Nothing was written.
413too_longA single append may add at most 100,000 characters. Send it in parts.
400emptyNeither text nor html carried content.
List
curl "https://token.gme-ai.com/v1/documents?q=runbook&limit=5" \
  -H "Authorization: Bearer $A1_KEY"
200 — list
{
  "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
    }
  ]
}
Append
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."}'
200 — append
{
  "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.

MCP tools
ToolArgumentsReturns
a1_balancenoneText plus { balance, status }. A null balance means unreadable, not zero.
a1_usagegranularity (day | week | month), days (1–365), scope (key | account)A readable summary plus the exact same object GET /v1/usage returns.
a1_keynoneCaps, rate limit, allowlist, plus rate_enforcement and cap_enforcement.
a1_modelsnone{ 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.0 jsonrpc is rejected -32600.
  • Notifications (notifications/* with no id) are answered 202 with an empty body.
  • GET on the endpoint returns 405 with Allow: 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-mcp is an equivalent alias for the same handler.
~/.cursor/mcp.json
{
  "mcpServers": {
    "a1-token": {
      "url": "https://gme-ai.com/api/mcp/a1",
      "headers": {
        "Authorization": "Bearer gme-YOUR_KEY"
      }
    }
  }
}
cline_mcp_settings.json
{
  "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 as a model provider — separate setting
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)
initialize
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":{}}'
initialize — result
{
  "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."
  }
}
tools/call — weekly usage
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" }
    }
  }'
tools/call — result
{
  "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."
    }
  }
}
JSON-RPC error codes
-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.

At a glance
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
429
{
  "error": {
    "message": "Rate limit exceeded — slow down.",
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded"
  }
}
402 — daily cap
{
  "error": {
    "message": "Daily spend cap reached ($25).",
    "type": "insufficient_quota",
    "code": "spend_cap_reached"
  }
}
Check headroom before a batch
# 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

Error status codes
StatusCodeWhen
400invalid_requestmessages missing or empty.
400bad_granularity / bad_scopeBad /v1/usage parameter.
401invalid_api_keyAny authentication failure on an inference route. Reason in message.
402spend_cap_reachedThe key's daily or monthly cap is met.
402insufficient_quotaThe company balance is exhausted. There is no fallback model.
402quota_exhaustedA usage quota upstream of the balance is exhausted.
403model_not_allowedThe key's allowlist does not include the resolved model.
403merchant_key_no_documents / key_has_no_document_accessDocument reach was not granted to this key.
404model_not_foundThe resolved model is unavailable at dispatch time.
404not_foundNo document with that id is visible.
409doc_fullThe document is at maximum size.
413too_longA document append exceeded 100,000 characters.
429rate_limit_exceededMore than 60 requests in a rolling minute for this key.
500internal_error / server_errorA bug on our side. Safe to retry once with backoff.
502upstream_errorThe model provider failed. See the warning below.
503feature_disabledThe 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.

Envelope
{
  "error": {
    "message": "human-readable, do not parse",
    "type": "invalid_request_error",
    "code": "invalid_request"
  }
}
type values in use
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
Handling it — Python
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

Member management endpoints
EndpointWhat it does
POST /api/gme-ai/merchant/customersCreates a member under your company and provisions their own balance account.
GET /api/gme-ai/merchant/customersLists your members with their current balances.
GET /api/gme-ai/merchant/usageUsage across your company, broken down by member.

Their keys

Member key endpoints
EndpointWhat it does
POST /api/gme-ai/merchant/keysMints a gme- key bound to one of your members. The full key is returned once.
GET /api/gme-ai/merchant/keysLists issued keys by prefix. The stored material is never readable.
PATCH /api/gme-ai/merchant/keys/:idAdjusts a key’s spend caps and rate.
POST /api/gme-ai/merchant/keys/:id/rotateIssues a replacement and retires the old one.
POST /api/gme-ai/merchant/keys/:id/revokeKills 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.

Money endpoint status codes
StatusMeaningWhat to do
200Moved.Nothing.
402Your pool does not hold enough. The response carries pool_balance.Fund the pool, then retry with the same idempotency key.
409request_in_progress — the same idempotency key is still running.Wait, then retry the same key. Do not mint a new one.
424The 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.
404customer_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.

Key management endpoints
EndpointDoes
POST /api/gme-ai/gateway-keysMint a key. Body: name, spend_cap_usd, scopes.
GET /api/gme-ai/gateway-keysList your keys, masked.
POST /api/gme-ai/gateway-keys/:id/revokeRevoke one. Immediate and permanent.
GET /api/gme-ai/gateway-keys/usageAccount-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.

Mint — session-authenticated
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": []
  }'
200 — shown once
{
  "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 ..."
}
409 — too many live keys
{
  "ok": false,
  "error": "key_limit_reached",
  "detail": "You can have at most 25 active API keys. Revoke an unused one first."
}
Account usage, split by key — abbreviated
{
  "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 }
  ]
}