526 lines
19 KiB
Markdown
526 lines
19 KiB
Markdown
# Service Architecture — Gateway Checker API
|
|
|
|
> Living document. Updated as we discuss and build. Read this before starting any work.
|
|
|
|
## Overview
|
|
|
|
REST API that wraps gateway checker scripts behind authenticated endpoints. Clients get an API key and hit endpoints — they never see source code. This is a **black-box service** — clients only know the API contract, not how it works internally.
|
|
|
|
### How the service works (the big picture)
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ YOUR SERVER │
|
|
│ │
|
|
│ Client A ──►┐ │
|
|
│ Client B ──►┤ FastAPI (api.py) │
|
|
│ Client C ──►┤ │ │
|
|
│ Tg Bot ──►┘ ├── Auth middleware (validate API key) │
|
|
│ ├── Rate limiter (per-key, per-minute) │
|
|
│ ├── Task queue (asyncio.Queue) │
|
|
│ │ │ │
|
|
│ │ ▼ │
|
|
│ ├── Gateway Registry │
|
|
│ │ ├── comwave_forbot.py [max_concurrent=1]│
|
|
│ │ ├── comwave_forbot.py [max_concurrent=5]│
|
|
│ │ └── future_forbot.py [max_concurrent=N]│
|
|
│ │ │
|
|
│ ├── Dedup cache (same card+gateway = cached) │
|
|
│ ├── Request logger (SQLite) │
|
|
│ └── DB (api.db) — keys, usage, logs │
|
|
│ │
|
|
│ Admin (you) ──► /admin/* endpoints (master key) │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### The flow (step by step)
|
|
|
|
1. **Client sends request:** `POST /api/check/comwave` with API key + card
|
|
2. **Auth middleware:** Validates key exists, is active, not expired, not over limit
|
|
3. **Gateway access check:** Is this key allowed to use `comwave`?
|
|
4. **Rate limit check:** Has this key exceeded N requests/minute?
|
|
5. **Dedup check:** Same card + same gateway in last 60s? Return cached result
|
|
6. **Task created:** Card is queued, client gets back `task_id` immediately
|
|
7. **Worker picks it up:** Respects gateway's `max_concurrent` semaphore
|
|
8. **Gateway runs:** `comwave_forbot.py` does the actual check (5-15s)
|
|
9. **Result stored:** In-memory results dict + logged to SQLite
|
|
10. **Client polls:** `GET /api/result/{task_id}` → gets result when ready
|
|
|
|
---
|
|
|
|
## Stack
|
|
|
|
| Component | Choice | Why |
|
|
|---|---|---|
|
|
| Framework | **FastAPI** | Async native, auto-docs, fast |
|
|
| DB | **SQLite** (via `aiosqlite`) | Zero setup, file-based, good for this scale |
|
|
| Auth | API key in `X-API-Key` header | Simple, standard |
|
|
| Server | **uvicorn** | ASGI server for FastAPI |
|
|
| Config | **python-dotenv** | Already installed, clean `.env` loading |
|
|
| Keys | `secrets.token_urlsafe(32)` | 43-char random, unguessable |
|
|
| Task queue | **asyncio.Queue** | No Redis needed, in-process |
|
|
| Concurrency | **asyncio.Semaphore** per gateway | Controls parallel load |
|
|
|
|
**Packages to install:** `fastapi`, `uvicorn`, `aiosqlite`
|
|
|
|
---
|
|
|
|
## Files
|
|
|
|
| File | Purpose | Status |
|
|
|---|---|---|
|
|
| `api.py` | FastAPI server, gateway registry, auth, task queue, workers | ⬜ Not started |
|
|
| `db.py` | SQLite key management + request logging | ⬜ Not started |
|
|
| `comwave_forbot.py` | Comwave checker engine (existing, unchanged) | ✅ Done |
|
|
| `comwave_bot.py` | Telegram bot (existing, will update to call API later) | ✅ Done |
|
|
| `api.db` | SQLite database (auto-created at runtime) | Auto |
|
|
| `.env` | `MASTER_KEY`, `BOT_TOKEN`, `API_HOST`, `API_PORT`, credentials | ✅ Exists (needs `MASTER_KEY`) |
|
|
| `SERVICE_ARCHITECTURE.md` | This file | ✅ |
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
### Public (no auth)
|
|
|
|
| Method | Endpoint | Description |
|
|
|---|---|---|
|
|
| `GET` | `/health` | Health check — `{"status": "ok", "uptime": 3600}` |
|
|
|
|
### Client Endpoints (require API key)
|
|
|
|
| Method | Endpoint | Body | Description |
|
|
|---|---|---|---|
|
|
| `POST` | `/api/check/{gateway}` | `{"card": "cc\|mm\|yyyy\|cvv"}` | Submit card for checking, returns `task_id` |
|
|
| `GET` | `/api/result/{task_id}` | — | Poll for result (queued/processing/completed) |
|
|
| `GET` | `/api/usage` | — | Check own usage/limits/expiry |
|
|
| `GET` | `/api/gateways` | — | List available gateways + status |
|
|
| `GET` | `/api/cooldown` | — | Check own cooldown timers per gateway |
|
|
|
|
### Admin Endpoints (require master key)
|
|
|
|
| Method | Endpoint | Body | Description |
|
|
|---|---|---|---|
|
|
| `POST` | `/admin/keys` | `{"owner", "gateways", "request_limit", "expires_days", "rate_per_minute"}` | Create new API key |
|
|
| `GET` | `/admin/keys` | — | List all keys with usage |
|
|
| `GET` | `/admin/keys/{key}` | — | Get single key details |
|
|
| `PATCH` | `/admin/keys/{key}` | fields to update | Update key limits/expiry/gateways |
|
|
| `DELETE` | `/admin/keys/{key}` | — | Revoke a key |
|
|
| `GET` | `/admin/stats` | — | Live server stats (queues, 24h counts, gateway health) |
|
|
|
|
---
|
|
|
|
## Gateway Registry
|
|
|
|
Each gateway declares its function, concurrency limit, timeout, and optional lifecycle hooks:
|
|
|
|
```python
|
|
GATEWAYS = {
|
|
"comwave": {
|
|
"fn": checker.check_card, # the async check function
|
|
"type": "$0 auth", # display label
|
|
"status": "online", # online/maintenance/offline
|
|
"max_concurrent": 1, # uses shared session, serialize
|
|
"timeout": 30, # seconds before giving up
|
|
"cooldown": 20, # per-user cooldown in seconds
|
|
"max_queue": 50, # max tasks waiting in queue
|
|
"init": checker.ensure_logged_in, # called on API startup
|
|
"shutdown": checker.shutdown, # called on API shutdown
|
|
},
|
|
"comwave3": {
|
|
"fn": check_card_3,
|
|
"type": "$3.33 charge",
|
|
"status": "online",
|
|
"max_concurrent": 5, # stateless, can run in parallel
|
|
"timeout": 60,
|
|
"cooldown": 20, # per-user cooldown in seconds
|
|
"max_queue": 50, # max tasks waiting in queue
|
|
"init": None,
|
|
"shutdown": None,
|
|
},
|
|
# Adding a new gateway:
|
|
# "duke": {
|
|
# "fn": duke_check,
|
|
# "type": "$1.00 charge",
|
|
# "status": "online",
|
|
# "max_concurrent": 3,
|
|
# "timeout": 45,
|
|
# "cooldown": 20,
|
|
# "max_queue": 50,
|
|
# "init": None,
|
|
# "shutdown": None,
|
|
# },
|
|
}
|
|
```
|
|
|
|
**Adding a new gateway:**
|
|
1. Code `new_site_forbot.py` with `async def check_card(card: str) -> dict` function
|
|
2. Import and register in `GATEWAYS` dict in `api.py`
|
|
3. Create/update client keys to include the new gateway name
|
|
4. Done — client uses `POST /api/check/newsite`
|
|
|
|
**Gateway function contract:**
|
|
```python
|
|
async def check_card(card: str) -> dict:
|
|
# card = "cc|mm|yyyy|cvv"
|
|
# Must return: {"status": "approved"|"declined"|"error", "message": "...", "time": float}
|
|
```
|
|
|
|
**Cooldown:**
|
|
- Each gateway has a `cooldown` value (seconds) — **per-user**, not global
|
|
- After a user submits a card to a gateway, they must wait `cooldown` seconds before submitting another to the **same** gateway
|
|
- Different users are independent — User A's cooldown doesn't block User B
|
|
- Different gateways are independent — cooldown on `comwave` doesn't affect `comwave3`
|
|
- Tracked via in-memory dict: `last_request[api_key][gateway] → timestamp`
|
|
- If a user submits too soon: `429 {"detail": "Cooldown active", "retry_after": 12.5}` (remaining seconds)
|
|
- Trade-off accepted: site load scales with number of active users (10 users = up to 10 concurrent requests), but users never wait for each other
|
|
|
|
**Concurrency control:**
|
|
- Each gateway gets its own `asyncio.Semaphore(max_concurrent)`
|
|
- `comwave` max_concurrent=1 → only 1 check at a time (shared session with lock)
|
|
- `comwave3` max_concurrent=5 → up to 5 parallel checks (stateless)
|
|
- Excess requests wait in queue, not rejected
|
|
|
|
**Lifecycle hooks:**
|
|
- `init()` — called once when API server starts (e.g. comwave login)
|
|
- `shutdown()` — called once when API server stops (e.g. comwave logout)
|
|
- Both are optional (`None` for stateless gateways)
|
|
|
|
---
|
|
|
|
## Task Queue System
|
|
|
|
Instead of making clients wait 5-15s for a response, we use async tasks:
|
|
|
|
### Submit flow
|
|
```
|
|
POST /api/check/comwave {"card": "4111...|12|2025|123"}
|
|
↓
|
|
Returns immediately:
|
|
{"task_id": "abc123", "status": "queued", "gateway": "comwave", "position": 2, "estimated_wait": 15}
|
|
```
|
|
|
|
### Poll flow
|
|
```
|
|
GET /api/result/abc123
|
|
↓
|
|
While queued: {"task_id": "abc123", "status": "queued", "position": 2, "expires_in": 290}
|
|
While processing: {"task_id": "abc123", "status": "processing", "expires_in": 270}
|
|
When done: {"task_id": "abc123", "status": "completed", "result": {...}, "remaining": 999}
|
|
```
|
|
|
|
### Implementation
|
|
- `asyncio.Queue` per gateway — tasks are queued and processed by workers
|
|
- Workers respect `max_concurrent` semaphore
|
|
- Results stored in `dict[task_id] → result` (in-memory, TTL 5 minutes)
|
|
- `task_id` = `uuid4()` short string
|
|
|
|
### Why this is better than synchronous
|
|
- No HTTP timeout issues (client gets response in <100ms)
|
|
- Client can submit multiple cards and poll all results
|
|
- Server controls processing speed via semaphores
|
|
- Batch support becomes trivial (submit N tasks, poll N results)
|
|
|
|
### Task ownership
|
|
- Each task is tied to the API key that created it
|
|
- Only the creating key can poll `GET /api/result/{task_id}`
|
|
- Other keys get `404 "Task not found"` — prevents cross-client result snooping
|
|
- Stored as `tasks[task_id] = {"api_key": key, "result": ..., ...}`
|
|
|
|
### Max queue depth
|
|
- Each gateway has a max queue size (default: 50 tasks)
|
|
- If queue is full: `503 {"detail": "Gateway overloaded", "queue_depth": 50}`
|
|
- Prevents a single client from flooding the queue during cooldown edge cases
|
|
- Configurable per gateway via `max_queue` field in registry
|
|
|
|
---
|
|
|
|
## Throttling Strategy
|
|
|
|
**Model: Per-user cooldown (20s default)**
|
|
|
|
Each user (API key) has an independent cooldown timer per gateway. After submitting a card, that user must wait `cooldown` seconds before submitting another card to the same gateway.
|
|
|
|
### How it works
|
|
```
|
|
User A submits card → comwave processes → User A must wait 20s
|
|
User B submits card → comwave processes → User B must wait 20s (independent of A)
|
|
User A submits to comwave3 → allowed immediately (different gateway)
|
|
```
|
|
|
|
### Implementation
|
|
```python
|
|
# In-memory tracker
|
|
last_request: dict[str, dict[str, float]] = {} # {api_key: {gateway: timestamp}}
|
|
|
|
# Before queuing a task:
|
|
now = time.time()
|
|
last = last_request.get(api_key, {}).get(gateway, 0)
|
|
remaining = cooldown - (now - last)
|
|
if remaining > 0:
|
|
raise HTTPException(429, {"detail": "Cooldown active", "retry_after": round(remaining, 1)})
|
|
last_request.setdefault(api_key, {})[gateway] = now
|
|
```
|
|
|
|
### Behavior with multiple users
|
|
| Time | User A | User B | User C | Site load |
|
|
|------|--------|--------|--------|-----------|
|
|
| 0s | submits card | submits card | — | 2 concurrent |
|
|
| 5s | waiting (15s left) | waiting (15s left) | submits card | 1 concurrent |
|
|
| 20s | submits next | submits next | waiting (5s left) | 2 concurrent |
|
|
| 25s | waiting | waiting | submits next | 1 concurrent |
|
|
|
|
**Max site load = number of active users** (each can send 1 request per 20s)
|
|
|
|
---
|
|
|
|
## Request Deduplication
|
|
|
|
If a client sends the **same card** to the **same gateway** within **60 seconds**, return the cached result instead of hitting the gateway again.
|
|
|
|
- **Key:** `hash(card + gateway)` (SHA256, not stored in plain text)
|
|
- **Cache:** In-memory dict with TTL
|
|
- **Why:** Prevents accidental double-charges on `comwave3`, saves gateway resources
|
|
|
|
---
|
|
|
|
## Database Schema (SQLite)
|
|
|
|
### `api_keys` table
|
|
|
|
| Column | Type | Description |
|
|
|---|---|---|
|
|
| `api_key` | TEXT PK | The key string (`sk_live_...`) |
|
|
| `owner` | TEXT | Client name/identifier |
|
|
| `created_at` | DATETIME | When key was created |
|
|
| `expires_at` | DATETIME NULL | NULL = never expires |
|
|
| `request_limit` | INTEGER NULL | NULL = unlimited |
|
|
| `requests_used` | INTEGER | Counter, starts at 0 |
|
|
| `rate_per_minute` | INTEGER | Max requests per minute (default 10) |
|
|
| `allowed_gateways` | TEXT | JSON list `["comwave","comwave3"]` or `"*"` for all |
|
|
| `is_active` | BOOLEAN | Can be deactivated without deleting |
|
|
| `is_paused` | BOOLEAN | Temporarily disabled (client sees "API key paused") |
|
|
|
|
### `request_log` table
|
|
|
|
| Column | Type | Description |
|
|
|---|---|---|
|
|
| `id` | INTEGER PK | Auto-increment |
|
|
| `api_key` | TEXT | Which key was used |
|
|
| `gateway` | TEXT | Which gateway |
|
|
| `card` | TEXT | Full card (`cc|mm|yyyy|cvv`) |
|
|
| `status` | TEXT | approved/declined/error |
|
|
| `response_time` | REAL | Seconds |
|
|
| `created_at` | DATETIME | Timestamp |
|
|
| `ip_address` | TEXT | Client IP |
|
|
|
|
### `admin_log` table
|
|
|
|
| Column | Type | Description |
|
|
|---|---|---|
|
|
| `id` | INTEGER PK | Auto-increment |
|
|
| `action` | TEXT | create_key / revoke_key / update_key / pause_key / unpause_key |
|
|
| `target_key` | TEXT | Which API key was affected |
|
|
| `details` | TEXT | JSON of what changed (e.g. `{"request_limit": [1000, 2000]}`) |
|
|
| `ip_address` | TEXT | Admin's IP |
|
|
| `created_at` | DATETIME | Timestamp |
|
|
|
|
---
|
|
|
|
## Security
|
|
|
|
| Layer | Implementation |
|
|
|---|---|
|
|
| **Auth** | `X-API-Key` header required on all client/admin endpoints |
|
|
| **Key format** | `sk_live_` + `secrets.token_urlsafe(32)` |
|
|
| **No docs in prod** | `FastAPI(docs_url=None, redoc_url=None)` |
|
|
| **HTTPS** | Behind nginx with SSL or Cloudflare tunnel |
|
|
| **Rate limit** | Per-minute limit per API key (in-memory sliding window) |
|
|
| **IP block** | Block IP after 10 failed auth attempts (in-memory, resets after 15min) |
|
|
| **Full card logging** | Full card stored in request_log (keep `api.db` protected) |
|
|
| **Dedup hashing** | Card+gateway hashed with SHA256, never stored raw |
|
|
| **Master key** | From `.env`, used for `/admin/*` only |
|
|
| **Health endpoint** | `/health` is the only unauthenticated endpoint, reveals nothing sensitive |
|
|
| **Graceful shutdown** | Finish in-progress checks before exit, call gateway shutdown hooks |
|
|
|
|
---
|
|
|
|
## Response Format
|
|
|
|
### Submit check
|
|
```json
|
|
{
|
|
"task_id": "a1b2c3d4",
|
|
"status": "queued",
|
|
"gateway": "comwave",
|
|
"position": 2,
|
|
"estimated_wait": 15
|
|
}
|
|
```
|
|
|
|
### Poll result (queued)
|
|
```json
|
|
{
|
|
"task_id": "a1b2c3d4",
|
|
"status": "queued",
|
|
"position": 2,
|
|
"expires_in": 290
|
|
}
|
|
```
|
|
|
|
### Poll result (processing)
|
|
```json
|
|
{
|
|
"task_id": "a1b2c3d4",
|
|
"status": "processing",
|
|
"expires_in": 270
|
|
}
|
|
```
|
|
|
|
### Poll result (completed)
|
|
```json
|
|
{
|
|
"task_id": "a1b2c3d4",
|
|
"status": "completed",
|
|
"result": {
|
|
"status": "declined",
|
|
"gateway": "comwave",
|
|
"message": "DECLINED [VISA] | Last4: 1111 | Auth: 000000 | Ref: 662321650018600120",
|
|
"time": 5.28
|
|
},
|
|
"remaining": 999
|
|
}
|
|
```
|
|
|
|
### Usage
|
|
```json
|
|
{
|
|
"owner": "client_john",
|
|
"requests_used": 347,
|
|
"requests_limit": 1000,
|
|
"expires_at": "2026-04-24T00:00:00",
|
|
"gateways": ["comwave", "comwave3"],
|
|
"rate_per_minute": 10
|
|
}
|
|
```
|
|
|
|
### Gateways
|
|
```json
|
|
{
|
|
"gateways": {
|
|
"comwave": {"status": "online", "type": "$0 auth"},
|
|
"comwave3": {"status": "online", "type": "$3.33 charge"}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Errors
|
|
```json
|
|
{"detail": "Missing API key"} // 401
|
|
{"detail": "Invalid API key"} // 401
|
|
{"detail": "API key expired"} // 401
|
|
{"detail": "API key deactivated"} // 401
|
|
{"detail": "Request limit exceeded", "used": 1000, "limit": 1000} // 429
|
|
{"detail": "Rate limit exceeded", "retry_after": 6} // 429
|
|
{"detail": "Cooldown active", "retry_after": 12.5} // 429
|
|
{"detail": "Access denied for gateway: duke"} // 403
|
|
{"detail": "Gateway not found: xyz"} // 404
|
|
{"detail": "Gateway offline: duke"} // 503
|
|
{"detail": "Gateway overloaded", "queue_depth": 50} // 503
|
|
{"detail": "API key paused"} // 401
|
|
{"detail": "Invalid card format"} // 400
|
|
{"detail": "Task not found"} // 404
|
|
{"detail": "IP blocked"} // 403
|
|
```
|
|
|
|
---
|
|
|
|
## Key Tiers (pricing model)
|
|
|
|
| Tier | Limit | Expiry | Gateways | Rate |
|
|
|---|---|---|---|---|
|
|
| Trial | 50 requests | 7 days | 1 gateway | 5/min |
|
|
| Standard | 1000/month | 30 days | specific | 10/min |
|
|
| Unlimited | no limit | no expiry | all | 50/min |
|
|
|
|
---
|
|
|
|
## Telegram Bot Integration (future)
|
|
|
|
**Option A (recommended):** Bot calls own API with its own key — cleanest separation, bot is just another API consumer. The bot gets an "unlimited" key with all gateways.
|
|
|
|
**Option B:** Bot imports checkers directly (current setup) — faster, no network hop but tightly coupled.
|
|
|
|
Decision: TBD — build API first, connect bot later.
|
|
|
|
---
|
|
|
|
## .env Structure
|
|
|
|
```env
|
|
# API Server
|
|
MASTER_KEY=your_admin_secret_here
|
|
API_HOST=0.0.0.0
|
|
API_PORT=8000
|
|
|
|
# Telegram Bot
|
|
BOT_TOKEN=your_bot_token
|
|
ALLOWED_USERS=123456,789012
|
|
|
|
# Gateway Credentials
|
|
COMWAVE_USERNAME=tu76502@gmail.com
|
|
COMWAVE_PASSWORD=root13579
|
|
```
|
|
|
|
---
|
|
|
|
## Build Order
|
|
|
|
### Phase 1: Core API (v1)
|
|
1. ⬜ Install `fastapi`, `uvicorn`, `aiosqlite`
|
|
2. ⬜ Build `db.py` — SQLite init, key CRUD, usage tracking, request logging
|
|
3. ⬜ Build `api.py` — FastAPI app, auth middleware, gateway registry, task queue + workers, all endpoints
|
|
4. ⬜ Test with comwave gateway — submit, poll, verify result
|
|
5. ⬜ Test admin endpoints — create key, list, revoke
|
|
6. ⬜ Test security — expired key, over-limit, wrong gateway, IP blocking
|
|
7. ⬜ Test concurrency — multiple clients hitting same gateway
|
|
|
|
### Phase 2: Bot + Polish
|
|
8. ⬜ Connect Telegram bot via API (Option A)
|
|
9. ⬜ Add more gateways as clients request them
|
|
|
|
### Phase 3: Future (v2)
|
|
10. ⬜ Webhook/callback support for async results
|
|
11. ⬜ Batch endpoint (`POST /api/check/batch/{gateway}`)
|
|
12. ⬜ IP whitelisting per API key
|
|
13. ⬜ Key rotation
|
|
14. ⬜ Read-only key scoping (usage-only vs full-access)
|
|
15. ⬜ Client dashboard (simple HTML)
|
|
16. ⬜ Gateway health monitoring (auto-disable on failure)
|
|
17. ⬜ Cloudflare Tunnel for easy HTTPS
|
|
|
|
---
|
|
|
|
## Existing Gateways Reference
|
|
|
|
### Comwave $0 auth (`comwave`)
|
|
- **File:** `comwave_forbot.py` → `ComwaveChecker.check_card()`
|
|
- **Type:** Account portal card update, Moneris Checkout v1
|
|
- **Amount:** $0 (auth only)
|
|
- **Session:** Single persistent login, `asyncio.Lock` for ticket serialization
|
|
- **Concurrency:** max_concurrent=1 (shared session)
|
|
- **Lifecycle:** init = `ensure_logged_in()`, shutdown = `shutdown()`
|
|
- **Note:** Server enforces one login at a time
|
|
|
|
### Comwave $3.33 charge (`comwave3`)
|
|
- **File:** `comwave_forbot.py` → `check_card_3()`
|
|
- **Type:** WooCommerce guest checkout, Moneris Checkout v1
|
|
- **Amount:** $3.33 CAD ($2.95 + 12% tax)
|
|
- **Session:** Stateless, each call gets own session
|
|
- **Concurrency:** max_concurrent=5 (fully parallel-safe)
|
|
- **Lifecycle:** no init/shutdown needed
|
|
|
|
---
|
|
|
|
*Last updated: 2026-03-24*
|