Authentication
Three ways to authenticate. For server-side / scripted use, API keys are recommended. Browser sessions use JWT. Wallet sign-in is for users whose subscription is tied to an on-chain wallet.
Authentication is optional for public data: an anonymous WebSocket connection works at the free-tier symbol cap (3 concurrent). Authenticate to unlock higher tiers.
| Method | Best for | Delivers |
|---|---|---|
| Password + JWT | Browser, interactive | 7-day JWT |
| Wallet (EIP-191) | On-chain subscription holders | JWT with on-chain tier synced |
| API key | Python / server-side | Long-lived mk_live_ token for WebSocket |
Method 1 — Password + JWT
Register
POST /api/auth/registerContent-Type: application/json
{ "username": "alice", "password": "correct-horse-battery-staple" }Constraints: username 3–32 chars (unique), password ≥ 8 chars.
Response 200:
{ "userId": 42, "username": "alice", "token": "eyJhbGciOi..." }Errors: 409 username_taken, 400 validation_error. Rate limit: 5/min per IP.
Login
POST /api/auth/loginContent-Type: application/json
{ "username": "alice", "password": "..." }Same response shape as register. Error: 401 invalid_credentials. Rate limit: 10/min per IP.
Use the JWT
Authorization: Bearer eyJhbGciOi...JWTs are valid for 7 days. Claims:
{ userId: number, username: string, tier: 'none' | 'api', role: 'trader' | 'super_admin' | ..., subscriptionExpiry: number, // epoch ms; 0 if no subscription exp: number // standard JWT expiry}Treat the JWT as opaque — use GET /api/auth/me to read current effective state rather than decoding it client-side.
Verify / inspect
GET /api/auth/meAuthorization: Bearer eyJ...{ "userId": 42, "username": "alice", "role": "trader", "tier": "api", "subscriptionExpiry": 1754000000000 }Returns 401 if the JWT is missing, malformed, or expired.
Refresh tier after on-chain purchase
POST /api/subscription/refresh-tokenAuthorization: Bearer eyJ...Mints a new 7-day JWT with current on-chain tier claims. Call this after a subscription purchase so the new tier takes effect without waiting for the old JWT to expire.
{ "token": "eyJhbGciOi..." }Method 2 — Wallet (EIP-191)
Two-step protocol for users with an on-chain subscription.
Step 1 — Request a nonce
GET /api/auth/nonce?address=0xabcd...{ "nonce": "f3a2c1...32hex..." }- Nonce TTL: 5 minutes, single-use.
- Calling
/nonceagain for the same address overwrites the previous nonce.
Step 2 — Sign and submit
Sign the literal string (including the \n):
Sign in to mackinacNonce: <nonce>Using EIP-191 personal_sign:
from eth_account import Accountfrom eth_account.messages import encode_defunct
msg = encode_defunct(text=f"Sign in to mackinac\nNonce: {nonce}")signed = Account.sign_message(msg, private_key=PRIVATE_KEY)signature = signed.signature.hex()Then submit:
POST /api/auth/walletContent-Type: application/json
{ "address": "0xabcd...", "signature": "0x..." }Response 200:
{ "userId": 17, "username": "0xabcd1234", "token": "eyJhbGciOi..." }The tier in the returned JWT is synced from the wallet’s on-chain subscription state at sign-in time.
Errors: 401 invalid_signature, 401 nonce_expired, 400 validation_error. Rate limit: 20/min per IP.
Method 3 — API key (recommended for Python clients)
API keys are long-lived tokens for unattended / server-side access. Requires tier api.
Issue a key
POST /api/apikeysAuthorization: Bearer eyJ...Content-Type: application/json
{ "label": "production server #1" }Response 200:
{ "id": 37, "key": "mk_live_0123456789abcdef0123456789abcdef", "label": "production server #1", "createdAt": "2026-05-26T12:00:00Z"}Key format: mk_live_ + 32 hex chars = 40 chars total.
Use the key (WebSocket)
Send as the first message after connecting:
{ "action": "auth", "key": "mk_live_..." }Server responds:
{ "type": "authed", "tier": "api", "symbolLimit": 100 }After authed, subscribe normally.
Use the key (REST)
REST endpoints currently require a JWT bearer token; API keys are WebSocket-only. For Python clients that need both REST and WebSocket, obtain a JWT via the wallet or password flow for REST calls and use the API key for WebSocket.
List / revoke keys
GET /api/apikeys # list your keys (metadata only, no secret material)DELETE /api/apikeys/{id} # revoke a keyAuthorization: Bearer eyJ...Subscription status
GET /api/subscription/statusAuthorization: Bearer eyJ...{ "tier": "api", "expiresAt": "2026-08-01T00:00:00Z", "active": true }This endpoint re-reads the on-chain contract on each call, so it reflects the live state.
Python quick-start (API key)
import os, asyncio, json, websockets
KEY = os.environ["MACKINAC_API_KEY"]URL = "wss://api.mackinac.io/feed"
async def main(): async with websockets.connect(URL) as ws: await ws.send(json.dumps({"action": "auth", "key": KEY})) authed = json.loads(await ws.recv()) assert authed["type"] == "authed", authed print("tier:", authed["tier"])
await ws.send(json.dumps({"action": "subscribe", "exchange": "hl", "symbol": "ETH"})) async for raw in ws: print(json.loads(raw))
asyncio.run(main())Python quick-start (wallet sign-in)
import httpxfrom eth_account import Accountfrom eth_account.messages import encode_defunct
base = "https://api.mackinac.io"addr = "0xabcd...1234"
nonce = httpx.get(f"{base}/api/auth/nonce", params={"address": addr}).json()["nonce"]msg = encode_defunct(text=f"Sign in to mackinac\nNonce: {nonce}")sig = Account.sign_message(msg, private_key=PRIVATE_KEY).signature.hex()jwt = httpx.post(f"{base}/api/auth/wallet", json={"address": addr, "signature": "0x" + sig}).json()["token"]
me = httpx.get(f"{base}/api/auth/me", headers={"Authorization": f"Bearer {jwt}"}).json()print(me)