Get OBOL
$PONTICAat launch

Docs / API

API reference

An OpenAI and OpenRouter compatible API: point any client at the base URL below with your Pontica key. Keys are issued by wallet signature, and the balance behind them is what the chain credited to your wallet.

Checked against this release on 22 Sep 2026

Base URL and auth#

Base URL
https://pontica.gg/api/v1
Auth
Authorization: Bearer sk-or-v1-…, your Pontica key
Format
JSON in, JSON out; server-sent events with "stream": true
Money
USD at the model's list price. 1 OBOL activated is $1 of balance.
Browsers
CORS is open to every origin; no cookies are read.

A Pontica key is an OpenRouter key that the gateway issued into Pontica's own OpenRouter workspace. The gateway accepts only the keys it issued: any other key gets 401 not_a_pontica_key.

Quick start#

Put your key in the environment variable PONTICA_API_KEY (issue it on your account page), then call any model id from the catalogue. The model in these snippets comes from Pontica's picks.

Illustrative
curl https://pontica.gg/api/v1/chat/completions \
  -H "Authorization: Bearer $PONTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-5",
    "messages": [{ "role": "user", "content": "Say hello in Greek." }]
  }'

Tools that speak the OpenAI API work the same way: set the base URL to https://pontica.gg/api/v1 and the key to your Pontica key.

Issue a key#

A wallet gets its key by signing a Sign-In with Ethereum (EIP-4361) message and posting it. The account page does this for you; here is the exact protocol for your own client.

1. Build the message

Byte for byte, with the lines joined by \n and no newline at the end:

Issue message
pontica.gg wants you to sign in with your Ethereum account:
{address, EIP-55 checksum}

Issue a Pontica API key for this wallet. This retires any key it already has.

URI: https://pontica.gg
Version: 1
Chain ID: 4663
Nonce: {16 lowercase hex from crypto.getRandomValues}
Issued At: {UTC ISO 8601 with seconds and Z, e.g. 2026-09-22T09:30:00Z}
Expiration Time: {Issued At + 10 minutes, same format}
  • The address line must be the EIP-55 checksum form of the address you post.
  • The nonce is 16 lowercase hex characters, fresh for every message.
  • Issued At may be at most 60 s ahead of the gateway's clock; Expiration Time at most 10 minutes after Issued At.
  • The statement line decides the action. Only the two statements on this page are accepted.

2. Sign it

Sign with personal_sign (EIP-191) over the UTF-8 bytes of the message, sent to the wallet as hex. The prefix counts bytes, not characters. Signatures of ordinary accounts are recovered (v may be 27, 28, 0 or 1; high s values are refused). When recovery does not match and the address has code, the gateway asks the contract with EIP-1271 isValidSignature. EIP-7702 accounts pass by recovery. Smart wallets that are not deployed yet (ERC-6492) are not supported: deploy the wallet first.

A signed issue message works like a password until it is used or expires, 10 minutes at most. Never paste it anywhere.

3. Post it

Request
POST https://pontica.gg/api/v1/keys
Content-Type: application/json

{ "address": "0x…", "message": "pontica.gg wants you to sign in…", "signature": "0x…" }

The gateway checks the message and the signature, reads what the chain credited to the wallet (at least 1 OBOL is needed), creates the new key, retires every other key of the wallet and answers 201 with the key, once, with cache-control: no-store:

keystring
The secret, sk-or-v1-…. In this answer only: Pontica cannot show it again.
hashstring
SHA-256 of the key, 64 hex. Safe to keep; it syncs the key.
base_urlstring
https://pontica.gg/api/v1
addressstring
The wallet, EIP-55.
activated_usdnumber
Everything the chain has credited to the wallet.
used_usdnumber
What the wallet's earlier keys spent (the carry).
limit_usd, remaining_usdnumber
The new key's limit: credited minus carry.
replacednumber
How many keys this retired.
notestring
"Shown once. Pontica cannot show it again."
  • Replay guard. Each key remembers the Issued At of its message. A message not newer than the wallet's last one is refused with 409 signature_used: sign a new one.
  • Racing requests. If two issues for one wallet cross, the earliest key wins and the other answers 409 issue_conflict. Sign again.
  • Rate. At most 5 keys per wallet in 24 hours: 429 too_many_keys with Retry-After.
  • Retired keys. The old key stops at once. Requests already running on it finish, and what they cost is carried into the new key's accounting.

