Skip to content

Latest commit

 

History

History
725 lines (584 loc) · 14.7 KB

File metadata and controls

725 lines (584 loc) · 14.7 KB

PreMarketML – iOS API Integration Guide (Final)

This document is the canonical integration guide for the PreMarketML iOS app. All APIs, schemas, and behaviors described here are stable and production-ready.


Base URL

Production

https://api.yourdomain.com

(Behind Cloudflare Tunnel – no port number)

Local Development

http://localhost:8000

API Docs

  • Swagger: /docs
  • ReDoc: /redoc

Authentication

You may use either JWT tokens or API keys (not both).

Option 1: JWT (recommended for user sessions)

Login

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>

Refresh Token

POST /auth/refresh
{ "refresh_token": "eyJ..." }

Option 2: API Key (persistent / background use)

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...

Job Status Values (Authoritative)

pending → processing → completed | failed

No other values are used by the API.


Credit System

Check Balance

GET /credits/balance
Authorization: Bearer <token>

Response:

{ "balance": 50, "user_id": 1 }

Credit Pricing

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

Purchase Credits (Stripe Checkout)

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.


Core Endpoints

1) AI Market Analysis

POST /agent/analyze
Authorization: Bearer <token>
Content-Type: application/json
{
  "symbol": "NVDA",
  "mode": "quick",
  "skip_cache": false
}

Cache Behavior

  • quick: cached 12 hours
  • debate: cached 48 hours
  • Cache hits cost 0 credits
  • In-flight deduplication enabled (same analysis running = reuse job, 0 credits)

Cache Hit Response (instant, free)

{
  "status": "completed",
  "symbol": "NVDA",
  "mode": "quick",
  "result": { ... },
  "from_cache": true,
  "cached_at": "2024-01-15T10:00:00Z",
  "hit_count": 5,
  "cost_credits": 0
}

New Job Response

{
  "job_id": "job_xyz789",
  "status": "pending",
  "symbol": "NVDA",
  "mode": "quick",
  "cost_credits": 3,
  "poll_url": "/job/job_xyz789",
  "estimated_seconds": 120
}

2) Stock Prediction

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 days
  • model_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
}

3) Poll Job Status

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
}

4) List Recent Jobs

GET /jobs?limit=20
Authorization: Bearer <token>

Normalized Result Schema (Stable – v1)

All results use schema_version: 1 for backward compatibility.

Agent Analysis Result

{
  "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 or null
  • transcript: Populated only for "debate" mode (up to 50 turns)
  • All arrays are capped at 10 items

Prediction Result

{
  "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

Error Handling

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" }

iOS Implementation Guidelines

Polling Strategy

  • Poll /job/{id} every 3-5 seconds
  • Use exponential backoff on errors (2s → 3s → 5s → 8s → 12s → 15s)
  • Stop when status is completed or failed
  • Timeout after 8 minutes

Token Management

  • Store JWT in Keychain (never UserDefaults)
  • Check expiry before requests
  • On 401: attempt /auth/refresh once, then re-login if that fails

Credits UX

  • 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)"

Caching

  • from_cache: true means 0 credits spent
  • Client may also cache results locally for UX
  • Display cached_at timestamp to user

TypeScript Types (Authoritative)

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;
}

API Client Example

// 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),
  });
}

Polling Helper with Backoff

// 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");
}

Usage Example

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;
}

Environment Variables

For Expo:

# .env
EXPO_PUBLIC_API_BASE_URL=https://api.yourdomain.com

Final Notes

  • 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