Skip to content

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.

MethodBest forDelivers
Password + JWTBrowser, interactive7-day JWT
Wallet (EIP-191)On-chain subscription holdersJWT with on-chain tier synced
API keyPython / server-sideLong-lived mk_live_ token for WebSocket

Method 1 — Password + JWT

Register

POST /api/auth/register
Content-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/login
Content-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/me
Authorization: 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-token
Authorization: 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 /nonce again for the same address overwrites the previous nonce.

Step 2 — Sign and submit

Sign the literal string (including the \n):

Sign in to mackinac
Nonce: <nonce>

Using EIP-191 personal_sign:

from eth_account import Account
from 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/wallet
Content-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.


API keys are long-lived tokens for unattended / server-side access. Requires tier api.

Issue a key

POST /api/apikeys
Authorization: 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 key
Authorization: Bearer eyJ...

Subscription status

GET /api/subscription/status
Authorization: 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 httpx
from eth_account import Account
from 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)