Sync a key#

After a new activation, POST /api/v1/keys/sync moves the key's limit to what the chain credited. It never creates a key, never returns a secret and never lifts a limit above what the chain proves. Three ways in:

  • The key: Authorization: Bearer sk-or-v1-… and no body. Best for code.
  • The hash: body { "hash": "…" }, the SHA-256 of the key. What the account page keeps.
  • A signature: body { address, message, signature } with the statement Sync the limit of this wallet's Pontica API key. in the same message format. For a lost hash.

It answers 200 with { address, hash, activated_usd, used_usd, limit_usd, remaining_usd, synced }, where synced is true when this call moved the limit. The new limit is the larger of the key's usage and credited minus carry. A hash sync repeated within 10 seconds returns the same numbers unless an activation landed.

Errors: 401 key_retired (another, older key of the wallet is the live one), 401 not_a_pontica_key, 404 no_key, 409 issue_conflict (a key for this wallet is being issued right now; try again in a minute), 400 missing_field for a malformed hash.

GET /key#

GET /api/v1/key with your key answers OpenRouter's own key answer (limit, limit_remaining, usage and the rest, in USD) plus a pontica field for keys Pontica issued:

address
The wallet the key belongs to.
activated_usd
Everything the chain has credited to that wallet.
used_usd
Everything the wallet's keys have spent.
remaining_usd
What is left to spend.
retired
True when a newer key replaced this one.
needs_sync
True when an activation is not in the key's limit yet: call sync.

For a key Pontica did not issue, pontica is null.

Chat completions#

POST /api/v1/chat/completions is OpenAI and OpenRouter compatible. The body goes to OpenRouter byte for byte, and the answer comes back untouched.

  • Streaming. With "stream": true the answer is server-sent events: data: lines, keep-alive comments (: OPENROUTER PROCESSING), then data: [DONE]. The last chunk before [DONE] carries usage with cost, the USD charged at list price, which is what your balance pays.
  • Cancelling. Close the connection. The gateway passes the cancel on to OpenRouter; some providers stop billing there, others finish the reply and bill it.
  • Limits. Bodies over 4.5 MB answer 413 body_too_large, and a request may run 300 s including its stream; past that the platform cuts it without [DONE]. For larger or longer requests call https://openrouter.ai/api/v1 directly with the same key: it is an OpenRouter key, with the same balance.
  • Headers. The gateway adds HTTP-Referer: https://pontica.gg and X-OpenRouter-Title: Pontica and forwards nothing else from your client: no IP address, cookies or user agent. It returns content-type, x-generation-id, x-provider-name and retry-after.
  • Errors. 401 missing_key, not_a_pontica_key or key_retired from the gateway; everything else, including 402 when your limit is used up, comes from OpenRouter unchanged. While the gateway cannot check keys (its OpenRouter key expired or OpenRouter is unreachable), chat keeps working for the keys it cannot check.

Models#

GET /api/v1/models answers OpenRouter's catalogue JSON unchanged (data, total_count, links) plus pontica: { book, fetched_at, source: "openrouter" }. book is null until the book has asks; then it is { obol_price_usd, discount_bps, depth_obol, fee_bps, quote_usdg: "10", block }, where obol_price_usd is what a 10 USDG book buy pays per OBOL, book fee included.

  • By default it lists text output models, like OpenRouter. Allowed query parameters: output_modalities, input_modalities, supported_parameters, category. Anything else answers 400 unsupported_parameter.
  • Prices are USD per token as decimal strings. Router models answer "-1": their price varies per request.
  • Cached 60 s in the browser and 5 minutes at the edge. When OpenRouter fails, the last good copy is served with x-pontica-stale: 1; with no copy the answer is 502, never a made up list.
  • GET /api/v1/models/count answers { count, fetched_at } and accepts only output_modalities.

Key activity#

GET /api/v1/keys/activity with your key answers { address, hash, retired, data }, where data is OpenRouter's activity report for that key: per UTC day and model, the requests, prompt, completion and reasoning tokens and the USD spent, for the last 30 completed days. No prompt data exists there. Retired keys can still read their history.

