Token Relay is a production-grade API gateway that lets you resell Chinese LLM APIs (Qwen, DeepSeek, Zhipu) to global customers. It exposes an OpenAI-compatible endpoint so your users can plug in their existing SDKs with zero code changes.
Built for the Southeast Asian B2B SaaS market — deploy on an Alibaba Cloud HK instance and start monetizing Chinese AI models.
User SDK (OpenAI client)
│
▼
Token Relay API (/v1)
│
┌────┴────┐
│ Auth │ Cookie + JWT (dashboard) / API Key (proxy)
├─────────┤
│ Billing │ Pre-auth credit hold → stream → refund delta
├─────────┤
│ Adapter │ OpenAI-compatible → Qwen / DeepSeek / Zhipu
├─────────┤
│ Logging │ Metadata-only (no conversation content stored)
└────┬────┘
│
▼
Upstream LLM Provider
- OpenAI-compatible
/v1/chat/completions,/v1/models,/v1/embeddings - Streaming support with accurate per-token billing
- Pre-auth billing — estimates cost upfront, refunds the delta after streaming completes
- Multi-provider key rotation — same provider, multiple keys, priority-based failover
- Configurable pricing — global markup ratio + per-model price overrides
- User portal — dashboard, API key management, usage analytics, billing history
- Admin console — user management, model CRUD, provider key management, system settings
- Dual auth — JWT (web dashboard) + API Key (proxy endpoints), Cookie pass-through
- SQLite dev / PostgreSQL prod — switch via
DATABASE_URL - Docker — single-command deploy with PostgreSQL + Redis
- Python 3.11+
- uv or pip
git clone https://github.com/functionkiller/token-relay.git
cd token-relay
cp .env.example .env
# Edit .env — set SECRET_KEY and ENCRYPTION_KEYpip install -r requirements.txt
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload| Interface | URL |
|---|---|
| User Portal | http://localhost:8000/login |
| Admin Console | http://localhost:8000/admin/login |
| API Docs (Swagger) | http://localhost:8000/docs |
| Health Check | http://localhost:8000/health |
Default admin: admin@tokenrelay.com / admin123456
# 1. Register via the portal or API
curl -X POST http://localhost:8000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"dev@example.com","password":"mypassword"}'
# 2. Login to get a cookie
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"dev@example.com","password":"mypassword"}'
# 3. Create an API key (or use the portal UI)
curl -X POST http://localhost:8000/api/users/me/keys \
-H "Content-Type: application/json" \
-b "token=<jwt>" \
-d '{"name":"my-app"}'
# 4. Call the proxy with your API key
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer tsk-your-key-here" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen-turbo",
"messages": [{"role": "user", "content": "Hello!"}]
}'from openai import OpenAI
client = OpenAI(
api_key="tsk-your-key-here",
base_url="http://localhost:8000/v1"
)
response = client.chat.completions.create(
model="qwen-turbo",
messages=[{"role": "user", "content": "你好,世界"}]
)
print(response.choices[0].message.content)# Set .env first, then:
docker compose up -dThis starts:
- app — FastAPI on
:8000 - db — PostgreSQL 16
- redis — Redis 7 (rate limiting)
For dev with SQLite:
docker build -t token-relay .
docker run -p 8000:8000 --env-file .env token-relay| Provider | Models |
|---|---|
| Qwen (通义千问) | qwen-turbo, qwen-plus, qwen-max, qwen-vl-plus |
| DeepSeek | deepseek-chat, deepseek-reasoner |
| Zhipu (智谱) | glm-4-flash, glm-4, glm-4v |
Add more via the Admin Console → Models or via POST /api/admin/models.
- Each model has a cost price (defaults in
proxy_service.py) and an optional sell price (set in Model Config) - If sell price is not set, the system applies
markup_percent(default 15%) on top of cost - Billing is pre-auth: the system estimates the max cost before calling upstream, holds that amount, then refunds the difference after the actual token count comes back
- All amounts are stored in cents (integers) — no floating point precision issues
Request → Estimate (max_tokens × output_price)
→ Atomic balance check + hold
→ Call upstream provider
→ Parse actual token usage
→ Refund delta (estimate - actual)
→ Log transaction + usage
If the user's balance is insufficient, the API returns HTTP 402 Payment Required before any upstream call.
app/
├── main.py # FastAPI entry, lifecycle, middleware
├── config.py # Pydantic Settings from .env
├── database.py # SQLAlchemy 2.0 async engine + session
├── api/
│ ├── deps.py # Auth dependencies (JWT + API Key)
│ ├── auth.py # Register, login, refresh
│ ├── users.py # User profile + API key CRUD
│ ├── admin.py # Admin: users, models, keys, settings, logs
│ ├── billing.py # Balance + transactions
│ ├── logs.py # User-scoped usage logs + stats
│ ├── dashboard_router.py # User portal page routes
│ ├── templates_router.py # Admin console page routes
│ └── v1/ # OpenAI-compatible proxy endpoints
│ ├── chat.py # POST /v1/chat/completions
│ ├── models.py # GET /v1/models
│ ├── embeddings.py # POST /v1/embeddings
│ └── router.py # v1 aggregator
├── models/ # SQLAlchemy ORM models (7 tables)
├── schemas/ # Pydantic request/response schemas
├── services/ # Business logic
│ ├── proxy_service.py # Core billing pipeline + upstream relay
│ ├── auth_service.py # Registration + login
│ ├── user_service.py # Profile + API key management
│ ├── admin_service.py # Admin CRUD operations
│ ├── billing_service.py # Balance + transaction queries
│ └── analytics_service.py # Stats + usage analytics
├── adapters/ # LLM provider adapters
│ ├── base.py # Adapter interface
│ ├── openai_adapter.py # OpenAI-compatible (covers all 3 providers)
│ └── registry.py # Adapter + key registry
├── security/ # JWT, encryption, API key hashing, rate limiter
├── templates/ # Jinja2 templates
│ ├── admin/ # Dark theme admin console
│ └── user/ # Light theme user portal
└── static/ # CSS + JS
├── base.css # Design system (both themes)
├── user.js # User portal interactivity
└── admin.js # Admin console interactivity
7 tables: users, api_keys, provider_keys, model_configs, system_settings, usage_logs, credit_transactions
- Dev: SQLite via
aiosqlite(zero config) - Prod: PostgreSQL via
asyncpg(setDATABASE_URLin.env)
All via .env — see .env.example for all options.
| Key | Description |
|---|---|
SECRET_KEY |
JWT signing secret (min 32 chars) |
ENCRYPTION_KEY |
Fernet key for stored API keys |
DATABASE_URL |
SQLite (default) or PostgreSQL |
REGISTRATION_OPEN |
true / false |
RATE_LIMIT_PER_USER_PER_MINUTE |
Per-user RPM cap |
RATE_LIMIT_GLOBAL_PER_MINUTE |
Global RPM cap |
- Stripe / payment gateway integration
- Multi-language user portal (EN, ZH, TH, VI)
- Webhook notifications for balance threshold
- Per-user rate limiting via admin
- Model playground (interactive chat tester)
- Prometheus metrics endpoint
- RBAC roles beyond user/admin
- Team / organization accounts
MIT — see LICENSE.