Paper Trading
Simulated trading against live market data, for user-facing paper portfolios, testing, and QA.
Two paths. They are not interchangeable.
| Path | Who calls it | Auth |
|---|---|---|
mode: "paper" on the managed quote and the fill | Your app, with the user's session | x-app-id and Authorization: Bearer. No API key. |
| Paper account admin | Your backend | x-app-api-key only |
Only managed quote and fill accept mode: "paper". Direct orders reject it, and an app in paper trading mode gets 400 with code: "paper_mode_unsupported" from POST /execution/orders. Reads listed below take the same mode so paper rows are not mixed with live ones.
Paper trading prices quotes against the current book and writes simulated balances, orders, and positions. It never submits to a venue and never runs a bridge, custody transfer, deposit, withdrawal, or redemption. Do not show a paper balance as a funded wallet.
The signed-in user's paper account is the one whose externalId is user:<aggUserId>. It is created the first time they request paper balances, positions, a quote, or a fill. Keep paper and live state visually separate.
User quote and fill
A paper quote must be filled with mode: "paper". A live fill cannot execute a paper quote. Switching modes needs a fresh quote.
// After the user signs in. This client has no apiKey.
const quote = await client.getSmartRoute({
venueMarketOutcomeId, // id from venueMarketOutcomes[].id
tradeSide: "buy",
maxSpend: 25,
mode: "paper",
});
await client.executeManaged({ quoteId: quote.quoteId, mode: "paper" });Get a quote and Execute quote. Poll Get execution status with the same mode until terminal is true.
Pass mode: "paper" on these reads as well:
| SDK method | API reference |
|---|---|
getManagedBalances({ mode: "paper" }) | Get balances |
getExecutionStatus({ quoteId, mode: "paper" }) | Get execution status |
getExecutionPositions({ mode: "paper" }) | Get user positions |
getExecutionOrders({ mode: "paper" }) | Get user orders |
POST /execution/orders is live only. It has no mode field and rejects one with 400.
UI components
Set paper mode once on AggProvider. HomePage, EventMarketPage, PlaceOrder, and UserProfilePage read it when no executionMode prop is passed.
import { AggProvider } from "@agg-build/ui";
import { QueryClient, QueryClientProvider } from "@agg-build/hooks";
import { HomePage } from "@agg-build/ui/pages";
const queryClient = new QueryClient();
export function App() {
return (
<QueryClientProvider client={queryClient}>
<AggProvider client={client} config={{ trading: { executionMode: "paper" } }}>
<HomePage />
</AggProvider>
</QueryClientProvider>
);
}Pass executionMode on one component when it must differ from the provider. Hide deposit, withdrawal, and redemption controls in a paper-only experience.
Admin accounts
These methods are on AggClient, not a separate admin client. Configure apiKey on a server-side client. Never call them from the browser. The route authorizes x-app-api-key only. Do not send a user bearer token.
The signed-in user sees the account whose externalId is user:<aggUserId>. externalId is unique per app. Creating one that already exists returns a validation error. Look it up with List paper trading accounts instead of creating it again.
// Server-side only. { appId } selects the account path.
const agg = createAggClient({
baseUrl: process.env.AGG_API_BASE_URL!,
appId: "your-app-id",
apiKey: process.env.AGG_APP_API_KEY!,
});
await agg.createPaperTradingAccount(
{
externalId: `user:${aggUserId}`,
initialBalanceRaw: "100000000", // 6-decimal USD. 1000000 = $1
},
{ appId: "your-app-id" },
);Create a paper trading account.
Additional paper-account operations:
| SDK method | API reference |
|---|---|
listPaperTradingAccounts | List paper trading accounts |
getPaperTradingAccount | Get a paper trading account |
setPaperTradingBalance | Set a paper trading account cash balance |
resetPaperTradingAccount | Reset a paper trading account |
placePaperTradingOrder | Place a paper trading order |
listPaperTradingOrders | List paper trading orders |
listPaperTradingPositions | List paper trading positions |
getPaperTradingPortfolio | Get a paper trading portfolio |
resetPaperTradingAccount clears open positions and sets cash. Order history survives a reset. setPaperTradingBalance changes cash only. Amounts are 6-decimal integer strings. initialBalance, balance, and spend also accept numbers. Prefer the raw string for a repeatable fixture.
On placePaperTradingOrder, clientOrderId is stored for traceability only. It is not an idempotency key. The order prices against the current book and comes back filled or rejected.
Testing
Sign in a test user, read paper balances and check PAPER_USD is present, quote an open market with mode: "paper", fill it, and poll to a terminal state. Then read paper orders and positions.
Negative paths: fill a paper quote without mode: "paper" and expect a rejection. Switch from paper to live only with a fresh quote. Paper pages show no deposit, withdrawal, bridge, or redemption.
For a repeatable run, reset the user:<aggUserId> account first, pick outcomes from current discovery, and use a scenario-owned clientOrderId on backend orders so logs map fixtures to orders.