Status and stats#

GET /api/status always answers 200 and is cached 15 s:

gateway.connected, reason
Whether the gateway can issue keys, and why not: not_configured, key_expired, key_invalid, not_management_key, upstream_unreachable.
gateway.workspace
The Pontica workspace's logging switches at OpenRouter: io_logging, data_discount_logging, broadcast.
gateway.budget_usd
The workspace's lifetime budget, kept equal to all OBOL ever credited.
credits_remaining_usd, outstanding_usd, covered
The operator's OpenRouter balance, the credited balance not spent yet, and whether the first covers the second.
chain
{ id, block, rpc, deployed, paused }, rpc one of ok, degraded, down.
models
{ count, fetched_at } of the live catalogue.
See the live answer
Loading

GET /api/stats answers protocol totals from the contracts' own counters, the events of the last 24 hours (at most 50, newest first, with real block times) and the gateway's usage over 30 days; cached 60 s. GET /api/stats/daily?days=7 (or 30, 90) answers one row per UTC day.

Errors#

The gateway's own errors use the OpenAI shape, { "error": { "message", "type", "code", "param" } }, with a plain sentence in message. Errors from OpenRouter pass through unchanged, in OpenRouter's shape.

400invalid_request_error

bad_json missing_field bad_address bad_message wrong_chain wrong_domain address_mismatch signature_expired signature_from_future unsupported_parameter

The request or the signed message was not accepted; the message says which part. Sign again.

401authentication_error

missing_key bad_signature not_a_pontica_key key_retired

No usable key or signature. Use your current key or issue a new one.

402insufficient_quota

nothing_activated

Activate OBOL to this wallet first.

404not_found_error

no_key not_found

This wallet has no key yet, or no key has that hash.

405invalid_request_error

method_not_allowed

Wrong method; the Allow header lists the right ones.

409conflict_error

signature_used issue_conflict

Another key request crossed this one, or the message was used. Sign again.

413invalid_request_error

body_too_large

Over 4.5 MB. Send it to openrouter.ai/api/v1 with the same key.

429rate_limit_error

too_many_keys

This wallet issued 5 keys in the last 24 hours. Retry-After says when to try again.

500api_error

internal_error

An unexpected error. Try again.

502api_error

upstream_unavailable chain_unavailable

The gateway could not reach OpenRouter or Robinhood Chain. Try again.

503service_unavailable

gateway_not_connected gateway_key_expired

The inference gateway is not connected yet, or its OpenRouter key expired.

Limits#

Free models and rate limits

  • Paid models: OpenRouter sets no request limit per key; its capacity is shared globally.
  • Free models (ids ending in :free) have per minute and per day caps per OpenRouter account. Every Pontica key shares the operator's account, so heavy use by one key can use up the free quota for everyone that day.
  • Your balance is the real limit. When your key's limit is used up, OpenRouter answers 402. Activate more OBOL and sync.
  • Overshoot. OpenRouter checks a key's limit when a request starts and charges it when it ends, so many expensive requests at once can end above the limit. The gateway counts the overshoot against the wallet: its next limit stays at its usage until a new activation covers it.

Known limits of the gateway

  • About 10,000 keys. The gateway keeps no database: each key's bookkeeping lives in its name at OpenRouter, and OpenRouter's key list cannot be filtered, so issuing a key and a signed sync read the whole list. That holds up to about 10,000 keys. Past it the ledger needs splitting across workspaces or a small store. A known limit, accepted for launch.
  • Hosting limits (Vercel). Request bodies up to 4.5 MB; a chat request may run 300 s including its stream; key routes 30 s; everything else 20 s.
  • A hard ceiling. The Pontica workspace at OpenRouter has a lifetime budget kept equal to all OBOL ever credited, so all keys together can never spend more than the chain credited.
  • The operator's key expires. OpenRouter fixes the expiry of the management key at creation. When it passes, key routes answer 503 gateway_key_expired until the operator rotates it; /api/status shows the date.

Contract addresses#

The Pontica contracts on Robinhood Chain (chain id 4663), non upgradeable, with their source verified on Sourcify. ABIs: obol, mint, exchange, staking, funds. The full agent guide is Pontica for agents.

Connect a wallet

Pontica never holds your keys. Connecting shares your address only.