API reference
Swing Deck exposes two HTTP surfaces. The cloud API at api.swing-deck.com handles license validation, checkout, sync, and alerts. The local dashboard at http://localhost:8001 serves the audit data, runs the AI coaches, and brokers order placement against E*TRADE / Tradier. Most endpoints are called by the dashboard automatically — this reference is for custom integrations or if you're auditing the behavior.
⬢ Cloud API — api.swing-deck.com
Base URL
Production: https://api.swing-deck.com
Interactive OpenAPI: api.swing-deck.com/docs (FastAPI auto-generated)
Authentication
Three auth patterns exist. Use whichever the endpoint requires:
| Pattern | Header / Body | Used by |
|---|---|---|
| License key | X-License-Key: SWING-XXXX-XXXX-XXXX |
All user-data endpoints (alerts, sync, portal, referrals) |
| None | — | Public health, version check, checkout creation, trial signup |
| Cron secret | Authorization: Bearer <CRON_SECRET> |
Internal: /api/cron/* endpoints |
Endpoints
System
status=ok and a timestamp. Used by uptime monitors and Railway's health check.License
{key: "SWING-XXXX-XXXX-XXXX"} — or pass via X-License-Key header.Returns tier, feature flags, email, expiry. Cached 24h by the local app.
Billing
{plan, email?, referral_code?, success_url?, cancel_url?}.plan: one of pro_monthly, pro_yearly, premium_monthly, premium_yearly.Returns a short-lived Stripe Checkout URL.
X-License-Key. Returns a Stripe Billing Portal URL where the user can update their card, change plan, cancel, and download invoices.
checkout.session.completed, customer.subscription.deleted, invoice.payment_failed, customer.subscription.updated. Verifies signature against STRIPE_WEBHOOK_SECRET.Trial
{email, source?}. No credit card required.Issues a 14-day Pro-tier license. Emails the key. Refuses disposable-email domains and limits to 2 trials per IP per 24h.
Alerts
X-License-Key with alerts_email or alerts_push feature.Body:
{alert_type, ticker?, message, details?, severity}.Dispatches to Resend (email) + ntfy.sh (push). Server-side 4h dedup on
(email, alert_type, ticker).
Cloud sync
X-License-Key with cloud_sync feature.Body:
{blob, meta, passphrase_fp} where blob is base64(nonce || ciphertext || tag) encrypted client-side with AES-256-GCM.Max size: 5 MB. Retention: newest 30 per license.
Referrals
X-License-Key. Returns referral code, share URL, total referred, credits earned, recent events.
v5.0 endpoints
Public endpoints added in v5.0. The consensus + daily-briefing endpoints require a valid license; track-record reads are public.
/api/track-record/api/daily-briefingX-License-Key and alerts_email entitlement. Accepts portfolio summary + state counts + actionable tickers; sends formatted email via Resend. Rate-limited.
/api/consensus/submit{fires: [{ticker,kind,strength,state}], regime}. License key is hashed server-side (SHA-256, first 16 hex chars) — the raw key is never stored. Dedupes per (hash, ticker, primitive, day).
/api/consensus/ticker/{ticker}{sample_users, armed_pct, tighten_pct, exit_pct, top_kinds}. Requires a license. Enforces a k-anonymity floor of 5 users — returns {"sufficient": false} below that so no single user's data leaks.
/api/consensus/toplimit=N (1-50, default 10). Same k-anonymity floor — tickers with <5 users are omitted.
/api/latest-version{version, min_version, download: {mac, win}, release_notes}. Auto-updater polls this every 24h. Below min_version → dashboard shows a hard-upgrade banner.
v6.4 endpoints (NEW · Founding 100 launch)
Public endpoints added with the v6.4 launch. Both are unauthenticated reads — they drive the live counter on /pricing and the tier-aware welcome page after Stripe checkout.
/api/founding-counter{
"claimed": 12,
"total": 100,
"remaining": 88,
"closed": false
}
closed flips to true once claimed >= 100. The counter is computed live from the licenses table (filtered to tier='founding' + status='active') so it reflects cancellations.
/api/checkout-session-info?session_id={SESSION_ID}success_url=...?session_id={CHECKOUT_SESSION_ID}). Returns enough info to render the right hero block:
{
"tier": "founding", // "founding" | "pro" | "premium" | "trial" | "unknown"
"customer_email": "you@example.com",
"slot_number": 12, // founding only — null otherwise
"slot_total": 100, // founding only — null otherwise
"downgraded_from_founding": false // true iff the cap was hit during checkout and pro was issued instead
}
Returns 404 if the session_id doesn't exist in Stripe; 400 if the session is for an unknown price. The license key itself is never returned through this surface — it's delivered exclusively via the welcome email.
Rate limits
In-memory token bucket per (bucket, license_key_or_ip). Resets on deploy. Exceeding the limit returns 429 with a Retry-After header and a JSON body:
{
"detail": {
"error": "rate_limit_exceeded",
"bucket": "alert_dispatch",
"limit": 30,
"window_sec": 3600,
"retry_after": 120,
"message": "Rate limit: 30 per 1h"
}
}
| Endpoint | Limit | Window | Keyed by |
|---|---|---|---|
/api/validate-license | 120 | 1 minute | license key |
/api/alert-dispatch | 30 | 1 hour | license key |
/api/sync/upload | 10 | 24 hours | license key |
/api/create-checkout-session | 10 | 1 hour | IP address |
/api/portal/session | 10 | 1 hour | license key |
Error codes
| Status | Meaning | What to do |
|---|---|---|
400 | Bad request — malformed body or invalid field values | Check the detail field for what's wrong |
401 | Missing / invalid license key (on auth-protected routes) | Re-check X-License-Key header |
403 | Valid license but tier doesn't include this feature | Upgrade, or check features dict from /validate-license |
404 | Resource not found (e.g. snapshot id) | Re-list; the id may have been deleted |
429 | Rate limit exceeded | Back off per Retry-After header |
502 | Upstream provider error (Stripe, Resend, Supabase) | Usually transient — retry after 10s |
503 | Feature not configured on this deployment | Service is live but missing an env var (Stripe key, Admin secret, etc.) |
Example — validating a key from bash
curl -X POST https://api.swing-deck.com/api/validate-license \
-H "Content-Type: application/json" \
-d '{"key": "SWING-XXXX-XXXX-XXXX"}'
# =>
# {
# "valid": true,
# "tier": "pro",
# "features": {"max_tickers": 25, "broker_writes": true, …},
# "email": "you@example.com",
# "expires_at": null,
# "days_remaining": null,
# "message": "Active pro license"
# }
⬢ Local dashboard — http://localhost:8001
The local dashboard's HTTP server is the same one rendering the UI you click. It runs entirely on your machine — no traffic leaves localhost — and exposes endpoints for the audit data, the eight AI coaches, and the broker integration. Most of these are called automatically by the dashboard JS; this reference is for users who want to script against their own dashboard or build custom integrations.
All POST endpoints validate an X-CSRF-Token header against an in-memory token regenerated on every server restart. Fetch the current token from GET /csrf-token first, then pass it on subsequent POSTs. Read endpoints (GET) are not CSRF-gated — they're heartbeats and audit data fetches.
CSRF token
{"token": "<64-hex-chars>"}. Pass this as X-CSRF-Token on every subsequent POST. The token rotates on server restart (and only on restart), so cache it for the duration of your script.
Audit data
X-CSRF-Token. Returns immediately with {"started": true}; poll GET /audit for the new payload (typically ready within 30s for a 25-ticker portfolio). Subject to the dashboard's own rate-limit (1 run per 60s).
AI coaches (8 surfaces)
Each AI coach has the same endpoint shape: a GET that returns a cached narration if one exists, regenerating from the LLM only when the cache key bursts. Pass force=1 to bypass the cache. All eight coaches log to the same ai_thesis_log.jsonl on disk — see the history modal docs for the schema.
For the full input/output spec of each surface (cache keys, output shapes, hallucination guards), see the AI surfaces reference. The endpoints below are the call shape only.
Broker
Order placement endpoints are intentionally not documented for direct curl use. They run through an 8-layer pre-flight (audit-freshness, confirm-token, position-size cap, risk-budget cap, options-level enforcement, halt check, dry-run window, idempotency keys), and bypassing the dashboard UI defeats those guards. Use the dashboard's order modals instead. The endpoints below are the broker-state surface, which is safe to script against.
{open: bool, cooldown_remaining: int, last_reason: str, fails: int}. The breaker trips after 3 consecutive 5xx responses from the upstream broker and locks for BROKER_CIRCUIT_COOLDOWN seconds (default 1800).
X-CSRF-Token. Logs a circuit_reset event with source: manual_api. Useful when an upstream maintenance window cleared and you don't want to wait the full cooldown.
portfolio.txt), and local-only positions (in portfolio.txt but not at the broker). Read-only — surfaces the diff for human resolution.
Mode toggle
{mode: "live"|"dryrun"}. Dryrun short-circuits all place and cancel calls so they log the intent but never hit the broker; preview still runs against real prod for full validation.
Example — call a coach from bash
# 1. Fetch the CSRF token (only needed if you'll POST later;
# /coach/* is GET so this step is skipped for read-only).
TOKEN=$(curl -s http://localhost:8001/csrf-token | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
# 2. Generate the Trap & Structure narration for AAPL.
curl -s "http://localhost:8001/coach/trap_structure/generate?ticker=AAPL" | python3 -m json.tool
# =>
# {
# "ticker": "AAPL",
# "state": "FADE",
# "verdict": "WAIT",
# "reason": "Price tagged primary resistance at $269.77 with thin tape lean...",
# "structure": { "primary_resistance": 269.77, "primary_support": 261.50, ... },
# "exhaustion": { "rsi": 63, "momentum": "rolling_down" },
# "tape": { "lean_at_resistance": "thin" },
# "better_entries": [{"price": 261.50, "reason": "support"}],
# "cached": false
# }