Skip to main content
Alpaca
Alpaca gives an AI agent a brokerage API: read an account, pull quotes, place/cancel orders, and close positions across stocks, options, and crypto — one endpoint where the order symbol decides the market (AAPL, BTC/USD, AAPL241213C00250000). It does that job well, and the API is mature. But the direct Trading API is also a separate vendor account:
  • The brokerage lives behind an APCA-API-KEY-ID + APCA-API-SECRET-KEY pair, its own dashboard, and its own funding — disconnected from wherever the agent’s cards, email, secrets, and KYC live.
  • One key pair = one account. To trade on behalf of many end-users you must adopt Alpaca’s OAuth2 flow or the Broker API yourself — registering an app, exchanging codes for tokens, storing and encrypting each user’s token, and isolating per user.
  • “Who let this agent place a $5,000 order, and what else can it touch?” is answered in Alpaca for trading, and in unrelated systems for everything else. There is no execution-time approval gate on money-moving actions — a leaked key trades freely until you notice.
Naive’s trading primitive gives the agent the same capability — it literally proxies the Alpaca Trading API v2 on the user’s behalf — but every order is rooted in one governed identity:
  • The tenant user that links the brokerage is the same user that owns its cards, its email inboxes, its vault secrets, and its KYC.
  • Naive owns the platform OAuth app and performs the token exchange itself, so you never touch OAuth plumbing, token storage, or per-user isolationconnect() returns an authorize URL and Naive stores the bearer token encrypted at rest.
  • Whether an agent may place, cancel, or close — and whether each money-moving action freezes for human approval — is decided by that user’s Account Kit and per-agent assignment at execution time, not by an API-key scope you manage separately.
