This document is the canonical integration guide for the PreMarketML iOS app. All APIs, schemas, and behaviors described here are stable and production-ready.
https://api.yourdomain.com
(Behind Cloudflare Tunnel – no port number)
http://localhost:8000
- Swagger:
/docs - ReDoc:
/redoc
You may use either JWT tokens or API keys (not both).
POST /auth/token
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}Response:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "bearer",
"expires_in": 86400,
"user": {
"id": 1,
"email": "user@example.com",
"tier": "free"
}
}Use header:
Authorization: Bearer <access_token>
POST /auth/refresh
{ "refresh_token": "eyJ..." }Create key (requires JWT auth):
POST /auth/api-key
{ "name": "iOS App Key" }Response (shown once):
{
"id": 1,
"token": "pmml_abc123...",
"name": "iOS App Key",
"created_at": "2024-01-15T10:00:00Z",
"expires_at": "2025-01-15T10:00:00Z",
"message": "Save this token - it won't be shown again"
}Use header:
X-API-Key: pmml_abc123...
pending → processing → completed | failed
No other values are used by the API.
GET /credits/balance
Authorization: Bearer <token>
Response:
{ "balance": 50, "user_id": 1 }| Operation | Credits | Description |
|---|---|---|
predict_quick |
2 | Quick LSTM prediction (5-day forecast) |
predict_full |
5 | Full prediction with multiple models |
agent_quick |
3 | Quick AI market analysis |
agent_debate |
10 | Full bull vs bear debate with transcript |
POST /credits/purchase
Authorization: Bearer <token>
{ "package_id": "pro" }Packages:
| ID | Name | Credits | Price |
|---|---|---|---|
starter |
Starter Pack | 50 | $4.99 |
pro |
Pro Pack | 200 | $14.99 |
power |
Power Pack | 500 | $29.99 |
enterprise |
Enterprise Pack | 2000 | $99.99 |
Response:
{
"checkout_url": "https://checkout.stripe.com/...",
"session_id": "cs_..."
}Open checkout_url in Safari or WKWebView.
POST /agent/analyze
Authorization: Bearer <token>
Content-Type: application/json
{
"symbol": "NVDA",
"mode": "quick",
"skip_cache": false
}- quick: cached 12 hours
- debate: cached 48 hours
- Cache hits cost 0 credits
- In-flight deduplication enabled (same analysis running = reuse job, 0 credits)
{
"status": "completed",
"symbol": "NVDA",
"mode": "quick",
"result": { ... },
"from_cache": true,
"cached_at": "2024-01-15T10:00:00Z",
"hit_count": 5,
"cost_credits": 0
}{
"job_id": "job_xyz789",
"status": "pending",
"symbol": "NVDA",
"mode": "quick",
"cost_credits": 3,
"poll_url": "/job/job_xyz789",
"estimated_seconds": 120
}POST /predict
Authorization: Bearer <token>
Content-Type: application/json
{
"symbol": "AAPL",
"mode": "quick",
"forecast_days": 5,
"model_type": "lstm"
}Parameters:
symbol: Stock ticker (e.g., "AAPL", "NVDA")mode:"quick"(2 credits) or"full"(5 credits)forecast_days: 1-30 daysmodel_type:"lstm","gru","xgboost","random_forest"
Response:
{
"job_id": "job_abc123",
"status": "pending",
"symbol": "AAPL",
"mode": "quick",
"cost_credits": 2,
"poll_url": "/job/job_abc123",
"estimated_seconds": 60
}GET /job/{job_id}
Authorization: Bearer <token>
Completed Response:
{
"job_id": "job_xyz789",
"status": "completed",
"symbol": "NVDA",
"job_type": "agent_analyze",
"mode": "quick",
"cost_credits": 3,
"created_at": "2024-01-15T10:00:00Z",
"completed_at": "2024-01-15T10:02:00Z",
"result": { ... },
"error_message": null
}GET /jobs?limit=20
Authorization: Bearer <token>
All results use schema_version: 1 for backward compatibility.
{
"schema_version": 1,
"ticker": "NVDA",
"mode": "quick",
"engine": "multi_agent_orchestrator",
"as_of": null,
"generated_at": "2024-01-15T10:02:00Z",
"action": "buy",
"summary": "NVIDIA shows strong momentum...",
"confidence": 0.75,
"bull_case": [
"AI chip demand remains strong",
"Data center revenue growing 200% YoY"
],
"bear_case": [
"Valuation stretched at 60x forward PE",
"China export restrictions tightening"
],
"risk_notes": [
"High concentration in AI chips"
],
"what_to_verify": [
"Q4 data center revenue guidance"
],
"sources": [],
"transcript": []
}Field Notes:
action: One of"buy","sell","hold","strong_buy","strong_sell"confidence: Float 0.0-1.0 ornulltranscript: Populated only for"debate"mode (up to 50 turns)- All arrays are capped at 10 items
{
"schema_version": 1,
"ticker": "AAPL",
"mode": "quick",
"engine": "lstm",
"generated_at": "2024-01-15T10:00:00Z",
"current_price": 185.50,
"predictions": [186.20, 187.10, 186.80, 188.50, 189.20],
"forecast_days": 5,
"model_type": "lstm",
"model_id": null,
"direction": "up",
"price_change_pct": 1.99,
"training_time_seconds": 45.2,
"data_points": 252
}Field Notes:
direction: One of"up","down","neutral"predictions: Array of floats (predicted prices)price_change_pct: Percentage change from current to final prediction
| HTTP Code | Meaning | Action |
|---|---|---|
| 401 | Invalid/expired token | Refresh token or re-login |
| 402 | Insufficient credits | Prompt credit purchase |
| 404 | Job/resource not found | Check job_id |
| 500 | Server error | Retry with backoff |
Error Response Format:
{ "detail": "Insufficient credits. Required: 3, Available: 1" }- Poll
/job/{id}every 3-5 seconds - Use exponential backoff on errors (2s → 3s → 5s → 8s → 12s → 15s)
- Stop when status is
completedorfailed - Timeout after 8 minutes
- Store JWT in Keychain (never UserDefaults)
- Check expiry before requests
- On 401: attempt
/auth/refreshonce, then re-login if that fails
- Show balance prominently in app header
- Pre-check balance before operations
- On 402: deep-link to purchase flow (open
checkout_url) - Show "from_cache: true" as "Instant result (cached)"
from_cache: truemeans 0 credits spent- Client may also cache results locally for UX
- Display
cached_attimestamp to user
Copy these directly into your project:
// src/types/api.ts
export type JobStatus = "pending" | "processing" | "completed" | "failed";
export type AnalyzeMode = "quick" | "debate";
export type PredictMode = "quick" | "full";
export type TradeAction = "buy" | "sell" | "hold" | "strong_buy" | "strong_sell";
export interface ApiErrorResponse {
detail: string;
}
export interface AuthTokenResponse {
access_token: string;
refresh_token?: string;
token_type: "bearer" | string;
expires_in?: number;
user?: { id: number; email: string; tier?: string };
}
export interface RefreshTokenResponse {
access_token: string;
refresh_token?: string;
token_type: "bearer" | string;
expires_in?: number;
}
export interface ApiKeyCreateResponse {
id: number;
token: string;
name: string;
created_at: string;
expires_at?: string | null;
message?: string;
}
export interface CreditBalanceResponse {
balance: number;
user_id: number;
}
export type CreditPackageId = "starter" | "pro" | "power" | "enterprise";
export interface CreditsPurchaseRequest {
package_id: CreditPackageId;
}
export interface CreditsPurchaseResponse {
checkout_url: string;
session_id: string;
}
/** ---------- Submit: Prediction ---------- */
export interface PredictRequest {
symbol: string;
mode: PredictMode;
forecast_days: number;
model_type: "lstm" | "gru" | "xgboost" | "random_forest";
}
export interface JobSubmitResponseBase {
job_id: string;
status: JobStatus;
symbol: string;
mode: string;
cost_credits: number;
poll_url: string;
estimated_seconds?: number;
}
/** ---------- Submit: Agent Analyze ---------- */
export interface AgentAnalyzeRequest {
symbol: string;
mode: AnalyzeMode;
skip_cache?: boolean;
}
export interface AgentAnalyzeCacheHitResponse {
status: "completed";
symbol: string;
mode: AnalyzeMode;
result: NormalizedAgentAnalysisV1;
from_cache: true;
cached_at: string;
hit_count: number;
cost_credits: 0;
}
export type AgentAnalyzeResponse =
| AgentAnalyzeCacheHitResponse
| JobSubmitResponseBase;
/** ---------- Normalized result: Agent (schema v1) ---------- */
export interface NormalizedAgentAnalysisV1 {
schema_version: 1;
ticker: string;
mode: AnalyzeMode;
engine: string;
as_of: string | null;
generated_at: string;
action: TradeAction;
summary: string;
confidence: number | null;
bull_case: string[];
bear_case: string[];
risk_notes: string[];
what_to_verify: string[];
sources: Array<string | { title?: string; url?: string }>;
transcript: string[];
}
/** ---------- Normalized result: Prediction (schema v1) ---------- */
export interface NormalizedPredictionV1 {
schema_version: 1;
ticker: string;
mode: PredictMode;
engine: string;
generated_at: string;
current_price: number;
predictions: number[];
forecast_days: number;
model_type: string;
model_id: string | null;
direction: "up" | "down" | "neutral";
price_change_pct: number;
training_time_seconds?: number;
data_points?: number;
}
/** ---------- Poll job ---------- */
export interface JobPollResponse {
job_id: string;
status: JobStatus;
symbol: string;
job_type?: string;
mode: string;
cost_credits?: number;
created_at?: string;
completed_at?: string | null;
result?: NormalizedAgentAnalysisV1 | NormalizedPredictionV1;
error_message?: string | null;
}// src/lib/api.ts
export const API_BASE_URL =
process.env.EXPO_PUBLIC_API_BASE_URL ?? "https://api.yourdomain.com";
let bearerToken: string | null = null;
let apiKey: string | null = null;
export function setBearerToken(token: string | null) {
bearerToken = token;
}
export function setApiKey(token: string | null) {
apiKey = token;
}
function buildAuthHeaders(): Record<string, string> {
if (apiKey) return { "X-API-Key": apiKey };
if (bearerToken) return { Authorization: `Bearer ${bearerToken}` };
return {};
}
async function requestJSON<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE_URL}${path}`, {
...init,
headers: {
"Content-Type": "application/json",
...(init?.headers ?? {}),
...buildAuthHeaders(),
},
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`HTTP ${res.status}: ${text || res.statusText}`);
}
return (await res.json()) as T;
}
// --- Auth ---
export async function login(email: string, password: string) {
return requestJSON<AuthTokenResponse>("/auth/token", {
method: "POST",
body: JSON.stringify({ email, password }),
});
}
export async function refresh(refresh_token: string) {
return requestJSON<RefreshTokenResponse>("/auth/refresh", {
method: "POST",
body: JSON.stringify({ refresh_token }),
});
}
// --- Credits ---
export async function getCredits() {
return requestJSON<CreditBalanceResponse>("/credits/balance");
}
export async function purchaseCredits(package_id: CreditPackageId) {
return requestJSON<CreditsPurchaseResponse>("/credits/purchase", {
method: "POST",
body: JSON.stringify({ package_id }),
});
}
// --- Jobs ---
export async function pollJob(jobId: string) {
return requestJSON<JobPollResponse>(`/job/${encodeURIComponent(jobId)}`);
}
// --- Agent ---
export async function submitAgentAnalyze(payload: AgentAnalyzeRequest) {
return requestJSON<AgentAnalyzeResponse>("/agent/analyze", {
method: "POST",
body: JSON.stringify(payload),
});
}
// --- Prediction ---
export async function submitPredict(payload: PredictRequest) {
return requestJSON<JobSubmitResponseBase>("/predict", {
method: "POST",
body: JSON.stringify(payload),
});
}// src/lib/polling.ts
const BACKOFF_MS = [2000, 3000, 5000, 8000, 12000, 15000];
export async function pollUntilDone(
jobId: string,
opts?: { maxMinutes?: number; onTick?: (r: JobPollResponse) => void }
): Promise<JobPollResponse> {
const deadline = Date.now() + (opts?.maxMinutes ?? 8) * 60_000;
let attempt = 0;
while (Date.now() < deadline) {
const r = await pollJob(jobId);
opts?.onTick?.(r);
if (r.status === "completed" || r.status === "failed") return r;
const wait = BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)];
attempt += 1;
await new Promise((res) => setTimeout(res, wait));
}
throw new Error("Timed out waiting for job completion");
}import { submitAgentAnalyze } from "./lib/api";
import { pollUntilDone } from "./lib/polling";
async function runAgent(symbol: string, mode: "quick" | "debate") {
const r = await submitAgentAnalyze({ symbol, mode, skip_cache: false });
// Cache hit returns completed result immediately
if ("from_cache" in r && r.from_cache === true) {
return r.result;
}
// Otherwise poll the job
const done = await pollUntilDone(r.job_id);
if (done.status === "failed") {
throw new Error(done.error_message ?? "Job failed");
}
return done.result;
}For Expo:
# .env
EXPO_PUBLIC_API_BASE_URL=https://api.yourdomain.com
- This API is polling-based by design (no SSE or WebSockets required)
- Schema versioning guarantees backward compatibility
- All timestamps are ISO 8601 UTC
- This document is the single source of truth for iOS integration
Generated: December 2024 API Version: 1.0.0 Schema Version: 1