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-KEYpair, 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.
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 isolation —
connect()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.
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 anauthorize_url; Naive exchanges the code (scopeaccount: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
| Alpaca | Naive | Notes |
|---|---|---|
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 vendors | tenant user via naive.forUser(id) — scopes every primitive | The core consolidation win |
You build OAuth2 (/oauth/authorize → /oauth/token) + token storage for multi-user | trading.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 live | env: "paper" | "live" per call (link both, pass env) | Same two environments |
alpaca.getAccount() → GET /v2/account | client.trading.account({ env }) | Read the connected account |
alpaca.getAssets({ asset_class }) → GET /v2/assets | client.trading.assets({ assetClass }) | Tradable instruments |
alpaca.getPositions() → GET /v2/positions | client.trading.positions({ env }) | Open positions |
alpaca.getPosition(symbol) | client.trading.position(symbol) | One position |
alpaca.getOrders({ status }) → GET /v2/orders | client.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/orders | client.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-privilege | Account Kit trading primitive + per-agent assignment | Permission is execution-time policy on the identity — see gain #2 |
client_order_id for dedupe | client_order_id on the order body, or Idempotency-Key header on REST | Naive forwards client_order_id when set; otherwise the request’s Idempotency-Key becomes client_order_id |
| OAuth app approval for live trading | Same gate — live needs the brokerage app approved | Paper 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/clock | — | No 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.- 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, andclosePositionare in Naive’s default approval set. HandleisPendingApproval(res)andapprovals.wait; reads (account,positions,orders,quote) never defer. envrides on the call. Where Alpaca picks paper/live at client construction, Naive takesenvper 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 samecreateOrder endpoint trades every asset class on both platforms — the symbol selects
the market. The field names are identical.
- Crypto (
BTC/USD) trades 24/7 withtime_in_forcegtc/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.Install the SDK and set your key
NAIVE_API_KEY (a server-side key from the dashboard).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).Swap reads
Map
getAccount → trading.account, getPositions → trading.positions,
getOrders → trading.orders, getLatestQuotes/getLatestCryptoQuotes → trading.quote.
These never require approval.Swap money-moving actions
Map
createOrder → trading.createOrder (same fields), cancelOrder → trading.cancelOrder,
closePosition → trading.closePosition. Wrap each in isPendingApproval(res) +
client.approvals.wait(res.approval_id) so a deferred action resumes after a human approves.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.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.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
tradingprimitive on the user’s Account Kit, and whether each order freezes for human review isrequiresApprovalpolicy — not a check you hand-write and not an Alpaca key scope you manage separately.
- 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
tradingis refused withforbidden, with no code change on your side. - With
trading.requiresApproval: true,createOrder/cancelOrder/closePositionreturn202 { 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.
- A user whose Account Kit does not enable
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:
- 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 feature | Status on Naive | Workaround |
|---|---|---|
| Broker API (open / fund / KYC brokerage accounts, ACH, journals, fully-disclosed/omnibus) | Not provided | Naive uses the OAuth Connect model — the user brings their own brokerage account |
Real-time streaming (trade_updates, market-data websocket) | Not provided | Poll trading.orders / trading.positions; no event stream for fills |
| Historical market data (bars, trades, news, snapshots, corporate actions) | Not provided — quote is latest only | Keep a data vendor (or Alpaca’s data API) for history/backtests |
Order replace (PATCH /v2/orders/{id}) | Not provided | cancelOrder then createOrder with new terms |
Cancel-all / close-all (DELETE /v2/orders, DELETE /v2/positions) | Per-order / per-symbol only | Loop over orders / positions and cancel/close individually |
| Watchlists, portfolio history, account activities, account configurations, calendar & clock | No equivalent endpoints | Track in your own app |
Advanced order classes detail (bracket / oco / oto legs, trailing_stop) | Forwarded fields exist; not all combinations validated end-to-end | Test the specific class on paper before relying on it |
| Live trading without app approval | Different model | The brokerage OAuth app must be approved for live (same as Alpaca); paper works for development |
Where to go next
tradingprimitive — full connect/account/order/position lifecycle + governancetradingSDK sub-client — typed method signaturestradingCLI — connect, account, order, positions, quote- Account Kits and Approvals — the policy model behind execution-time governance and money-moving approval
- Tenant users — the identity that owns the brokerage link, cards, email, and vault
- Cards, Email, Vault — the sibling primitives the same identity owns