Skip to main content
Autonome’s API uses oRPC for type-safe remote procedure calls. The oRPC client is configured once and provides access to all API procedures with full TypeScript types and TanStack Query integration.

Client Setup

The oRPC client is initialized in src/server/orpc/client.ts:
import { createORPCClient } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";
import type { RouterClient } from "@orpc/server";
import { createTanstackQueryUtils } from "@orpc/tanstack-query";

import { getRpcUrl } from "@/core/shared/api/apiConfig";
import type router from "@/server/orpc/router";

// Create RPC link with API endpoint
const link = new RPCLink({
  url: getRpcUrl(), // Returns "http://localhost:8081/api/rpc" in dev
});

// Create typed client from router
export const client: RouterClient<typeof router> = createORPCClient(link);

// Create TanStack Query utilities
export const orpc = createTanstackQueryUtils(client);

Key Components

  1. RPCLink - HTTP transport layer that connects to /api/rpc endpoint
  2. RouterClient - Type-safe client inferred from server router
  3. TanStack Query Utils - Provides queryOptions() for data fetching
The client is typed against the server router, so TypeScript will autocomplete all available procedures and validate input/output types at compile time.

API URL Configuration

The API endpoint is determined by getRpcUrl() in src/core/shared/api/apiConfig.ts:
export function getApiBaseUrl(): string {
  // 1. Check environment variable (production)
  if (import.meta.env.VITE_API_URL) {
    return import.meta.env.VITE_API_URL.replace(/\/$/, "");
  }

  // 2. Use relative path (development with Vite proxy)
  if (typeof window !== "undefined") {
    return window.location.origin;
  }

  // 3. Server-side fallback
  return "http://localhost:8081";
}

export function getRpcUrl(): string {
  return `${getApiBaseUrl()}/api/rpc`;
}

Environment Priority

  1. Production - VITE_API_URL points to remote API server (e.g., https://api.autonome.ai)
  2. Development - Vite proxy forwards /api/* to http://localhost:8081
  3. Fallback - Direct connection to http://localhost:8081

Using the oRPC Client

The orpc utility provides the queryOptions() pattern for TanStack Query:

Basic Query

import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/server/orpc/client";

function TradesList() {
  const { data, isLoading, error } = useQuery(
    orpc.trading.getTrades.queryOptions({
      input: {
        variant: "Apex",
        limit: 50,
      },
    })
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data.trades.map((trade) => (
        <li key={trade.id}>
          {trade.symbol} - {trade.side} - ${trade.netPnl}
        </li>
      ))}
    </ul>
  );
}

Query with No Input

const { data } = useQuery(
  orpc.models.getModels.queryOptions({
    input: {}, // Empty object for procedures with no parameters
  })
);

Mutations (Write Operations)

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { client } from "@/server/orpc/client";

function ResetButton() {
  const queryClient = useQueryClient();

  const { mutate, isPending } = useMutation({
    mutationFn: async () => {
      return await client.simulator.resetAccount({
        accountId: "default",
      });
    },
    onSuccess: () => {
      // Invalidate related queries
      queryClient.invalidateQueries({ queryKey: ["simulator", "account"] });
    },
  });

  return (
    <button onClick={() => mutate()} disabled={isPending}>
      {isPending ? "Resetting..." : "Reset Account"}
    </button>
  );
}
For mutations, use the raw client object instead of orpc.*.*.queryOptions(). The client provides direct procedure calls that return promises.

Type Safety

The oRPC client provides full end-to-end type safety:
// ✅ TypeScript autocomplete shows all available procedures
orpc.trading.getTrades
orpc.trading.getPositions
orpc.models.getModels
orpc.simulator.placeOrder

// ✅ Input validation at compile time
orpc.trading.getTrades.queryOptions({
  input: {
    variant: "Apex", // Must be a valid VariantId
    limit: 50,       // Must be between 1-500
  },
});

// ❌ TypeScript error: Invalid input
orpc.trading.getTrades.queryOptions({
  input: {
    variant: "InvalidVariant", // Error: Type '"InvalidVariant"' is not assignable
    limit: 999,               // Error: Must be max 500
  },
});

// ✅ Output types are inferred automatically
const { data } = useQuery(orpc.trading.getTrades.queryOptions({ input: {} }));
// data.trades is typed as Array<TradeRecord>

SSE Event Streams

For real-time data, use Server-Sent Events (SSE) endpoints:
import { getSseUrl } from "@/core/shared/api/apiConfig";

function usePositionUpdates() {
  const queryClient = useQueryClient();

  useEffect(() => {
    const eventSource = new EventSource(getSseUrl("/api/events/positions"));

    eventSource.onmessage = (event) => {
      const positions = JSON.parse(event.data);
      
      // Invalidate positions query to trigger re-fetch
      queryClient.invalidateQueries({ 
        queryKey: ["trading", "positions"] 
      });
    };

    eventSource.onerror = (error) => {
      console.error("SSE error:", error);
      eventSource.close();
    };

    return () => eventSource.close();
  }, [queryClient]);
}

Available SSE Endpoints

EndpointDescriptionEvent Data
/api/events/positionsReal-time position updatesArray of positions by model
/api/events/tradesReal-time trade updatesArray of completed trades
/api/events/conversationsAI invocationsArray of conversation records
/api/events/portfolioPortfolio snapshotsPortfolio summary data
/api/events/workflowWorkflow executionWorkflow step events
SSE streams send a heartbeat ping every 15 seconds to keep connections alive. The initial message contains current state, followed by incremental updates.

Error Handling

All oRPC procedures wrap handlers with Sentry spans for monitoring:
export const getTrades = os
  .input(TradesInputSchema)
  .output(TradesResponseSchema)
  .handler(async ({ input }) => {
    return Sentry.startSpan({ name: "getTrades" }, async () => {
      try {
        const result = await fetchTrades(input);
        return { trades: result };
      } catch (error) {
        Sentry.captureException(error);
        throw new Error("Failed to fetch trades");
      }
    });
  });

Client-Side Error Handling

const { data, error, isError } = useQuery(
  orpc.trading.getTrades.queryOptions({ input: {} })
);

if (isError) {
  console.error("Query failed:", error.message);
  // error.message contains the server-thrown error message
}

CORS Configuration

The API server enables CORS for local development and production origins:
// api/src/index.ts
app.use(
  "*",
  cors({
    origin: (origin) => {
      // Allow localhost for development
      if (!origin || origin.includes("localhost") || origin.includes("127.0.0.1")) {
        return origin || "*";
      }
      
      // In production, check allowed origins
      const allowedOrigins = env.CORS_ORIGINS?.split(",") ?? [];
      return allowedOrigins.includes(origin) ? origin : "";
    },
    allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
    allowHeaders: ["Content-Type", "Authorization"],
    credentials: true,
  })
);

Production CORS Setup

Set CORS_ORIGINS environment variable on the API server:
CORS_ORIGINS=https://autonome.ai,https://app.autonome.ai

Authentication (Future)

Currently, Autonome runs in single-user mode with no authentication. Future versions will add:
  • API Keys - Per-user keys for API access
  • JWT Tokens - Session-based authentication
  • OAuth - Third-party authentication providers
Authentication will be implemented via oRPC middleware that validates tokens before routing to procedure handlers.

Next Steps

Trading API

Fetch trades, positions, and market data

Simulator API

Paper trade with the exchange simulator

Models API

Manage AI models and view invocations

Analytics API

Access performance metrics and leaderboards

Build docs developers (and LLMs) love