Your tip jar, programmatically.
Read your tips and balance, rotate your inference key, and receive the tip lifecycle as it happens — over signed webhooks or a live stream. One bearer token, exact-decimal money, and a contract published as OpenAPI and AsyncAPI.
Base URL
https://tokentip.to/v1
Authentication
Every request carries a bearer token minted in your
dashboard. Tokens start with tt_, are shown
exactly once, and are stored only as a SHA-256 hash — we cannot recover one for you.
curl -H "Authorization: Bearer tt_your_token" https://tokentip.to/v1/me
Scopes
A token is one of two scopes, enforced server-side on every endpoint and event:
| Scope | Sees | Can do |
|---|---|---|
| creator | Its own account only — its tips, balance, key. | Read, manage webhooks, rotate its key. |
| operator | The whole system, every creator. | Everything, plus force-settle and disable a key. |
A creator token calling an /v1/operator/* endpoint gets 403.
Quickstart
Fetch the authenticated account and its current inference credit:
curl -H "Authorization: Bearer tt_your_token" https://tokentip.to/v1/me
from tokentip import TokenTip
with TokenTip("tt_your_token") as tt:
me = tt.me()
print(me.handle, me.credit) # credit is a Decimal, never a float
tt := tokentip.New("tt_your_token")
me, err := tt.Me(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println(me.Handle, me.Credit)
import { TokenTip } from "@copyleftdev/tokentip";
const tt = new TokenTip("tt_your_token");
const me = await tt.me();
console.log(me.handle, me.credit);
Conventions
Money is never a float
Every monetary value is an exact decimal string with six-decimal precision —
the same arithmetic the server settles with. Parse it into a decimal type
(Decimal, big.Rat, decimal.js), never a binary float.
{ "gross": "5.000000", "net_to_tokens": "4.068225" }
Cursor pagination
List endpoints take limit (1–100, default 40) and return
next_cursor. Pass it back as cursor for the next page;
null means you have reached the end.
curl -H "Authorization: Bearer tt_…" \
"https://tokentip.to/v1/tips?limit=20&status=settled&cursor=2026-07-19T10:03:00Z"
Idempotency
Mutating calls accept an Idempotency-Key header (1–255 characters) so
a retry after a timeout cannot double-apply. Reuse the same key for the same logical
operation. Replayed responses carry Idempotent-Replay: true; a request
that arrives while an identical one is still running gets 409.
One exception, deliberately. POST /v1/key/rotate
returns a secret the server never stores, so a replay cannot hand it back — it
returns 409 explaining that the rotation already happened. The key
still does its job: your retry will not silently mint a second key and disable the
one you just received.
Keys are remembered for 24 hours.
Rate limits
Requests are limited per token — per client address when unauthenticated — to a
sustained 4 requests/second with a burst of 120. Exceeding it
returns 429 with a Retry-After header in seconds; wait
that long and retry.
Concurrent event streams are capped at 5 per creator. Opening a
sixth returns 429. Close streams you are not reading — the budget is
released as soon as the connection drops.
Errors
Every non-2xx response is application/problem+json
(RFC 7807). The SDKs map these
onto typed exceptions.
{ "title": "not an operator", "status": 403,
"detail": "This endpoint requires an operator-scoped token." }
| Status | Meaning |
|---|---|
| 401 | Missing, malformed, or unknown token. |
| 403 | Valid token, wrong scope — operator required. |
| 404 | No such resource, or not yours. |
| 409 | Conflict — e.g. rotating a key that changed underneath you. |
| 429 | Rate limited, or too many open streams. See Retry-After. |
| 502 | Upstream (OpenRouter) failed; safe to retry. |
The contract
Both specs are published and validate clean. They share
components/schemas.yaml, so a Tip is the same shape whether you
read it from REST or receive it in an event — the two surfaces cannot drift.