This guide maps Alpaca’s Trading API to Naive’s, shows the smallest working swap, and is explicit about what does not map yet.
Alpaca is a trademark of Alpaca Securities LLC, used here for identification only. Naive is not a broker, broker-dealer, or investment adviser and does not exercise trading discretion — the connected brokerage (Alpaca) is the broker, and the user directs and approves the trades. No endorsement or affiliation is implied beyond the documented Trading API proxy. See the trading primitive for the full disclosure.
Tested against: the Alpaca Node helper library @alpacahq/alpaca-trade-api v3.1.3 against the Alpaca Trading API v2 (paper base https://paper-api.alpaca.markets, live base https://api.alpaca.markets, data base https://data.alpaca.markets, docs snapshot June 2026), and the Naive Node SDK @usenaive-sdk/server against the Naive API (base https://api.usenaive.ai/v1, docs snapshot June 2026).Version notes:
  • Order schema is shared. Naive forwards the same Trading-API order fields Alpaca documents — symbol, qty/notional, side, type, time_in_force, limit_price, stop_price, order_class, extended_hours, client_order_id. The before/after below is close to a rename.
  • Auth model differs. Alpaca’s direct Trading API authenticates with a static key pair for one account. Naive uses Alpaca’s OAuth2 Connect flow per user: trading.connect({ env }) returns an authorize_url; Naive exchanges the code (scope account:write trading data) and stores the token. A user may link paper and/or live (env: "paper" | "live").
  • Money-moving actions are approval-gated by default on Naive (createOrder, cancelOrder, closePosition) — Alpaca’s Trading API has no equivalent. See gain #2.
  • No equivalent yet for Alpaca’s Broker API (opening/funding brokerage accounts), real-time streaming, historical market data, order replace, watchlists, or portfolio history — see what doesn’t map yet.

Concept map

AlpacaNaiveNotes
new Alpaca({ keyId, secretKey, paper })new Naive({ apiKey }) then naive.forUser(id)Server-side credential in both cases
APCA-API-KEY-ID / APCA-API-SECRET-KEY — one brokerage account, isolated from your other vendorstenant user via naive.forUser(id) — scopes every primitiveThe core consolidation win
You build OAuth2 (/oauth/authorize/oauth/token) + token storage for multi-usertrading.connect({ env })authorize_url (Naive owns the OAuth app and stores the token)Naive performs the code-for-token exchange and encrypts the token at rest
paper: true / APCA_API_BASE_URL selects paper vs liveenv: "paper" | "live" per call (link both, pass env)Same two environments
alpaca.getAccount()GET /v2/accountclient.trading.account({ env })Read the connected account
alpaca.getAssets({ asset_class })GET /v2/assetsclient.trading.assets({ assetClass })Tradable instruments
alpaca.getPositions()GET /v2/positionsclient.trading.positions({ env })Open positions
alpaca.getPosition(symbol)client.trading.position(symbol)One position
alpaca.getOrders({ status })GET /v2/ordersclient.trading.orders({ status, limit })List orders
alpaca.getOrder(id)client.trading.getOrder(id)One order
alpaca.getLatestQuotes(...) / getLatestCryptoQuotes(...) (data API)client.trading.quote(symbols, { assetClass })Latest quote(s) only — no history (see gaps)
alpaca.createOrder({ ... })POST /v2/ordersclient.trading.createOrder({ ... }) (sensitive)Identical fields; may return a PendingApproval
alpaca.cancelOrder(id)DELETE /v2/orders/{id}client.trading.cancelOrder(id) (sensitive)May return a PendingApproval
alpaca.closePosition(symbol)DELETE /v2/positions/{symbol}client.trading.closePosition(symbol, { qty, percentage }) (sensitive)May return a PendingApproval
API-key scopes for least-privilegeAccount Kit trading primitive + per-agent assignmentPermission is execution-time policy on the identity — see gain #2
client_order_id for dedupeclient_order_id on the order body, or Idempotency-Key header on RESTNaive forwards client_order_id when set; otherwise the request’s Idempotency-Key becomes client_order_id
OAuth app approval for live tradingSame gate — live needs the brokerage app approvedPaper works for development
alpaca.replaceOrder(id, ...)PATCH /v2/orders/{id}No modify-in-place; cancel + re-place
Broker API (create/fund/KYC brokerage accounts)Naive uses the Connect model — the user brings their own account
Streaming (trade_updates, market-data websocket)Poll orders / positions; no event stream
Historical market data (bars, trades, news, snapshots)quote is latest only
Watchlists, portfolio history, account activities, calendar/clockNo equivalent endpoints

Before / after: the core path

The path that matters for almost every trading agent is read the account, then place an order. Here it is on both platforms.
import Alpaca from "@alpacahq/alpaca-trade-api";

// One account behind a static key pair (paper here).
const alpaca = new Alpaca({
  keyId: process.env.APCA_API_KEY_ID!,
  secretKey: process.env.APCA_API_SECRET_KEY!,
  paper: true,
});

// 1. Read the account (GET /v2/account)
const account = await alpaca.getAccount();

// 2. Place an order (POST /v2/orders) — runs immediately, no approval gate.
const order = await alpaca.createOrder({
  symbol: "BTC/USD",
  notional: 25,           // qty OR notional
  side: "buy",
  type: "market",
  time_in_force: "gtc",
});
// → order.id, order.status

// To serve many end-users you must build the OAuth2 flow yourself
// (/oauth/authorize → /oauth/token) and store + encrypt each user's token.
import { Naive, isPendingApproval } from "@usenaive-sdk/server";

const naive = new Naive({ apiKey: process.env.NAIVE_API_KEY! });
const client = naive.forUser("alice"); // same id space as your own users

// 0. One-time: link the user's brokerage via OAuth (Naive owns the app).
//    Open authorize_url in a browser; Naive stores the token encrypted.
const { authorize_url } = await client.trading.connect({ env: "paper" });

// 1. Read the account (proxies GET /v2/account)
const account = await client.trading.account({ env: "paper" });

// 2. Place an order (proxies POST /v2/orders) — SENSITIVE: may freeze for
//    human approval depending on the user's Account Kit.
const res = await client.trading.createOrder({
  symbol: "BTC/USD",
  notional: "25",         // qty OR notional
  side: "buy",
  type: "market",
  time_in_force: "gtc",
  env: "paper",
});
if (isPendingApproval(res)) {
  await client.approvals.wait(res.approval_id); // runs only after a human approves
}
// → order id, status
The order shape lines up almost exactly. The real differences to plan for:
  • One-time connect, not a static key. Alpaca’s direct API authenticates with a key pair for a single account. Naive links each user’s brokerage once via OAuth and proxies thereafter — you never store or encrypt the token.
  • Money-moving actions can defer. createOrder, cancelOrder, and closePosition are in Naive’s default approval set. Handle isPendingApproval(res) and approvals.wait; reads (account, positions, orders, quote) never defer.
  • env rides on the call. Where Alpaca picks paper/live at client construction, Naive takes env per call (a user can link both).
  • The id is your identity, not a separate account. In Alpaca the key pair scopes Alpaca. In Naive the same forUser(id) handle also owns the agent’s cards, email, vault, and KYC.

Stocks, options, and crypto

The same createOrder endpoint trades every asset class on both platforms — the symbol selects the market. The field names are identical.
// Stock: limit order, good-for-day
await alpaca.createOrder({
  symbol: "AAPL", qty: 2, side: "buy",
  type: "limit", limit_price: 150, time_in_force: "day",
});
// Stock: limit order, good-for-day (SENSITIVE)
const res = await client.trading.createOrder({
  symbol: "AAPL", qty: 2, side: "buy",
  type: "limit", limit_price: 150, time_in_force: "day", env: "paper",
});
if (isPendingApproval(res)) await client.approvals.wait(res.approval_id);
  • Crypto (BTC/USD) trades 24/7 with time_in_force gtc/ioc; the brokerage may enforce minimum order sizes for crypto — see trading and Alpaca’s docs.
  • Options use OCC symbols (AAPL241213C00250000); crypto trading must be enabled on the user’s brokerage account (trading.account()crypto_status) — Naive surfaces it but does not toggle it.

Minimal viable migration

The smallest swap that keeps a working agent running is connect + read + place/cancel/close.
1

Install the SDK and set your key

npm install @usenaive-sdk/server
Set NAIVE_API_KEY (a server-side key from the dashboard).
2

Link the brokerage once (replaces your key pair / OAuth code)

Replace new Alpaca({ keyId, secretKey, paper }) with naive.forUser(id).trading.connect({ env }). Open the returned authorize_url; Naive exchanges the code and stores the token. Drop any hand-built OAuth callback, token table, and encryption — Naive owns all of it. Confirm with client.trading.connections() (status active).
3

Swap reads

Map getAccounttrading.account, getPositionstrading.positions, getOrderstrading.orders, getLatestQuotes/getLatestCryptoQuotestrading.quote. These never require approval.
4

Swap money-moving actions

Map createOrdertrading.createOrder (same fields), cancelOrdertrading.cancelOrder, closePositiontrading.closePosition. Wrap each in isPendingApproval(res) + client.approvals.wait(res.approval_id) so a deferred action resumes after a human approves.
5

Keep idempotency

If you set client_order_id for dedupe, pass it on createOrder directly — or, on raw REST, send an Idempotency-Key header when client_order_id is omitted. Naive uses client_order_id when provided; otherwise it forwards the Idempotency-Key as client_order_id, so a retried logical order can deduplicate at the broker.
6

Ship it

At this point you are off Alpaca’s direct API for the core trade path. Everything below is upside, not a requirement.

Consolidate further once you’re on Naive

This is where the migration pays for itself. In Alpaca, the key pair isolates a brokerage account and nothing else, and money-moving actions run the instant a key is used. On Naive, the unit of isolation is a tenant user, and every order is governed by that identity’s policy at execution time.
// One account per key pair; to go multi-user you build OAuth + token storage.
const alpaca = new Alpaca({ keyId, secretKey, paper: true });
await alpaca.createOrder({ symbol: "BTC/USD", notional: 25, side: "buy", type: "market", time_in_force: "gtc" });

// The card, the vault secrets, the email inbox, the KYC for this customer's
// agent live in entirely separate systems with their own tenancy — and nothing
// stops the key from placing an order the moment it leaks.
// One tenant user per customer; isolates *every* primitive.
const acme = await naive.users.create({
  external_id: dbCustomer.id,
  email: dbCustomer.email,
});

const client = naive.forUser(acme.id);

await client.trading.connect({ env: "paper" }); // link once
const res = await client.trading.createOrder({
  symbol: "BTC/USD", notional: "25", side: "buy", type: "market", time_in_force: "gtc", env: "paper",
});
if (isPendingApproval(res)) await client.approvals.wait(res.approval_id);

// The SAME client owns this customer's card, email inbox, vault, and KYC —
// one identity, isolated end to end, every order governed by its Account Kit.
await client.cards.create({ spending_limit_cents: 25_000 });
await client.email.createInbox({ local_part: "ops" });
await client.vault.put("internal.api_key", "key_xyz");

Gain #1 — one identity across primitives

  • With Alpaca, the agent’s brokerage is an island behind a key pair. With Naive, naive.forUser(acme.id) is a single handle to trading and cards and email and vault and KYC.
  • Tear the customer down from one place; sibling tenants should not be able to read each other’s orders, positions, cards, or secrets. The brokerage token is stored encrypted at rest and is never handed to the agent.

Gain #2 — execution-time permission enforcement

  • Whether an agent may trade at all is the trading primitive on the user’s Account Kit, and whether each order freezes for human review is requiresApproval policy — not a check you hand-write and not an Alpaca key scope you manage separately.
// Trading enabled, but every money-moving action freezes for human approval.
const desk = await naive.accountKits.create({
  name: "Trading desk",
  primitives_config: {
    trading: { enabled: true, requiresApproval: true }, // place/cancel/close → approval
    cards:   { enabled: false },                          // no card for this tier
  },
});
await naive.accountKits.assignUser(desk.id, acme.id);
  • The agent’s code is identical for every tier — client.trading.createOrder({ ... }). Whether the order runs is decided at execution time:
    • A user whose Account Kit does not enable trading is refused with forbidden, with no code change on your side.
    • With trading.requiresApproval: true, createOrder / cancelOrder / closePosition return 202 { status: "pending_approval" } and the broker call replays only after a human approves. Reads (account, positions, quote) are never gated.
    • This is a key governance difference for money-moving actions: a leaked Alpaca key can trade immediately; a Naive agent’s trades are subject to Account Kit policy and optional approval.

Gain #3 — unified accountability

  • Every connect, order, cancel, and close for a customer lands in one per-user activity log — alongside their card, email, vault, and KYC events, not in a separate Alpaca dashboard:
const { events } = await naive.forUser(acme.id).logs.query({ limit: 50 });
// "what did this agent do, for whom?" — trading, cards, email, vault, one timeline
  • That is the question that is hard to answer when trading lives in Alpaca, cards live in Stripe, and secrets live somewhere else. Under Naive it is a single query.

What does not map yet

A migration guide that hides gaps is worse than none. The core path (connect → read → place/cancel/close) maps cleanly across stocks, options, and crypto, but the following Alpaca capabilities have no direct equivalent on Naive’s trading primitive today. Check this list against your app before you commit.
Alpaca featureStatus on NaiveWorkaround
Broker API (open / fund / KYC brokerage accounts, ACH, journals, fully-disclosed/omnibus)Not providedNaive uses the OAuth Connect model — the user brings their own brokerage account
Real-time streaming (trade_updates, market-data websocket)Not providedPoll trading.orders / trading.positions; no event stream for fills
Historical market data (bars, trades, news, snapshots, corporate actions)Not provided — quote is latest onlyKeep a data vendor (or Alpaca’s data API) for history/backtests
Order replace (PATCH /v2/orders/{id})Not providedcancelOrder then createOrder with new terms
Cancel-all / close-all (DELETE /v2/orders, DELETE /v2/positions)Per-order / per-symbol onlyLoop over orders / positions and cancel/close individually
Watchlists, portfolio history, account activities, account configurations, calendar & clockNo equivalent endpointsTrack in your own app
Advanced order classes detail (bracket / oco / oto legs, trailing_stop)Forwarded fields exist; not all combinations validated end-to-endTest the specific class on paper before relying on it
Live trading without app approvalDifferent modelThe brokerage OAuth app must be approved for live (same as Alpaca); paper works for development
If your agent needs the Broker API (you open and fund accounts for users), real-time fill streaming, historical market data, or order replace, those are the gaps most likely to matter — Naive proxies the Alpaca Trading API v2 for an account the user connects, not a brokerage you operate. Equally, note the model inversion: Alpaca’s direct key trades the instant it is used, while Naive routes every money-moving action through an Account Kit that can freeze it for human approval. That governance is the gain — but it is a real behavior change your agent code must handle (isPendingApproval + approvals.wait).

Where to go next