@ellipsis-labs/rise
v0.6.2
Published
Rise SDK - Phoenix HTTP client
Keywords
Readme
@ellipsis-labs/rise
rise is the TypeScript SDK for Phoenix public APIs, exchange metadata,
instruction building, Flight routing, and live streams.
It gives you:
- a unified client for HTTP, auth, session storage, and WebSocket streams
- typed route clients for candles, markets, funding, traders, trades, and more
- optional end-user auth with managed or externally controlled sessions
- an exchange metadata cache for bootstrapping market parameters
- reusable store engines for market stats, trader state, and orderbooks
- built-in WebSocket adapters plus typed custom subscription registration
- low-level transport helpers for custom HTTP endpoints
Changelog
See the public Rise TypeScript changelog for release notes.
Main Entry Points
import {
auth,
createPhoenixClient,
createPhoenixWsClient,
get,
registerSubscription,
} from "@ellipsis-labs/rise";The most common constructor is createPhoenixClient(...), which returns:
client.api: typed HTTP route clientsclient.pda: PDA/address helper surface with memoized derivations by defaultclient.auth: auth client, only when auth is configuredclient.sessionManager: shared auth session manager, only when auth is configuredclient.streams: typed WebSocket streams, unlessws: falseis configuredclient.exchange: client-owned exchange metadata cache with API/RPC source managementclient.orderPackets: async order-packet builders backed by cached market metadataclient.ixs: async instruction builders backed by cached exchange metadata and PDA resolutionclient.rpc: explicit on-chain read surface for raw account-backed data
Pass pdaCache: false to createPhoenixClient(...) if you need to disable PDA
memoization on client.pda, or pdaCache: { maxEntries: 1024 } if you want to
spell out the default bounded LRU cache for long-running processes.
Directory Structure
src/api/: typed HTTP route clients forcandles,collateral,exchange,funding,invite,markets,notifications,orderbook,orders,splines,traders, andtrades, plus shared helpers inapi/utils/src/auth/: auth client, session manager, storage adapters, runtime helpers, and retry/backoff logicsrc/accounts/: Phoenix and Flight account decoders plus reusable fetcherssrc/core/: low-level Phoenix instruction builders, account resolution helpers, and shared client typessrc/ixs.ts: exchange-aware async instruction client built onsrc/core/and PDA derivationsrc/orderPackets.ts: synchronous order-packet builders from explicit market paramssrc/exchange-cache/: API/RPC metadata bootstrap, selectors, store logic, and projected market viewssrc/flight/: Flight account fetchers, proxy-instruction helpers, register-builder/update-fee builders, fee-collector derivation, and the Flight routing guide insrc/flight/README.mdsrc/ws/: websocket transport, built-in adapters, and custom subscription registrationsrc/market-data/,src/orderbooks/,src/trader-state/: long-lived live stores for exchange data and trader statesrc/rpc.ts,src/pdas.ts, andsrc/pdaClient.ts: raw account reads and address-derivation helperssrc/primitives/,src/types/, andsrc/margin/: shared SDK value types, wire types, and margin helperssrc/builders.tsandsrc/flows.ts: higher-level async helpers that compose packet builders, ix builders, and common deposit/withdraw/order flowssrc/_exports/: curated stable re-export bundles for schemas, SDK helpers, instruction builders, and public typesexamples/,scripts/, andtests/: runnable examples, task-specific scripts, and test coverage
Examples
Use the examples as the fastest reference for intended SDK usage:
examples/README.md: entry point for the runnable TS examples
examples/01-http-client.ts: basic HTTP client
examples/02-ws-fills.ts: websocket fills stream
examples/03-build-limit-order-ix.ts: metadata-backed packet and ix construction
examples/04-trader-state-store.ts: live trader-state store wiring
examples/05-cancel-all-conditional-orders.ts: advanced conditional-order cancellation workflow
examples/06-flight-market-order.ts: Flight-routed market order placement with builder fee routing
examples/09-referral-activation-tx.ts: referral activation through
/v1/referral/activate-txexamples/10-builder-onboarding-tx.ts: builder onboarding without a referral code through
/v1/exchange/build-register-ixsand/v1/exchange/send-register-ixs. The fee-payer keypair path defaults to~/.config/solana/id.json; the optional trader authority defaults to the fee payer's public key.bun examples/10-builder-onboarding-tx.ts \ [--fee-payer-keypair-path <PATH>] \ [--trader-authority <TRADER_AUTHORITY_PUBKEY>]examples/phoenix-client-example.ts: broader API walkthrough
examples/phoenix-ws-example.ts: interactive websocket demo
Atomic isolated market entry with TP/SL v2
client.api.orders().placeIsolatedMarketOrder() and
client.api.orders().placeIsolatedMarketOrderEnhanced()
accept greaterTrigger, lessTrigger, and sizePercent: 100. Supply either
trigger or both. Both triggers share one position conditional and cancel
together when one executes. The backend allocates the isolated subaccount if
needed and includes collection creation in the same instruction bundle.
const instructions = await client.api.orders().placeIsolatedMarketOrder({
authority,
symbol: "SOL-PERP",
side: "buy",
quantity: 1,
transferAmount: 10_000_000,
sizePercent: 100,
greaterTrigger: { side: "sell", triggerPrice: 180 },
lessTrigger: { side: "sell", triggerPrice: 140 },
});For short entries, use side: "buy" on the closing triggers; take profit is
then the less trigger and stop loss the greater trigger. Authority, fee-payer,
and Flight options apply to the whole bundle. Do not combine v2 fields with
legacy tpSl. Partial and fixed-lot protection are not supported on this path.
This request requires a backend release containing PRO-829.
Recommended Client Setup
createPhoenixClient() works with no config and defaults apiUrl to
https://perp-api.phoenix.trade.
For a long-running app that uses exchange-aware helpers, start with:
import { createPhoenixClient } from "@ellipsis-labs/rise";
const client = createPhoenixClient({
apiUrl: "https://perp-api.phoenix.trade",
rpcUrl: "https://api.mainnet-beta.solana.com",
pdaCache: { maxEntries: 1024 },
exchangeMetadata: {
stream: true,
},
});If you route supported placement flows through Flight, configure it once on the client:
Use embedded wallets for Flight integrations. Integrators are strongly encouraged to provision an embedded wallet per user rather than routing Flight orders through the user's raw/external wallet. A raw wallet shares its Phoenix trader state with every other dapp the user trades on, which results in bad UX (positions, balances, and orders bleeding across integrations). Embedded wallets isolate per-integration state.
import { createPhoenixClient } from "@ellipsis-labs/rise";
const client = createPhoenixClient({
apiUrl: "https://perp-api.phoenix.trade",
rpcUrl: "https://api.mainnet-beta.solana.com",
pdaCache: { maxEntries: 1024 },
exchangeMetadata: {
stream: true,
},
flight: {
builderAuthority: "Builder111111111111111111111111111111111",
builderPdaIndex: 0,
builderSubaccountIndex: 0,
},
});Recommended defaults:
- Prefer
apiUrl;baseUrlis a deprecated alias. - Keep PDA memoization enabled for long-lived processes.
- Enable
exchangeMetadata: { stream: true }when you useclient.exchange,client.orderPackets, orclient.ixsover time and want websocket deltas applied after the initial snapshot bootstrap. - Set
rpcUrlwhen you useclient.rpcor want RPC-backed exchange metadata fallback if the API snapshot is unavailable. If you omit it, rise will also readNEXT_PUBLIC_SOLANA_RPC_URLorSOLANA_RPC_URL. - Keep the default rate-limit cooldown enabled for long-lived clients. When a
retryable GET or HEAD receives
429withRetry-After, later retryable requests on the same client wait for that server window before sending. The cooldown is client-local, not process-global, adds positive release jitter to avoid synchronized retries, and does not delay non-idempotent methods such as POST. Server-provided delays are capped at 30 seconds by default. - If you use Flight consistently, configure it once on the client with
builderAuthority, plus optionalbuilderPdaIndexandbuilderSubaccountIndex. rise derives the builder trader account from those values and defaults both indexes to0. SetfeeBpsOverrideonly when the client should emit Flight's fee-override proxy instruction for supported local order builders.
To tune or disable shared rate-limit cooldowns:
import { createPhoenixClient } from "@ellipsis-labs/rise";
const client = createPhoenixClient({
rateLimitCooldown: {
maxDelayMs: 30_000,
fallbackDelayMs: 1_000,
},
});
const cooldownDisabledClient = createPhoenixClient({
rateLimitCooldown: false,
});For short runnable onboarding examples, see:
- examples/01-http-client.ts
- examples/02-ws-fills.ts
- examples/03-build-limit-order-ix.ts
- examples/04-trader-state-store.ts
- examples/README.md
Quick Start
Public HTTP only
import { createPhoenixClient } from "@ellipsis-labs/rise";
const client = createPhoenixClient();
const exchange = await client.api.exchange().getExchange();
const metadata = await client.exchange.ready();
const markets = await client.api.markets().getMarkets();
const now = Date.now();
const candlesPage = await client.api.candles().getCandlesV2("SOL", {
timeframe: "1m",
from: now - 60 * 60_000,
to: now,
limit: 10_000,
includePartial: false,
});
const trader = await client.api.traders().getTrader("TRADER_PUBKEY");getCandlesV2 exposes millisecond timestamps, cursor pagination, partial-bar
control, mark-price OHLC, external-source metadata, and isFinal. The existing
getCandles method remains available with its original array response and
millisecond timestamps. It remains on the legacy endpoint to preserve its
exchange-only default, server-relative finalized window, and 2,500-bar server
limit. Use getCandlesV2 for canonical v2 data; it defaults to 1,000 bars per
page and accepts explicit page sizes up to 10,000.
Unified HTTP + auth + WebSocket client
import { auth, createPhoenixClient } from "@ellipsis-labs/rise";
const client = createPhoenixClient({
apiUrl: "https://perp-api.phoenix.trade",
auth: true,
authConfig: {
storage: new auth.LocalStorageAuthSessionStorage(),
},
exchangeMetadata: {
stream: true,
},
});
await client.auth!.loginWithPrivyToken(privyToken);
const notifications = await client.api
.notifications()
.getNotificationsByTrader("TRADER_PUBKEY");
for await (const update of client.streams!.marketStats("SOL")) {
console.log(update);
break;
}
const solMarket = client.exchange.market("SOL-PERP");By default rise creates a lazy websocket transport for client.streams.
Pass ws: false to disable it. When the owned transport needs a URL, it
derives it from apiUrl, which defaults to
https://perp-api.phoenix.trade. In lazy mode, the owned transport uses a
30-second idle close by default.
client.exchange still bootstraps from the HTTP snapshot on first use even
when client.streams is available. Opt into exchange websocket follow-up with
exchangeMetadata: { stream: true } when you want live exchange deltas applied
after bootstrap.
The heavier live stores stay explicit:
createPhoenixMarketData({ exchange, marketStats, allMids? })createPhoenixTraderStateManager({ api, traderState? })createTraderStateStore(client, config?)createPhoenixOrderbookManager({ api, l2Book? })
See examples/04-trader-state-store.ts
for a runnable trader-state store example backed by
client.streams.traderState(...).
Prefer leaf selectors against resource.store in React. Avoid selectors that
allocate a fresh wrapper object on every read.
Exchange Metadata
rise now treats exchange metadata as a first-class client subsystem.
const client = createPhoenixClient({
apiUrl: "https://perp-api.phoenix.trade",
rpcUrl: "https://api.mainnet-beta.solana.com",
exchangeMetadata: {
stream: true,
},
});
await client.exchange.ready();
const context = client.exchange.instructionContext("SOL-PERP");
const source = client.exchange.source();
const packet = await client.ixs.orderPackets.buildLimitOrderPacket({
symbol: "SOL-PERP",
side: Side.Bid,
priceUsd: "135.87",
baseUnits: "0.25",
});
const ix = await client.ixs.placeLimitOrder({
authority,
symbol: "SOL-PERP",
orderPacket: packet,
});
client.exchange.subscribe((event) => {
if (event.type === "marketAdded") {
console.log("new market", event.symbol);
}
});Default behavior:
- API-first metadata bootstrap from
/v1/exchange/snapshot - exchange websocket live updates when
wsis configured rpcUrlenablesclient.rpc- if
rpcUrlis omitted, rise also checksNEXT_PUBLIC_SOLANA_RPC_URLandSOLANA_RPC_URL - when an RPC URL is available, exchange metadata can fall back to RPC even in the default API-first mode
- lazy startup: no metadata fetch happens until
client.exchangeis used
For Flight-specific order-routing examples, see src/flight/README.md.
Flame Deposit Funding
For sponsored USDC deposits that should avoid creating user-owned rent accounts, build a Flame proxy-funding transaction instead of the direct deposit flow:
import { buildFlameDepositFundingFlow } from "@ellipsis-labs/rise";
const deposit = await buildFlameDepositFundingFlow(
{
authority,
amount: 1_000_000n,
traderPdaIndex: 0,
feePayer,
sponsorshipToken,
},
ixClient
);The returned instructions create the user's Flame proxy USDC ATA and transfer USDC from the user's wallet ATA into that proxy ATA. They do not perform the Phoenix collateral deposit directly; the Flame crank/indexer completes that asynchronously after the proxy account is funded.
For an atomic sponsored deposit, use buildFlameAtomicDepositFlow. It adds
permission setup plus the Flame DepositToPhoenix instruction. The user signs
the deposit permission; the sponsor fee payer cranks the deposit so wallets
holding no SOL can deposit (the crank fronts rent for the transient proxy
Phoenix ATA and the same instruction refunds it on close).
buildDepositFlow remains the direct Ember + Phoenix deposit path.
Sync Order Packet Builders
If you already have market units in hand, rise also exposes synchronous packet
builders that do not need the client:
import {
buildLimitOrderPacketFromMarketParams,
buildMarketOrderPacketFromMarketParams,
Side,
} from "@ellipsis-labs/rise";
const limitPacket = buildLimitOrderPacketFromMarketParams(
{
side: Side.Bid,
priceUsd: "135.87",
baseUnits: "0.25",
},
{
tickSize: 100,
baseLotsDecimals: 2,
}
);
const marketPacket = buildMarketOrderPacketFromMarketParams(
{
side: Side.Ask,
baseUnits: "0.50",
priceLimitUsd: "135.80",
},
{
tickSize: 100,
baseLotsDecimals: 2,
}
);If you want the client to source market params from the exchange metadata cache,
use client.ixs.orderPackets.* instead:
const packet = await client.ixs.orderPackets.buildMarketOrderPacket({
symbol: "SOL-PERP",
side: Side.Bid,
baseUnits: "0.50",
priceLimitUsd: "136.10",
});Sync Resolved Ix Builders
If you already have resolved exchange, market, and trader accounts in hand,
rise also exposes synchronous resolved ix builders:
import { buildPlaceLimitOrderIxResolved } from "@ellipsis-labs/rise";
const ix = buildPlaceLimitOrderIxResolved({
exchange: {
phoenixProgramAddress,
logAuthorityAddress,
globalConfigurationAddress,
perpAssetMap,
globalTraderIndex,
activeTraderBuffer,
},
market: {
marketAddress,
splineCollection,
},
trader: {
authority,
traderAccount,
},
orderPacket,
});This is the intended non-async SDK layer. The same resolved surface also
includes deposit, withdraw, transfer, and market-order builders. client.ixs.*
is the async convenience layer on top of it.
Common HTTP Usage
Route clients are grouped under client.api:
const client = createPhoenixClient({
apiUrl: "https://perp-api.phoenix.trade",
});
const exchange = await client.api.exchange().getExchange();
const market = await client.api.markets().getMarket("SOL");
const traderState = await client.api
.traders()
.getTraderStateSnapshot("AUTHORITY");
const fills = await client.api.trades().getMarketFills("SOL");
const funding = await client.api.funding().getFundingRateHistory("SOL");Available groups include:
candles()collateral()exchange()funding()invite()markets()notifications()orders()traders()trades()
Authentication
Configure auth when you want the client to attach bearer tokens, refresh
sessions, or authenticate WebSocket subscriptions.
Browser-managed sessions
import { auth, createPhoenixClient } from "@ellipsis-labs/rise";
const client = createPhoenixClient({
apiUrl: "https://perp-api.phoenix.trade",
auth: true,
authConfig: {
storage: new auth.LocalStorageAuthSessionStorage(),
},
});
await client.auth!.loginWithPrivyToken(privyToken);
const snapshot = await client.sessionManager!.exportSnapshot();
console.log(snapshot);External session control
Use this when your app owns the session lifecycle and only wants rise to read
the current session. In this mode rise will not automatically refresh,
clear, or otherwise mutate session state on your behalf. If you explicitly
provide or import a session through your configured session manager, that
manager may still write the session to its backing storage.
import { auth, createPhoenixClient } from "@ellipsis-labs/rise";
const sessionManager = new auth.AuthSessionManager(
new auth.MemoryAuthSessionStorage()
);
const client = createPhoenixClient({
apiUrl: "https://perp-api.phoenix.trade",
auth: true,
authConfig: {
sessionManager,
sessionControl: "external",
},
});
await sessionManager.importSnapshot(snapshotFromYourApp);When auth: true, rise hydrates from the configured storage automatically.
It does not force an eager login flow. Built-in route clients use the shared
session opportunistically when one exists. For custom transport calls, use
RequestOptions.auth to disable auth or require it explicitly.
React Native package entry
The root package export selects dist/native/index.js when the resolver enables
the react-native condition. It exposes the same public API and types as the
default Node/browser entry. The native entry contains no Node filesystem loader;
UTF-8 encoding, randomness, and cryptographic capabilities required by SDK
operations must be supplied by the application's runtime.
Service-account credential files are unsupported in the native entry. Calling
loadServiceAccountCredentialFromPath, or configuring
PHOENIX_SERVICE_ACCOUNT_CREDENTIAL, rejects with PhoenixAuthError code
service_account_credential_file_unavailable. A configured file path takes
precedence over split environment credentials, so this error does not fall back
to those credentials. Explicit credentials and split environment loading retain
the default entry's contract. The default entry continues to load Node
fs/promises lazily when a credential file is requested.
Service-account sessions
Server-side tools can authenticate with the same service-account credential env vars supported by Rise Rust:
PHOENIX_SERVICE_ACCOUNT_CREDENTIAL: path to a JSON credential withclient_id,key_id, andprivate_keyPHOENIX_SERVICE_ACCOUNT_CLIENT_IDPHOENIX_SERVICE_ACCOUNT_KEY_IDPHOENIX_SERVICE_ACCOUNT_PRIVATE_KEY
The split vars also accept the legacy PHOENIX_SERVICE_CLIENT_ID,
PHOENIX_SERVICE_KEY_ID, and PHOENIX_SERVICE_PRIVATE_KEY aliases.
import { createPhoenixClient } from "@ellipsis-labs/rise";
const client = createPhoenixClient({
apiUrl: "https://perp-api.phoenix.trade",
auth: true,
});
await client.auth!.loginWithServiceAccountFromEnv();WebSocket Streams
If you only need streams, you can construct the WS client directly:
import { createPhoenixWsClient } from "@ellipsis-labs/rise";
const streams = createPhoenixWsClient({
url: "wss://perp-api.phoenix.trade/v1/ws",
authMode: "anonymous",
});
for await (const update of streams.orderbook("SOL")) {
console.log(update);
break;
}
streams.close();Built-in stream adapters include:
allMidscandlesexchangeexchangeStatusfillsfundingRatel2BookmarkPricemarketmarketStatsnotificationsorderbookorderbookSnapshottraderState
If you already own a raw websocket transport that implements the exported
WsClient interface, use createPhoenixWsFacade(...) instead of creating
another socket:
import { createPhoenixWsFacade, type WsClient } from "@ellipsis-labs/rise";
declare const sharedWs: WsClient;
const riseStreams = createPhoenixWsFacade({
wsClient: sharedWs,
});
for await (const update of riseStreams.exchange()) {
console.log(update);
break;
}Custom WebSocket Subscriptions
rise exposes typed custom subscription registration on both
createPhoenixWsClient(...) and the top-level registerSubscription(...) /
registerSubscriptions(...) helpers.
import { createPhoenixWsClient } from "@ellipsis-labs/rise";
import z from "zod";
const streams = createPhoenixWsClient({
url: "wss://perp-api.phoenix.trade/v1/ws",
authMode: "anonymous",
});
const customPrices = streams.registerSubscription({
subscriptionChannel: "custom-prices",
schema: z.object({
channel: z.literal("custom-prices"),
symbol: z.string(),
price: z.number(),
}),
buildKey: (symbol: string) => symbol,
buildSubParams: (symbol: string) => ({ symbol }),
getKeyFromMessage: (message) => message.symbol,
processMessage: (message) => message.price,
});
for await (const price of customPrices("SOL")) {
console.log(price);
break;
}Custom HTTP Endpoints
PhoenixHttpClient implements the exported HttpTransport interface, so you
can reuse the built-in get / post / put / patch / del helpers for
custom endpoints.
import { createPhoenixClient, get } from "@ellipsis-labs/rise";
import z from "zod";
const client = createPhoenixClient({
apiUrl: "https://perp-api.phoenix.trade",
});
const HealthSchema = z.object({
ok: z.boolean(),
});
const health = await get(client.api, "/internal/health", HealthSchema, {
auth: "disabled",
});This is the cleanest way to add a small custom route without building another
wrapper client first. The same pattern works with post, put, patch, and
del.
Errors
The main error types are:
PhoenixHttpError: HTTP status, code, retry-after, and parsed response bodyPhoenixAuthError: auth/login/refresh/bootstrap failures
import {
PhoenixAuthError,
PhoenixHttpError,
createPhoenixClient,
} from "@ellipsis-labs/rise";
try {
const client = createPhoenixClient({
apiUrl: "https://perp-api.phoenix.trade",
});
await client.api.markets().getMarkets();
} catch (error) {
if (error instanceof PhoenixAuthError) {
console.error("Auth failed", error.code);
} else if (error instanceof PhoenixHttpError) {
console.error("HTTP failed", error.status, error.code);
} else {
throw error;
}
}Development
bun install
bun run build
bun run lint
bun run type-check
bun run test