# Ensure the caller's RFQ subaccount exists (Joyride pays the rent once) Source: https://docs.joyride.exchange/api-reference/account/ensure-the-callers-rfq-subaccount-exists-joyride-pays-the-rent-once /_generated/openapi.yaml post /v1/rfq/subaccount Asks core to make sure the authenticated wallet's margin-vault `Subaccount` PDA exists, with Joyride paying its rent at most once per wallet. The owner is always the bearer's wallet — the request has no body fields and cannot provision another wallet. Idempotent and safe to retry: `exists` costs nothing, `submitted` reports the in-flight `init_subaccount` signature (concurrent callers share one send), and `refused` is an answer, not an error. Paced at one call per minute per wallet. On-chain presence is visible within seconds; the RFQ desk's `RFQ_NOT_READY` gate clears once the vault watcher attributes the new subaccount, up to about a minute later. # List deposit history Source: https://docs.joyride.exchange/api-reference/account/list-deposit-history /_generated/openapi.yaml get /v1/deposits Returns the authenticated account's newest deposit audit entries. # Delete conversation Source: https://docs.joyride.exchange/api-reference/ai-chat/delete-conversation /_generated/openapi.yaml delete /v1/ai/conversations/{id} Soft-delete a conversation by setting `deleted_at`, while preserving the underlying message and usage data. # Get conversation messages Source: https://docs.joyride.exchange/api-reference/ai-chat/get-conversation-messages /_generated/openapi.yaml get /v1/ai/conversations/{id}/messages Load user-visible messages for a conversation. Tool-call messages are filtered out server-side. Messages are returned oldest-first. # List conversations Source: https://docs.joyride.exchange/api-reference/ai-chat/list-conversations /_generated/openapi.yaml get /v1/ai/conversations List the authenticated user's conversations, excluding soft-deleted ones. Results are ordered by `updated_at DESC`. Pagination is cursor-based using a compound `updated_at|id` cursor. # Rename conversation Source: https://docs.joyride.exchange/api-reference/ai-chat/rename-conversation /_generated/openapi.yaml patch /v1/ai/conversations/{id} Rename a conversation. The title is trimmed and rejects HTML angle brackets as a stored-XSS defense. # Send message to AI assistant Source: https://docs.joyride.exchange/api-reference/ai-chat/send-message-to-ai-assistant /_generated/openapi.yaml post /v1/ai/chat Send a message to the AI trading assistant and receive a streaming response via Server-Sent Events (SSE). **Authentication:** Requires a Joyride Core session JWT as a Bearer token. **Rate limiting:** 20 requests per minute per wallet. **Response format:** The response is an SSE stream (`text/event-stream`). Each event has an `event` type and JSON `data` field. The stream ends with a `done` event containing the conversation ID and token usage. **Conversation continuity:** Omit `conversation_id` to start a new conversation. Include it to continue an existing conversation with message history. **Trade boundary:** A `strategy` event is a proposal, not an order. The model has no order-placement tool. In the Joyride app, the user taps Buy or Sell and the client keeps that intent while it completes wallet authentication, any required USDC deposit, and final order submission. Account data failures are not represented as a zero or fallback balance. **SSE event types:** - `text` — AI-generated text chunk (streamed incrementally) - `tool_start` — Tool execution started (for UX loading indicators) - `tool_result` — Tool execution completed with summary - `strategy` — Trade recommendation card with a client-owned Buy or Sell action - `table` — Tabular data display - `options_chain` — Options chain data - `price_quote` — Single instrument price quote - `account_summary` — Account balance overview - `order_status` — Order placement/cancellation result - `error` — Error during processing (non-fatal, stream may continue) - `done` — Stream complete with conversation ID and usage stats **Data conventions in SSE events:** Unlike the main REST API which uses internal units (micros, millicontracts), AI Chat SSE events use human-readable units: - Prices in USD (e.g., `1.50` not `1500000`) - Sizes in contracts (e.g., `2.0` not `2000`) - Balances in USD (e.g., `10000.0` not `10000000000`) # Get greeks, model price, and probability of profit Source: https://docs.joyride.exchange/api-reference/market-data/get-greeks-model-price-and-probability-of-profit /_generated/openapi.yaml get /v1/greeks Per-instrument option greeks computed by Joyride: Black-76 (r = 0, forward = spot) model price, delta, gamma, theta, vega, probability-of-profit, and the BlockScholes-derived `mark_iv` passthrough. Served entirely from an in-process cache refreshed by a background poller — no upstream call is made per request. Conventions: - All numeric fields are fixed-8 decimal **strings**. - `null` means unknown/stale — `"0.00000000"` is never emitted for an unknown value. Rows whose source inputs (quote `as_of_ms` / oracle spot publish time) are older than the staleness window (default 30s) serve null fields while retaining their last timestamps. - `model_price` is a display-only theoretical value. It is NOT the margin/liquidation-authoritative mark and must never be used as a PnL/margin/liquidation price source. - `rho` is intentionally omitted (convention-dependent, unrendered). - Rows are sorted by `symbol`. Staleness never yields a 5xx — the endpoint returns 200 with null fields. - Adding response fields is backward-compatible; removing or renaming fields is a breaking change. # Get public market configuration Source: https://docs.joyride.exchange/api-reference/public/get-public-market-configuration /_generated/openapi.yaml get /v1/market/config # Get the current implied volatility for an instrument Source: https://docs.joyride.exchange/api-reference/public/get-the-current-implied-volatility-for-an-instrument /_generated/openapi.yaml get /v1/market/quotes/{symbol} Returns the latest validated BlockScholes mark IV for an open instrument. Quotes at least two minutes old are unavailable. This display-data window does not change the five-second trading risk gate. # Health check Source: https://docs.joyride.exchange/api-reference/public/health-check /_generated/openapi.yaml get /health # List available implied volatilities for open instruments Source: https://docs.joyride.exchange/api-reference/public/list-available-implied-volatilities-for-open-instruments /_generated/openapi.yaml get /v1/market/quotes Returns the latest validated BlockScholes mark IVs that are less than two minutes old. Missing or older quotes are omitted. # List public instrument metadata Source: https://docs.joyride.exchange/api-reference/public/list-public-instrument-metadata /_generated/openapi.yaml get /v1/market/instruments Returns only instruments whose lifecycle state is `open`. # Check read-model database connectivity Source: https://docs.joyride.exchange/api-reference/query/check-read-model-database-connectivity /_generated/openapi.yaml get /query/health # List account balances Source: https://docs.joyride.exchange/api-reference/query/list-account-balances /_generated/openapi.yaml get /query/balances # List accounts Source: https://docs.joyride.exchange/api-reference/query/list-accounts /_generated/openapi.yaml get /query/accounts # List completed withdrawal projection rows Source: https://docs.joyride.exchange/api-reference/query/list-completed-withdrawal-projection-rows /_generated/openapi.yaml get /query/withdrawals # List deposit projection rows Source: https://docs.joyride.exchange/api-reference/query/list-deposit-projection-rows /_generated/openapi.yaml get /query/deposits # List positions Source: https://docs.joyride.exchange/api-reference/query/list-positions /_generated/openapi.yaml get /query/positions # List RFQ fill history for the authenticated maker or taker Source: https://docs.joyride.exchange/api-reference/query/list-rfq-fill-history-for-the-authenticated-maker-or-taker /_generated/openapi.yaml get /query/rfq-fills # List withdrawal reservation lifecycle rows Source: https://docs.joyride.exchange/api-reference/query/list-withdrawal-reservation-lifecycle-rows /_generated/openapi.yaml get /query/withdrawal-requests # The caller's RFQ collateral, as the vault-watcher last projected it Source: https://docs.joyride.exchange/api-reference/query/the-callers-rfq-collateral-as-the-vault-watcher-last-projected-it /_generated/openapi.yaml get /query/vault-subaccount Zero or one row. A wallet that has never funded the RFQ side has no subaccount, and an empty `rows` array means exactly that: a real zero, not an error and not an outage. `free_balance` and `locked_total` are NULLABLE: a subaccount row can exist before its first projected balance, and a null there means UNKNOWN, never zero. Cursor pagination is not supported on this endpoint (`after` returns 400). The app reads the same account straight off the chain; this projection exists so a server-side consumer can see where a user's money is without an RPC round trip, and it lags the chain by the vault-watcher's projection delay. # Associate a referral code with the authenticated account Source: https://docs.joyride.exchange/api-reference/referrals/associate-a-referral-code-with-the-authenticated-account /_generated/openapi.yaml post /v1/referral/associate Records attribution for a referral code against the authenticated wallet. The request body contains only the referral code; wallet and account identity are resolved from the Bearer session by the platform service. The endpoint is idempotent per wallet: if the wallet already has a referral attribution, it returns `associated: true` with `already: true` and does not write a second redemption row. # Issue a single-use signup nonce Source: https://docs.joyride.exchange/api-reference/referrals/issue-a-single-use-signup-nonce /_generated/openapi.yaml post /v1/onboarding/nonce Issues a short-lived, single-use nonce for the signup flow. The client embeds the returned nonce in the purpose-bound signup message it signs, then submits that proof to `/v1/onboarding/signup`, which consumes the nonce exactly once. Nonces expire after `expires_in` seconds; expired, already-consumed, or client-generated nonces are rejected at signup with `401 NONCE_INVALID`. The request body is empty. # Provision a core account with a referral code Source: https://docs.joyride.exchange/api-reference/referrals/provision-a-core-account-with-a-referral-code /_generated/openapi.yaml post /v1/onboarding/signup Self-service signup for a new wallet. The caller first obtains a server-issued nonce from `/v1/onboarding/nonce`, signs the purpose-bound signup message (with the nonce embedded) with the wallet, and submits the referral code bound into that message. The platform service consumes the nonce (single use), allocates a durable account id, consumes the referral code, and submits core `account_create` with a scoped provisioner token. Signup never trusts Platform's durable allocation row as proof that core still has the account. It attempts a core lookup first; if core cannot confirm the account (for example after a reset), the request reuses the same durable `account_id` and replays the idempotent `account_create`. Existing referral attribution remains valid, so recovery does not require deleting or duplicating a referral redemption. Fresh accounts start unfunded. Devnet/Mainnet balances come from the vault/deposit path; there is no server-side paper funding. # Oracle Source: https://docs.joyride.exchange/api-reference/websockets/oracle Receive-only. Every frame carries an RFC 3339 `timestamp` (the envelope serialization time) and a `type` tag, with the payload fields flattened into the same object. Spot frames also carry Pyth's own `publish_time` so a consumer can measure freshness independently of transport latency. # Prices Source: https://docs.joyride.exchange/api-reference/websockets/prices Bidirectional. The server sends `connected` immediately after the upgrade. The client then sends `subscribe` with one or more `{ symbol, timeframe }` channels and receives `subscription_confirmed` followed by a `candle_update` whenever a subscribed candle changes (the service polls its store every 250 ms, so updates arrive at most four times a second per channel). Keepalive is mandatory: the server sends `ping` every 30 s and closes any socket that has not answered with `pong` before the next ping. A client may also send `ping` and will receive `pong`. # RFQ market-maker feed Source: https://docs.joyride.exchange/api-reference/websockets/rfq-market-maker-feed Role-gated `open` requests for every quoter plus the quoting maker's own `state`, `fill`, and `finality` events, delivered as `subscription` frames with `params.channel: rfq_maker` on the trading connection after `rfq.maker.subscribe`. The subscription is per socket and is not restored by `public/session_resume`; page `rfq.maker.poll` on every connect to recover requests opened while disconnected. # Taker RFQ lifecycle Source: https://docs.joyride.exchange/api-reference/websockets/taker-rfq-lifecycle Private `quote`, `state`, and `finality` events for the authenticated taker, delivered as `subscription` frames with `params.channel: rfq` on the trading connection after `rfq.subscribe`. # Trading rpc Source: https://docs.joyride.exchange/api-reference/websockets/tradingrpc # List withdrawal history Source: https://docs.joyride.exchange/api-reference/withdrawals/list-withdrawal-history /_generated/openapi.yaml get /v1/withdrawals Returns the authenticated account's newest withdrawal audit entries. # Reserve funds and return a signed withdrawal authorization bundle Source: https://docs.joyride.exchange/api-reference/withdrawals/reserve-funds-and-return-a-signed-withdrawal-authorization-bundle /_generated/openapi.yaml post /withdrawals # Chance of Profit Source: https://docs.joyride.exchange/exchange/chance-of-profit How Joyride calculates chance of profit for option markets Chance of profit, shown as **COP** in the trading app, estimates the probability that an option position finishes profitable at expiry. Joyride computes COP server-side and exposes it as `greeks.pop`. The value is a probability from `0.0` to `1.0`; the app formats it as a percentage. ## What COP Measures COP is based on the option's **break-even price**, not just whether the option expires in the money. For a long call: ```text theme={null} break_even = strike + option_mark COP = probability(expiry_price > break_even) ``` For a long put: ```text theme={null} break_even = strike - option_mark COP = probability(expiry_price < break_even) ``` This means COP includes the premium paid for the option. A call can expire in the money and still lose money if the expiry price does not clear `strike + option_mark`. ## Inputs The calculation uses: | Input | Meaning | | ------------------ | -------------------------------------------------- | | Spot price | Current underlying price | | Strike | Option strike price | | Time to expiry | Annualized time remaining until expiry | | Risk-free rate | Annualized rate used by the pricing model | | Implied volatility | Annualized IV from Joyride's volatility skew model | | Option mark | The option's mark price, used as the premium | The option's model price is the Black-76 value at the mark implied volatility, as described on [Pricing and Greeks](./risk-engine). ## Formula Joyride uses the same lognormal model as the Black-76 Greeks. First, it evaluates `d2` at the break-even price: ```text theme={null} d1 = (ln(spot / break_even) + (rate + volatility^2 / 2) * time) / (volatility * sqrt(time)) d2 = d1 - volatility * sqrt(time) ``` Then it converts `d2` into a probability with the standard normal cumulative distribution function: ```text theme={null} long_call_COP = N(d2) long_put_COP = N(-d2) ``` The result is clamped to `[0.0, 1.0]`. ## Long and Short Display The server value is always the **long-side** probability. When the app is showing the sell or short side, it displays the complement: ```text theme={null} short_side_COP = 1 - long_side_COP ``` ## Interpretation COP is a model estimate, not a guarantee. It depends on the current spot price, current mark, time remaining, and implied volatility assumption at the moment it is calculated. Use COP as one market signal alongside price, delta, IV, and your own view of the underlying. # Fees Source: https://docs.joyride.exchange/exchange/fees What a trade costs on Joyride and what is charged at expiry There are two fees. A trading fee is taken when a quote fills, and a settlement fee is taken at expiry from positions that finish in the money. | Fee | Rate | Charged to | | -------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------- | | Taker fee | 0.05% of the notional value, and never more than 12.5% of the option's price | the account that requested and accepted the quote, on each fill | | Maker fee | 0% at launch | the market maker whose quote filled | | Settlement fee | 0.015% of the notional value, capped at 12.5% of the payout | in-the-money positions at expiry, whichever side holds them | Notional value is the underlying's index price times the number of contracts, calculated when the trade executes. ## Trading fee The taker fee is 0.05% of the notional value, and never more than 12.5% of the option's price, so a cheap option never costs more than an eighth of its price in fees. On a multi-leg trade each leg is charged this way and the leg fees are added up. The maker side pays its own rate, which is 0% at launch. | Trade, BTC index at \$100,000 | Fee | | --------------------------------------------------------------------------------------- | ------------------------------------------ | | Buy one call for \$300 | \$37.50 (12.5% of \$300 is less than \$50) | | Buy one far out-of-the-money call for \$20 | \$2.50 | | Buy one deep in-the-money call for \$5,000 | \$50.00 (0.05% of \$100,000) | | Risk reversal, buy one call and sell one put, each leg worth \$300, net cost about \$10 | \$75.00 | A multi-leg trade is charged on each leg, so its fee can exceed 12.5% of the package's net price, as the risk reversal shows. The cap protects each leg, not the package total. ## Settlement fee At expiry an in-the-money position is paid its intrinsic value in USDC, and the settlement fee is taken from that payout. The fee is the lesser of 0.015% of the notional value and 12.5% of the payout. Out-of-the-money positions pay nothing, and a writer pays nothing at settlement. If an account holds both winning and losing positions on the same expiry, the fee is charged against the account's net result for that expiry, never against the winning positions alone. An account that nets a loss at expiry pays no settlement fee. ## Where fees appear Each fill shows the fee taken beside the premium, and each settlement shows the fee beside the gross payout, so the amount before fees and the fee itself are both visible in the app. Market makers can find how fee tiers are applied and collected on [Fees and Limits](/market-makers/fees-and-limits), and [Settlement](/market-makers/settlement) describes the expiry sequence in full. # Leverage & Lambda Source: https://docs.joyride.exchange/exchange/leverage How Joyride calculates the Leverage and Lambda (λ) columns in the options chain The options chain offers two related but different multiples, each as an optional column: **Leverage** and **Lambda (λ)**. Both are derived from the option's **model price**, the theoretical Black-76 value Joyride computes from the mark implied volatility. They answer different questions. Showing both under distinct names follows the convention used in retail warrant markets, where "gearing" and "effective gearing" are displayed side by side. ## Leverage **Leverage** is how many dollars of notional exposure each dollar of premium controls: ```text theme={null} leverage = spot_price / model_price ``` A leverage of `50×` means one dollar of premium controls fifty dollars of the underlying. Leverage is a **cost ratio, not a margin requirement**. Buying an option costs the full premium up front. There is no additional margin posted or borrowed against a long option. Because the denominator is the option's model price, cheap deep out-of-the-money options show very large leverage. A high number means the option is cheap relative to spot. It does not describe how the option's value will move. ## Lambda **Lambda** is a sensitivity measure: approximately how much the option's model value moves for a 1% move in the underlying: ```text theme={null} lambda = |delta| × spot_price / model_price ``` Lambda is Leverage scaled by the option's delta. Deep in-the-money options (delta near 1) have λ close to their leverage; deep out-of-the-money options (delta near 0) have λ far below their leverage, because most of a small underlying move does not reach the option's value. Near expiry, λ for far out-of-the-money strikes can grow very large while the probability of any payoff shrinks. A triple-digit λ is a signal that the option is a long shot, not a promise of amplified returns. ## Leverage vs Lambda | | Leverage | Lambda (λ) | | ----------------- | --------------------------------------- | -------------------------------- | | Formula | `spot / model_price` | `\|delta\| × spot / model_price` | | Answers | "How much exposure per premium dollar?" | "How much does my P\&L amplify?" | | Uses delta | No | Yes | | Deep OTM behavior | Very large | Large, but delta-damped | | Display | Raw | Raw | ## Inputs | Input | Source | | -------------- | --------------------------------------------- | | Spot price | Oracle index price | | Model price | Black-76 value at the mark implied volatility | | Delta (λ only) | Black-76 delta | The model price is a display-only theoretical value. It is not an executable quote and is not the mark used for margin or liquidation. ## Interpretation Neither number is a promise of returns, and neither describes margin. For a bought (long) option, loss is limited to the premium paid, Nothing else is posted or at risk. Selling (writing) an option is different: you post margin, and your loss is not capped at the premium, so neither Leverage nor Lambda reflects a seller's risk. Use Leverage to compare how much exposure your premium buys across strikes, and Lambda to compare how strongly each option's value reacts to the underlying. # Program Addresses Source: https://docs.joyride.exchange/exchange/programs The Solana program and accounts Joyride uses on Mainnet and Devnet Joyride custodies collateral in one Anchor program on Solana, the margin vault. Quotes bind to its deployment id, fills execute inside it against both parties' collateral, and expiry settlement is applied there. Every address below can be checked on any Solana explorer. Devnet is a full deployment with its own accounts, so an address is only valid on the cluster it is listed under. | | Mainnet | Devnet | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Program id | | [`4BecHK2cEh6DgfpCdxbQAm812audv4ZXxTQB12RkMWB8`](https://explorer.solana.com/address/4BecHK2cEh6DgfpCdxbQAm812audv4ZXxTQB12RkMWB8?cluster=devnet) | | Config account | | [`ECJGDQXPnzjoaaPveifT4FSKmYgSbNf1GeiHbtdi626u`](https://explorer.solana.com/address/ECJGDQXPnzjoaaPveifT4FSKmYgSbNf1GeiHbtdi626u?cluster=devnet) | | Deployment id | | `c791d865a52876a0360dc75a1a3feddd4c6fff396c60258daa3e6e377bd6bb19` | | USDC mint | [`EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`](https://explorer.solana.com/address/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v) | [`4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`](https://explorer.solana.com/address/4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU?cluster=devnet) | The deployment id is a hash that identifies one deployment of the program on one cluster; a signed quote for one deployment is rejected by every other. Market makers will find the byte-level identity a quote binds to on [Devnet Sandbox](/market-makers/sandbox). # Pricing and Greeks Source: https://docs.joyride.exchange/exchange/risk-engine How Joyride marks every instrument, what the Greeks and chance of profit mean, and how to read a stale value Joyride publishes a model price, a mark implied volatility, and per-unit Greeks for every listed instrument. The app's options chain, the [Chance of Profit](./chance-of-profit) and [Leverage](./leverage) columns, and the API all read the same values. ## Where the numbers come from The mark implied volatility comes from a continuously refreshed volatility surface for the underlying. The model price is the Black-76 value at that IV, with the forward equal to spot and a zero rate. Every price and Greek Joyride shows, on the chain, on the position cards, and on the charts, uses this same model and convention. Margin and liquidation do not use the model price. They use a separate mark price that arrives with the same price feed and is read directly by the risk engine. The two are usually close, since both come from the same volatility surface, but they are not the same number, and the mark price used for margin is not published. ## The fields | Field | Meaning | | ---------------- | -------------------------------------------------------------------- | | Mark IV | The implied volatility the mark is derived from | | Model price | Theoretical option price in USDC at the mark IV | | Delta | Change in option value per unit move in the underlying, from -1 to 1 | | Gamma | Change in delta per unit move in the underlying | | Theta | Change in option value per day, negative for a long position | | Vega | Change in option value per one-point change in IV | | Chance of profit | Probability that a long position is in the money at expiry | ## Option price charts The price chart on an instrument's screen is a modeled series, not a record of past trades or past marks. Each point is the Black-76 value of the option at that moment's spot price, the strike, the time then remaining to expiry, and the instrument's current mark IV. Spot history comes from the underlying's price feed; the IV is today's, applied across the whole series. The line therefore shows how the option's value would have tracked the underlying at the current volatility, and it is recomputed when the mark IV moves. Until an instrument has a mark IV, the chart falls back to the midpoint of any quotes it has received, or stays empty. # Trading Hours and Rollover Source: https://docs.joyride.exchange/exchange/trading-hours The 24-hour round, the 08:00 UTC settlement, the TWAP window, and what changes at rollover Joyride trades around the clock in rounds of 24 hours. Every round settles at 08:00 UTC. All times on Joyride are UTC. ## The round | | | | ------------ | --------------------------------------------------- | | Round length | 24 hours | | Settlement | 08:00 UTC on the expiry date in the instrument name | | TWAP window | Opens 30 minutes before settlement (at 07:30 UTC) | | Listing | New instruments at rollover post-settlement | For example, `SOL_USDC-16MAR20-300-C` settles at 08:00 UTC on 16 March 2020. ## Settlement price The settlement price is a time-weighted average of the underlying's oracle price over the 30 minutes before settlement. One settlement price per underlying is shared by every instrument on that expiry. [Settlement](/market-makers/settlement) describes the full sequence. ## Rollover At 08:00 UTC the expiring round closes and the next one opens: * Expiring instruments stop trading. Requests for quotes on them are rejected. * Positions on the expiring round are settled in USDC against the settlement price. * The next round's instruments are created and announced. The tradable instrument set changes at that moment, so anything that holds instrument names, whether a bot, an agent, or a person with a chart open, should refresh its list around 08:00 UTC rather than assume yesterday's names are still live. For builders, new listings are visible through `public/get_instruments` on the trading WebSocket and `GET /api/v1/market/instruments`, and settlement results are read from `GET /api/query/rfq-fills`. # Getting Started Source: https://docs.joyride.exchange/getting-started Endpoints, authentication, and the reading order for building a bot, integration, or frontend on Joyride. ## No API key required Authentication is Sign-In with Solana: the wallet signs a server nonce and receives a session token. Accounts are created in the app at [joyride.exchange](https://joyride.exchange), then the same wallet signs in over the API. A wallet with no account is rejected at sign-in with an error naming the sign-up step. ## Choose your path ### I want to build a custom trading bot Use the HTTP and WebSocket APIs directly in any language. A bot trades by requesting a quote on a package of legs with `rfq.create`, following the request on the `rfq` channel after `rfq.subscribe`, and accepting one side of the winning quote with `rfq.accept`; the fill lands on chain. * [Connectivity for Bots](/market-makers/connectivity) covers the SIWS flow, sessions, rate budgets, and the JSON-RPC WebSocket protocol * [Quoting RFQs](/market-makers/rfq-quoting) covers the request, quote, and fill objects and the RFQ error codes * The REST API and WebSocket API references in this tab are the full spec No CLI or npm package is required, only HTTP, WebSockets, and a Solana keypair. ### I want to build a custom frontend or integration Use the hosted endpoints and exchange docs below. ## Hosted endpoints Every surface derives from the single public origin `https://joyride.exchange`: | Surface | URL | Example | | ---------------------- | ------------------------------------------ | ---------------------------------------------------------- | | Trading WebSocket | `wss://joyride.exchange/api/client` | JSON-RPC 2.0 (SIWS auth, instruments, RFQ) | | Oracle WebSocket | `wss://joyride.exchange/api/oracle` | Spot/TWAP price feed | | Public HTTP | `https://joyride.exchange/api/v1/market/*` | `curl .../api/v1/market/instruments` | | Query API (Bearer) | `https://joyride.exchange/api/query/*` | `curl -H "Authorization: Bearer …" .../api/query/balances` | | Platform HTTP (Bearer) | `https://joyride.exchange/api/v1/*` | greeks, deposits and withdrawals, onboarding | All integrations use the single `joyride.exchange` origin above. ## Recommended reading order for API integrators 1. [Overview](/) 2. [Trading Hours and Rollover](./exchange/trading-hours) 3. [Connectivity for Bots](/market-makers/connectivity) 4. [Quoting RFQs](/market-makers/rfq-quoting) 5. REST API reference 6. WebSocket API reference 7. [Pricing and Greeks](./exchange/risk-engine) # Joyride Docs Source: https://docs.joyride.exchange/index Documentation for trading, market making, and building with Joyride's 0DTE options exchange Joyride is a non-custodial, cash-settled options exchange on Solana with on-chain trade execution. It lists BTC daily-expiry calls and puts with other underlying assets coming soon. Every contract settles automatically in USDC at 08:00 UTC, and every account is a Solana wallet. There are no API keys. ## Contract specifications | | | | ---------------- | ----------------------------------------------------------------------- | | Instruments | European calls and puts | | Underlyings | BTC (others coming soon) | | Strike currency | USDC | | Contract size | 1 unit of the underlying | | Expiry | Daily at 08:00 UTC | | Settlement price | 30-minute TWAP of the oracle price into expiry | | Payout | intrinsic value in USDC to in-the-money positions; no physical delivery | Instrument names follow one pattern: ``` {ASSET}_USDC-{DMMMYY}-{STRIKE}-{C|P} ``` For example, `BTC_USDC-16MAR20-100000-C` breaks down as: | Token | Value | Meaning | | -------------- | ---------- | ------------------------------------------------------------ | | `{ASSET}_USDC` | `BTC_USDC` | the underlying, quoted and settled in USDC | | `{DMMMYY}` | `16MAR20` | expiry day, month, and year; the day carries no leading zero | | `{STRIKE}` | `100000` | strike in whole USDC | | `{C\|P}` | `C` | call or put | ## Trading schedule Joyride trades 24/7 in 24-hour rounds with settlement at 08:00 UTC. [Trading Hours and Rollover](./exchange/trading-hours) covers the timing contract, the TWAP window, and what happens at rollover. ## Trading Every trade on Joyride is currently a request for quote (RFQ). A trader names a package of one to thirteen legs on one underlying and one expiry, in the app or over the API. Market makers holding the quoter role receive the request and answer with a signed two-sided quote that is firm until it expires. The taker accepts the bid or the ask and the fill lands in one on-chain transaction against both parties' collateral in the margin vault. Every leg fills or none does. Positions settle at expiry against the settlement price above. ## Pricing data Mark prices, mark IV, and Greeks for every instrument are served from the same pricing model the app uses. [Risk Engine](./exchange/risk-engine) lists the fields and how to read them. ## Access Accounts are created in the app at [joyride.exchange](https://joyride.exchange/trade). The same wallet signs in on every other surface. # Becoming an RFQ Quoter Source: https://docs.joyride.exchange/market-makers/becoming-a-quoter What Joyride provisions for an RFQ quoter, the readiness checklist, and the obligations of a quoter ## What onboarding produces Quoting RFQs is a provisioned role; no API call enables it. | Provisioned | Observable outcome | | ----------------------------------------- | ----------------------------------------------------------------------------------------- | | An account | `public/auth` succeeds for your wallet and returns an `account_id` | | The RFQ quoter role | `roles` in the `public/auth` response contains `rfq_quoter` | | An on-chain RFQ subaccount, reconciled | `GET /api/query/vault-subaccount` returns 1 row for your account | | Strategy quoting enabled for your account | `rfq.maker.poll` returns a page without `RFQ_NOT_READY`, and `open` events carry `legs[]` | | Required from the maker | Used for | | -------------------------------------------- | ------------------------------------------------ | | The ed25519 public key of the signing wallet | Account binding and quote signature verification | | A contact address | Provisioning confirmation and incident traffic | The wallet key is the credential; there are no API keys. The quoter role is bound to that wallet, so binding a new key goes through Joyride. ## Readiness checklist Run these four checks in order on one authenticated WebSocket session. Each depends on the one before it. Request and response frames are on [Connectivity for Bots](/market-makers/connectivity) and [Quoting RFQs](/market-makers/rfq-quoting). 1. **Role.** `public/auth` returns `roles` containing `rfq_quoter`. An empty list means the account exists without the role, and every `rfq.maker.*` method returns `RFQ_WRONG_ROLE`. 2. **Subscription.** `rfq.maker.subscribe` returns `subscribed: true`. 3. **Subaccount.** `GET /api/query/vault-subaccount`, with the session token as bearer, returns one row for the account. 4. **Readiness.** `rfq.maker.poll` returns a page instead of `RFQ_NOT_READY`. The checklist passes when step 4 returns a page. A session that has not passed must not quote. ## Reading RFQ\_NOT\_READY `RFQ_NOT_READY` is one error code with five causes, told apart by the `message` field. All five are `retryable: true`, but only two clear on their own. | Cause | Symptom | Resolved by | | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | No reconciled subaccount | `authenticated account has no verified on-chain RFQ subaccount` on `rfq.maker.poll` or `rfq.maker.respond`; `GET /api/query/vault-subaccount` returns an empty `rows` list | Joyride. Reconciliation is part of provisioning; if the row has not appeared, email [support@joyride.exchange](mailto:support@joyride.exchange) | | Chain service still warming | `RFQ chain-service state is not warm` on `rfq.maker.poll` or `rfq.maker.respond`, typically for a few seconds after a gateway restart | Nobody. Retry with backoff; the warm-up completes on its own. Escalate if it persists past 60 seconds | | No ready deployment epoch | `RFQ deployment snapshot is not ready` or `RFQ deployment is not ready` on `rfq.maker.poll` or `rfq.maker.respond`, and `open` events stop arriving | Joyride. The deployment identity is ours to publish; retry with backoff and escalate if it persists past 60 seconds | | Stale mark, IV, or spot inputs | `mark/IV/spot for this instrument or the account's positions are not fresh` on `rfq.maker.respond` only; `rfq.maker.poll` and `rfq.maker.decline` still succeed | Nobody. Retry the respond inside the request window; if it does not clear, let the request time out or decline with `stale_data` | | Your maker subaccount not yet registered with the fill relayer | `maker subaccount is not resident in the fill lookup table` on `rfq.maker.respond` only; subscribe and poll succeed and `open` events arrive | Joyride. Registration is part of provisioning; report it when steps 1 through 4 pass and every respond fails this way | `retryable: true` means the request was well formed and may succeed later. For the three Joyride-owned causes, a retry alone will not clear it. Email [support@joyride.exchange](mailto:support@joyride.exchange) if one of those persists. ## Obligations * A delivered quote is firm until its `expires_at`, with no cancel and no replace. Choose a TTL that hedging can honor, since the quote stands even if prices move. * Never call the vault's `cancel_nonce` for a nonce once its RFQ is `accepted`. * Reconcile from durable history: `GET /api/query/rfq-fills?role=maker` is the record of fills, and open requests are purged at settlement. ## Capital Collateral is agreed per maker during onboarding. The on-chain subaccount holds the USDC that backs fills. The amount, the deposit path, and any staged increase are not published. Devnet uses a fixed cap described on [Devnet Sandbox](/market-makers/sandbox). ## Contact Provisioning, role changes, wallet rotation, and every Joyride-owned cause in the table above go through [support@joyride.exchange](mailto:support@joyride.exchange). Accounts that are not yet provisioned start from [Market Making on Joyride](/market-makers/overview). Next: [Quoting RFQs](/market-makers/rfq-quoting) # Connectivity for Bots Source: https://docs.joyride.exchange/market-makers/connectivity Endpoints, wallet-signed authentication, session limits, keepalive, rate budgets, and error codes for the trading WebSocket ## Endpoints Everything is served from the public origin `https://joyride.exchange`, with no separate API host. | Surface | URL | Auth | | -------------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------- | | Trading WebSocket (RFQ) | `wss://joyride.exchange/api/client` | SIWS on the socket (`public/auth` or `public/session_resume`) | | Oracle WebSocket (spot price) | `wss://joyride.exchange/api/oracle` | None | | Query HTTP (self-scoped reads: balances, deposits, withdrawals, RFQ fills, vault subaccount) | `https://joyride.exchange/api/query/*` | `Authorization: Bearer ` | | Public market HTTP (config, instruments, mark IV) | `https://joyride.exchange/api/v1/market/*` | None | The trading WebSocket speaks JSON-RPC 2.0. Every request carries `"jsonrpc": "2.0"`, an `id`, a `method`, and optional `params`; every response echoes the `id`. Server pushes arrive as `"method": "subscription"` notifications with no `id`. ## Authentication Identity is an ed25519 keypair. There are no API keys. An account is provisioned against a Solana wallet public key (see the venue pages linked at the end), and that keypair signs in from a bot, the CLI, or the web app. Sign-in uses three methods on the trading WebSocket. | Method | Request | Response | Errors | | ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `public/get_nonce` | `{ "wallet": "" }` | `{ "nonce": "" }`, single use, valid for 300 seconds | `-32602` invalid params | | `public/auth` | `{ "wallet", "signature": "", "message" }` | `{ "wallet", "account_id", "roles", "session_token" }` | `1001` bad signature, message mismatch, nonce expired or reused, unknown account | | `public/session_resume` | `{ "session_token": "" }` | `{ "wallet", "account_id", "roles" }`; the socket is now authenticated and no new token is issued | `1001` token expired or invalid, unknown or reassigned account | ### Request a nonce ```json theme={null} {"jsonrpc":"2.0","id":1,"method":"public/get_nonce","params":{"wallet":"7Gx2XvMk9Z1sZbY3qJ8pKcWnD4tFhRr6sA2eLmN5uVwQ"}} ``` ```json theme={null} {"jsonrpc":"2.0","id":1,"result":{"nonce":"3fA9kLm2PqRs7TuVwXyZ1bCdEfGh4JkL"}} ``` ### Sign and authenticate The message is built byte for byte as below: a blank line between the greeting and the `Wallet:` line, a single newline before `Nonce:`. ```text theme={null} Sign in to Joyride Wallet: {wallet} Nonce: {nonce} ``` The signature is ed25519 over the UTF-8 bytes of that message, base58-encoded (64 bytes). The request carries the wallet, the signature, and the exact message signed. The server rebuilds the message from `wallet` and the nonce inside `message` and rejects any deviation, including a trailing newline or reordered lines. ```json theme={null} {"jsonrpc":"2.0","id":2,"method":"public/auth","params":{"wallet":"7Gx2XvMk9Z1sZbY3qJ8pKcWnD4tFhRr6sA2eLmN5uVwQ","signature":"","message":"Sign in to Joyride\n\nWallet: 7Gx2XvMk9Z1sZbY3qJ8pKcWnD4tFhRr6sA2eLmN5uVwQ\nNonce: 3fA9kLm2PqRs7TuVwXyZ1bCdEfGh4JkL"}} ``` ```json theme={null} {"jsonrpc":"2.0","id":2,"result":{"wallet":"7Gx2XvMk9Z1sZbY3qJ8pKcWnD4tFhRr6sA2eLmN5uVwQ","account_id":1042,"roles":["rfq_quoter"],"session_token":""}} ``` `roles` lists what the account may do. A quoter's array contains `rfq_quoter`; a taker's is empty. A wallet with no provisioned account is rejected with `1001`. Signing in never creates an account. ### Resume on reconnect A stored `session_token` authenticates a new socket without a fresh signature. Resume checks that the wallet still owns the `account_id` in the token and recomputes `roles` from live account state, so a role change takes effect on the next resume. ```json theme={null} {"jsonrpc":"2.0","id":3,"method":"public/session_resume","params":{"session_token":""}} ``` ```json theme={null} {"jsonrpc":"2.0","id":3,"result":{"wallet":"7Gx2XvMk9Z1sZbY3qJ8pKcWnD4tFhRr6sA2eLmN5uVwQ","account_id":1042,"roles":["rfq_quoter"]}} ``` A `1001` on resume means the cached token is no longer valid; the full nonce, sign, auth sequence is required. `public/session_resume` on a socket already authenticated as the same account refreshes that session and evicts nothing. ## Session token | Parameter | Value | Applies to | | ------------ | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------- | | Format | JWT, HS256, issued by the trading gateway | `session_token` | | Lifetime | 24 hours from issue (`exp` claim) | `session_token` TTL | | Revocation | None; a token stays valid until `exp` | `session_token` | | Invalidation | Rotate the wallet key: a token whose wallet no longer maps to the account fails `public/session_resume` and Bearer reads | `session_token` | | Storage | Keep the ed25519 secret and the token in a secrets manager, never in a repo, image, or log line | Wallet keypair, `session_token` | The session token is the JWT that `public/auth` returns after the wallet signs the nonce. There is no revocation list, so a leaked token can trade the account until its `exp`. Joyride never holds a maker's private key, so the remedy is on the maker's side: generate a new keypair and email [support@joyride.exchange](mailto:support@joyride.exchange) to bind the account to the new public key. Once the account is rebound, tokens issued to the old key are refused on `public/session_resume`, and any connection still open under one ends when it disconnects. ## Sessions per account | Parameter | Value | Applies to | | --------------------------------- | ------------------------------------------------------------------------------ | ----------------- | | Concurrent authenticated sessions | 3 per account | Trading WebSocket | | Eviction on a 4th session | Oldest session closed with WebSocket close code `4001`, reason `session_limit` | Trading WebSocket | | Delivery of account pushes | Every session receives every RFQ event for the account | Trading WebSocket | Every session receives every account-scoped push, so a bot needs one authenticated socket. A client closed with `4001` and reason `session_limit` must not reconnect automatically: the reconnect evicts the newer session and the two clients evict each other in a loop. Reconnect only on an operator action. ## Keepalive | Parameter | Value | Applies to | | ------------------------- | ----------------------------------- | ----------------- | | Server-initiated ping | Never | Trading WebSocket | | Unauthenticated idle reap | 60 seconds without an inbound frame | Trading WebSocket | The server never pings. The client sends a protocol-level WebSocket ping at least every 30 seconds to keep intermediaries from dropping a quiet flow, and the gateway answers each with a pong. An unauthenticated socket that sends nothing for 60 seconds is reaped with error `1001`. ## Rate budget | Parameter | Value | Applies to | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | Request budget | 100 requests per second per account | Every request the account sends over its trading sessions, counted together | | Over budget | Error `1200` with `data.error` `RFQ_RATE_LIMITED` for `rfq.maker.*` methods, error `1007` for other methods; both `retryable: true` | The rejected request only; the socket stays open | The budget is per account, so three sessions share it. Each request counts as one, whether it is a poll, a response, or a decline. ```json theme={null} {"jsonrpc":"2.0","id":4,"error":{"code":1007,"message":"Rate limit exceeded","data":{"retryable":true}}} ``` The window resets at the next second. The rejected request was never admitted and can be resent as is. ## Error codes Errors use the JSON-RPC envelope `{ "code", "message", "data"? }`. Close codes are WebSocket close frames, not JSON-RPC errors. | Code | Surface | Meaning | Client action | | -------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | | `1001` | Trading WebSocket | Not authenticated: bad signature, expired nonce, expired or invalid token, unknown or reassigned account, or the 60-second idle reap | Discard any cached token, run `public/get_nonce` then `public/auth`, and resend | | `1007` | Trading WebSocket | Rate limit exceeded; the 100 requests per second account budget is spent | Retry after backoff; nothing was admitted | | `1200` | Trading WebSocket | RFQ error; `data.error` carries the RFQ code (for example `RFQ_NOT_READY`, or `RFQ_RATE_LIMITED` when an `rfq.maker.*` call exceeds the same budget), plus `operation_id`, `retryable`, and `rfq_id` or `quote_id` where known | Branch on `data.error`; retry only when `data.retryable` is `true` | | `4001` (close) | Trading WebSocket, reason `session_limit` | A fourth session evicted this one | Do not auto-reconnect; alert and wait for an operator | The `1200` frame: ```json theme={null} {"jsonrpc":"2.0","id":6,"error":{"code":1200,"message":"maker is not ready to quote","data":{"error":"RFQ_NOT_READY","message":"maker is not ready to quote","operation_id":"op_01J9X4M2QK","retryable":true,"rfq_id":"0192f1a3b4c5d6e7f8091a2b3c4d5e6f"}}} ``` The standard JSON-RPC codes (`-32700` parse, `-32600` invalid request, `-32601` method not found, `-32602` invalid params, `-32603` internal) follow the JSON-RPC 2.0 specification. An unknown instrument symbol in a request is rejected as `-32602`. ## Support Support for live makers is [support@joyride.exchange](mailto:support@joyride.exchange). A report that includes the `operation_id` from a `1200` error, the `rfq_id` or `quote_id` in question, and the UTC time of the event can be traced directly. Next: [Becoming an RFQ Quoter](/market-makers/becoming-a-quoter) # Fees and Limits Source: https://docs.joyride.exchange/market-makers/fees-and-limits The fee tier applied to a market-maker account, how each fee is collected, and the limits the gateway enforces ## Fees Every account references one fee tier. The values below are the launch tier for a market-maker account. An account with different commercial terms is attached to a different tier. Fill records carry the fees actually charged. | Parameter | Value | Applies to | | ------------------------ | ------------------------------------------------- | ------------------------------------------- | | `maker_fee_bps` | 0 basis points of index notional | the maker side of a fill, per leg | | `taker_fee_bps` | 5 basis points of index notional (0.05 %) | the taker side of a fill, per leg | | `trade_fee_cap_bps` | 1250 basis points of leg value (12.5 %) | the upper bound on the trading fee, per leg | | `settlement_fee_ppm` | 150 parts per million of index notional (0.015 %) | the in-the-money holder at expiry | | `settlement_fee_cap_bps` | 1250 basis points of intrinsic value (12.5 %) | the upper bound on the settlement fee | | rebate | none | no side, no volume threshold | Trading-fee rates are basis points of index notional, and the trading-fee cap is basis points of leg value. The settlement rate is parts per million because it is smaller than one basis point. The settlement price and expiry sequence the settlement fee is charged against are on [Settlement](/market-makers/settlement). ## How fees are charged There are two fees. A trading fee is charged when a fill executes, to each side at its own tier's rate. A settlement fee is charged at expiry to the holder of an in-the-money position. The writer pays nothing at settlement. The trading fee is computed per leg and summed over the package: ``` fee = Σ over legs min( index_notional × rate_bps / 10000, leg_value × trade_fee_cap_bps / 10000 ) ``` `index_notional` is the underlying's index price at execution times the leg quantity. `leg_value` is the leg's premium: the package premium for a one-leg package, and the leg's mark value for a multi-leg package. There is no combo discount, so a two-leg structure pays on both legs. Both sides pay their own rate, and the maker rate is 0 at launch. Fees are collected on chain in the fill transaction and reported as `maker_fee` on the fill. The settlement fee is the smaller of 0.015 % of the index notional and 12.5 % of the intrinsic value, charged to in-the-money holders and netted per account for the expiry, so it never exceeds what the settlement pays. The formula and the expiry sequence are on [Settlement](/market-makers/settlement). There is no rebate at any volume. ## Limits The gateway enforces these bounds. A request over a limit is rejected or the connection is closed; the gateway does not queue it. | Parameter | Value | Applies to | | -------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Rate budget | 100 requests per second | every request an account sends over its trading sessions, counted together | | Sessions per account | 3 sessions | authenticated trading WebSocket connections; a fourth evicts the oldest with close code 4001, and a client that receives that code must not reconnect automatically | | Legs per package | 13 legs | one RFQ package, whether a single option or a multi-leg strategy | | Quote TTL maximum | 3600 seconds | `expires_at − issued_at` on a quote you sign; there is no fixed minimum, but a quote is refused if 2 seconds or less of life remain when it lands | | Request window | 5 seconds | how long an RFQ stays open for quotes after the taker creates it | | Submission window | 30 seconds | how long after the taker accepts your quote the fill must be submitted on chain, cut short by your quote's own expiry if that comes first | The rate budget is a fixed window rather than a refilling bucket. The counter opens on the first request and resets once a full window has elapsed since it opened, so a burst that exhausts the budget is refused until the window turns over. The sessions limit bounds what a stuck client can cost the gateway, since every private push to an account is fanned out to every open session. Next: [Settlement](/market-makers/settlement) # Market Making on Joyride Source: https://docs.joyride.exchange/market-makers/overview How the RFQ venue works, what a market maker commits to when it quotes, and how onboarding starts Market makers on Joyride answer requests for quotes. A taker names a package of one or more option legs, a maker prices the whole package as one signed firm bid and ask, then the accepted side fills on-chain against both parties' collateral. Joyride provisions the account and the quoter role. The contract terms and instrument naming can be found in the [Exchange Overview](/). This page covers RFQ execution, the quoter role, and onboarding. ## Summary | Parameter | Value | | ---------------- | ----------------------------------------------------------------------------------- | | Legs per request | 1 to 13 on a single underlying and one expiry | | Quote firmness | A delivered quote is firm until its expiry; no cancel, no replace | | Fills | One on-chain transaction for the entire package, all legs or none | | Fees | Maker and taker fees by tier, see [Fees and Limits](/market-makers/fees-and-limits) | A sandbox with the same protocol and payloads runs on Solana Devnet at [devnet.joyride.exchange](/market-makers/sandbox). ## How an RFQ works 1. A taker sends a request naming its legs. Each leg is one instrument, one side, and one quantity. All legs share one underlying and one expiry. 2. The venue publishes the request as an `open` event on the `rfq_maker` channel of the trading WebSocket. Every quoter subscribed to that channel receives it on its own connection and prices the package inside the request window. 3. Quoters respond with a signed two-sided quote, a bid and an ask for the whole package, that is binding until the expiry the quoter set, at most one hour later. 4. The taker accepts one side of the quote, bid or ask, for the whole package. 5. The fill executes on-chain in one transaction against both parties' collateral. Every leg lands or none does. The resulting positions settle at expiry against the settlement price. A spread or a butterfly is never legged: the fill moves collateral and positions for every leg atomically. ## Who quotes Quoting requires a provisioned account and the quoter role. Joyride creates the account with its margin policy and fee tier, and provisions the role together with the maker's on-chain subaccount during onboarding. Once the subaccount is funded, the maker authenticates with its wallet and starts receiving requests. ## Timing | Window | Value | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Request window | A request stays open for quotes for 5 seconds after the taker creates it | | Quote lifetime | Set by the quoter on each quote, up to 3,600 seconds. There is no fixed minimum, but a quote must reach the venue with more than 2 seconds of life left or it is refused | | Acceptance | The taker can accept until 2 seconds before the quote expires | | Submission window | The fill must land on chain within 30 seconds of acceptance, or 2 seconds before the quote expires, whichever comes first | ## What is not supported * No quote cancel or replace. A delivered quote is firm until its expiry. The quote TTL is the only bound on exposure. * No notification to losing quoters. The first firm quote wins the request; later quotes receive no message and expire on their own. Makers need a local expiry timer per quote to release reserved risk. * No partial fills. A package is all-or-nothing: every leg fills in one transaction or none does, and a quote fills for its full quantity or not at all. * No cross-asset or calendar packages. A request carries between one and thirteen legs on one underlying and one expiry. * No API keys. Every session opens with a wallet signature. ## Next steps Onboarding starts by email to [support@joyride.exchange](mailto:support@joyride.exchange). Next: [Connectivity for Bots](/market-makers/connectivity) # The Signed Quote Source: https://docs.joyride.exchange/market-makers/quote-payload What a maker signs when it quotes an RFQ and how the venue checks the signature Every quote carries an Ed25519 signature by the maker's wallet key. The maker signs the quote's contents and sends only the signature, as `maker_signature` in `rfq.maker.respond`, together with the fields in the respond frame. The venue rebuilds the same contents from the open request and that frame, checks the signature against them, and reports a `quote_id` that identifies the signed quote in every later message. ## What the signature covers | Field | Where the value comes from | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Deployment | the `deployment` object on the open request: program id and deployment id. A quote signed for one deployment is rejected on every other | | Request | the `rfq_id` being answered | | Maker subaccount | the maker's margin-vault subaccount, returned by `GET /api/query/vault-subaccount` | | Taker subaccount | `taker_subaccount` on the open request | | Legs | the request's legs, in the order the taker submitted them | | Prices | `bid` and `ask` in the frame, one price each for the whole package | | Nonce | `quote_nonce` in the frame | | Times | `issued_at` and `expires_at` in the frame | The program ids and config accounts for each cluster are on [Program Addresses](/exchange/programs). ## Nonce and expiry `quote_nonce` must be higher than every nonce the account has used before, across restarts. `expires_at` can be at most one hour after `issued_at`, and a quote must reach the venue with more than 2 seconds of life left. The full rules are under Responding on [Quoting RFQs](/market-makers/rfq-quoting#responding). ## When a signature is refused * `RFQ_INVALID_SIGNATURE`: the signature does not verify over the contents the venue rebuilt. Usually the maker serialized a field differently, or signed with a key other than the wallet that owns the maker subaccount. * `RFQ_CANONICAL_MISMATCH`: the contents decoded, but a field differs from the venue's record of the request, such as the deployment, the legs, or a subaccount that does not belong to the signing wallet. Resending the same frame produces the same result. ## Serialization The byte-level format of the signed quote, with test vectors and a reference signer, is shared with market makers during onboarding. Email [support@joyride.exchange](mailto:support@joyride.exchange). Next: [Devnet Sandbox](/market-makers/sandbox) # Quoting RFQs Source: https://docs.joyride.exchange/market-makers/rfq-quoting The maker RFQ methods and pushes, from receiving a request and pricing the package as one signed bid and ask, to responding or declining inside the window and reconciling fills after a reconnect An RFQ is a package of one to thirteen option legs on one underlying and one expiry. The taker submits the legs. A quoter prices the package as one signed `bid` and one signed `ask`. The first firm quote wins, and the taker accepts one side of it. This page covers the maker methods and pushes. It assumes the quoter role and the readiness checks on [Becoming an RFQ Quoter](/market-makers/becoming-a-quoter). Session mechanics are on [Connectivity for Bots](/market-makers/connectivity); what the signature covers is on [The Signed Quote](/market-makers/quote-payload). Full schemas for every method and push are in the WebSocket API reference under Builders. ## Summary | Parameter | Value | Applies to | | ------------------ | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Request window | 5 seconds from creation | how long a request accepts quotes; `request_deadline` on the `open` object | | Quote TTL maximum | 3600 seconds, bounded by the nearest leg close | `expires_at - issued_at` on `rfq.maker.respond`; `max_quote_expires_at` on the `open` object is the hard ceiling | | Landing haircut | 2 seconds | a quote is refused if fewer than 2 seconds of life remain when it lands, and the accept and submission deadlines are clamped 2 seconds before `expires_at` | | Submission window | 30 seconds after accept | time allowed to submit the fill on chain; shortened if the quote expires first | | Legs per request | 1 to 13 legs, one underlying, one expiry | `legs[]` on every request | | Quotes per request | 1 quote per request; the first firm quote wins | `rfq.maker.respond` | | Decimal precision | 6 decimals, decimal string | every `bid`, `ask`, `quantity`, `premium`, and `maker_fee` | | Quote nonce | unsigned 64-bit integer as a decimal string, strictly increasing per account | `quote_nonce` on `rfq.maker.respond` | | Poll page size | 1 to 100 items, default 100 | `limit` on `rfq.maker.poll` | ## Lifecycle A request is first visible as `pending`. States after `quoted` are delivered only to the maker whose quote won. | State | Meaning | What the maker receives | | ------------ | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pending` | The request is open and no quote has been accepted by the venue yet | an `open` push, or an item in a `rfq.maker.poll` page | | `quoted` | The maker's quote won and is firm until `expires_at` | the `rfq.maker.respond` result with `quote_id` and `state: "quoted"` | | `accepted` | The taker accepted one side of the maker's quote | a `state` push with `state: "accepted"` and `accepted_side` set | | `submitting` | The fill transaction is being submitted on chain | a `state` push with `state: "submitting"` | | `filled` | The fill landed on chain | a `fill` push carrying the fill object, then a `finality` push when the fill is finalized or reverted | | `declined` | A quoter declined it, which ends the request for everyone, or the request window closed with no quote from anyone | a `state` push with `state: "declined"` if this maker declined; a broadcast `state` push with `quote_id: null` when nobody quoted; nothing if another quoter declined | | `expired` | The winning quote reached `expires_at` without an accept | a `state` push with `state: "expired"` | | `failed` | The fill was attested or submitted and did not land | a `state` push with `state: "failed"` | After another quoter wins, the request is no longer visible. A late `rfq.maker.respond` returns `RFQ_STATE_CONFLICT`, and no push reports the outcome. ## Connecting The maker channel is per socket. On every connection, including a resume with `public/session_resume`, call `rfq.maker.subscribe`, then page `rfq.maker.poll` until a result has no `next_cursor`. Subscribing first closes the gap in which a request could open unseen. Poll items and `open` pushes have the same shape and overlap in time, so deduplicate on `rfq_id`. An error from either method means the connection is not usable; reconnect with backoff. | Method | Request | Response | Errors | | --------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `rfq.maker.subscribe` | `{}` | `{channel: "rfq_maker", subscribed: true}` | `1200` `RFQ_WRONG_ROLE` | | `rfq.maker.poll` | `cursor?` (an `rfq_id`), `limit?` 1 to 100 | `{items: [open object, ...], next_cursor?}` | `1200` `RFQ_NOT_READY`, `RFQ_WRONG_ROLE`, `RFQ_RATE_LIMITED` | | `rfq.maker.respond` | `rfq_id`, `quote_nonce`, `bid`, `ask`, `issued_at`, `expires_at`, `maker_signature` | `{rfq_id, quote_id, state: "quoted"}` | `1200` `RFQ_NOT_READY`, `RFQ_WRONG_ROLE`, `RFQ_STATE_CONFLICT`, `RFQ_CANONICAL_MISMATCH`, `RFQ_INVALID_SIGNATURE`, `RFQ_QUOTE_EXPIRED`, `RFQ_VALUE_OUT_OF_RANGE`, `RFQ_PRECISION_UNSUPPORTED`, `RFQ_INVALID_INSTRUMENT`, `RFQ_NOT_FOUND`, `RFQ_RATE_LIMITED` | | `rfq.maker.decline` | `rfq_id`, `reason` | `{rfq_id, state: "declined", decline_reason}` | `1200` `RFQ_WRONG_ROLE`, `RFQ_STATE_CONFLICT`, `RFQ_NOT_FOUND`, `RFQ_RATE_LIMITED` | All four methods count against the 100 requests per second account budget on [Connectivity for Bots](/market-makers/connectivity). ```json theme={null} { "jsonrpc": "2.0", "id": 1, "method": "rfq.maker.subscribe", "params": {} } ``` ```json theme={null} { "jsonrpc": "2.0", "id": 1, "result": { "channel": "rfq_maker", "subscribed": true } } ``` The first page. `items` lists every request still open for quotes. `next_cursor` is present when another page follows. ```json theme={null} { "jsonrpc": "2.0", "id": 2, "method": "rfq.maker.poll", "params": { "limit": 100 } } ``` ```json theme={null} { "jsonrpc": "2.0", "id": 2, "result": { "items": [ { "rfq_id": "0192f1a3b4c5d6e7f8091a2b3c4d5e6f", "legs": [ { "instrument": "BTC_USDC-2MAY26-77000-C", "quantity": "1.000000" }, { "instrument": "BTC_USDC-2MAY26-80000-C", "quantity": "-2.000000" } ], "taker_account": 2077, "taker_subaccount": "", "taker_funded": true, "request_deadline": "", "deployment": { "program_id": "", "deployment_id": "<64 hex characters>", "config_pda": "", "epoch": 3 }, "nearest_leg_close": 1777680000, "max_quote_expires_at": 1777653600 } ], "next_cursor": "0192f1a3b4c5d6e7f8091a2b3c4d5e6f" } } ``` Pass `next_cursor` back as `cursor`. Paging ends when a result has no `next_cursor`. ## The open request After subscribing, each new request arrives as a `subscription` notification on the `rfq_maker` channel with `type: "open"` and an `rfq` object of the same shape as a poll item. | Field | Meaning | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `rfq_id` | 16-byte request id as 32 lowercase hex characters; the key for every later method and push, and a field of the signed payload | | `legs[]` | The package, in the taker's submitted order. Each leg is a listed `instrument` symbol and a signed 6-decimal `quantity` in taker-buy orientation: positive means the taker receives that many contracts on a `buy` accept, negative means the taker delivers them. Legs are signed in this order; the venue never reorders them | | `taker_account` | The taker's account id | | `taker_subaccount` | The taker's on-chain subaccount PDA, base58. Copied into the signed payload, which binds the quote to this counterparty | | `taker_funded` | `true` when the taker has a positive free balance. A coarse signal: it does not show that the taker can fund either side of this package, and it discloses no balances | | `request_deadline` | RFC3339 UTC. Quotes that arrive after it are refused and the request drains to `declined` | | `deployment` | The margin-vault deployment the request is bound to: `program_id` and `config_pda` in base58, `deployment_id` as 64 hex characters, and the integer `epoch` the request was created under. All four are copied verbatim into the signed payload. A `program_id` that differs from the pinned program id for the cluster should not be quoted | | `nearest_leg_close` | Unix seconds; the earliest expiry among the legs. No quote may expire after it | | `max_quote_expires_at` | Unix seconds; the latest `expires_at` the venue accepts for this request. It is never later than `nearest_leg_close` and is further bounded by the 3600-second TTL ceiling. Sign an `expires_at` at or before it; a later value is rejected, never clamped | ## Pricing a package `bid` and `ask` are signed USDC totals for the whole package in `legs[]`, not per-contract or per-leg prices, and no size multiplier applies. `bid <= ask` is required. Either side may be negative, since a credit structure or an inverted ratio can have negative package value. The accepted side fixes both the cash and the contracts. Quantities are in taker-buy orientation: on a `buy`, the taker's leg vector is `legs[]` as given and the maker's is its negation. | Taker accepts | Cash | Taker receives per leg | Maker receives per leg | | ------------- | -------------------------------------- | ---------------------- | ---------------------- | | `buy` | taker pays `ask`; maker receives `ask` | `quantity` | `-quantity` | | `sell` | taker receives `bid`; maker pays `bid` | `-quantity` | `quantity` | The same rules apply to negative prices. With `bid: "-120.000000"` and `ask: "-80.000000"`, a taker who buys pays `-80`, so the taker collects 80 USDC from the maker. A taker who sells receives `-120`, so the taker pays 120 USDC to the maker. **Example: 1×2 call spread.** The request above asks for `+1.000000` of the 77000 call and `-2.000000` of the 80000 call. With marks of 1270 USDC and 400 USDC, the package is worth `1 × 1270 - 2 × 400 = 470` USDC to a buyer, and a quote 30 USDC wide around that value is `bid` 440, `ask` 500. | Taker accepts | Cash movement | Taker position after the fill | Maker position after the fill | Maker `premium` | `maker_fee` | | ------------- | ------------------------------------------------ | ----------------------------- | ----------------------------- | --------------- | ----------- | | `buy` | taker pays 500.000000; maker receives 500.000000 | +1 of 77000-C, −2 of 80000-C | −1 of 77000-C, +2 of 80000-C | 500.000000 | 0.000000 | | `sell` | taker receives 440.000000; maker pays 440.000000 | −1 of 77000-C, +2 of 80000-C | +1 of 77000-C, −2 of 80000-C | −440.000000 | 0.000000 | `maker_fee` is 0 at launch; the fee formula and rates are on [Fees and Limits](/market-makers/fees-and-limits). Either accepted side leaves the maker short at least one leg, so `writer` on the fill is `true`. ## Responding A response is a signed two-sided quote. The maker signs the quote with the wallet key bound to the account and sends the fields from which the venue rebuilds it. The venue verifies the signature over its own reconstruction and compares every field; any difference is `RFQ_CANONICAL_MISMATCH`. What the signature covers is on [The Signed Quote](/market-makers/quote-payload). ```json theme={null} { "jsonrpc": "2.0", "id": 4, "method": "rfq.maker.respond", "params": { "rfq_id": "0192f1a3b4c5d6e7f8091a2b3c4d5e6f", "quote_nonce": "1777650001234567890", "bid": "440.000000", "ask": "500.000000", "issued_at": 1777650001, "expires_at": 1777650031, "maker_signature": "" } } ``` ```json theme={null} { "jsonrpc": "2.0", "id": 4, "result": { "rfq_id": "0192f1a3b4c5d6e7f8091a2b3c4d5e6f", "quote_id": "<64 hex characters>", "state": "quoted" } } ``` `quote_id` identifies the signed quote in every later push. **Declining.** A decline ends the request. The venue moves it to `declined` at once, the taker sees the reason instead of waiting for the 5-second timeout, and any later `rfq.maker.respond` or `rfq.maker.decline` on it, from any quoter, returns `RFQ_STATE_CONFLICT`. Decline only requests that would not be quoted at any price, once per request. The result echoes `rfq_id` with `state: "declined"` and the `decline_reason`. ```json theme={null} { "jsonrpc": "2.0", "id": 5, "method": "rfq.maker.decline", "params": { "rfq_id": "0192f1a3b4c5d6e7f8091a2b3c4d5e6f", "reason": "size_limit" } } ``` | Reason | Use it when | A reference policy | | --------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `unlisted_instrument` | A leg names an instrument the maker does not quote | a leg symbol is outside the configured instrument set; decided before pricing | | `near_expiry` | The package is too close to `nearest_leg_close` to hedge | time to the leg expiry is under a configured minimum | | `size_limit` | The package exceeds the maker's size or position limit | a leg quantity exceeds the maximum size, or the position on the instrument is at its cap; a quote commits both sides at the taker's option, so the whole request is declined rather than one side | | `stale_data` | Spot, IV, or mark inputs are missing or stale | no fair value can be computed because spot or IV is absent | | `low_confidence` | A price can be computed but the maker will not stand behind it | the pricing parameters are invalid, or the rounded bid falls to zero | | `queue_overflow` | More requests are open than the maker can price inside the window | not needed when pricing keeps up with the request rate | **Quote rules.** * `expires_at` is at most 3600 seconds after `issued_at` and no later than `max_quote_expires_at` from the open request. A later value is `RFQ_VALUE_OUT_OF_RANGE`; the venue never clamps. * A quote must reach the venue with more than 2 seconds of life left, or it is `RFQ_QUOTE_EXPIRED`. The taker's accept and submission windows end 2 seconds before the quote's expiry. * A quote is firm for its full TTL regardless of market moves, so a short, fixed TTL is the recommended policy. * `quote_nonce` is an unsigned 64-bit integer sent as a decimal string, and it must be higher than every nonce the account has ever sent, across restarts; a wall-clock seed at startup satisfies this. A nonce at or below the account's high-water mark is refused with `RFQ_STATE_CONFLICT`. * One quote per request. ## After quoting Three push types follow a winning quote, all on the `rfq_maker` channel and delivered only to the winning maker. * **`state`** arrives on every transition after the quote: `accepted`, `submitting`, `expired`, and `failed`. `accepted_side` is `null` until the taker accepts and then carries `buy` or `sell`. On a taker buy the maker writes every positive leg and the taker every negative one; on a taker sell the roles flip. * **`fill`** arrives when the fill lands. `premium` is the maker's signed cash movement before fees: `+ask` when the taker bought, `-bid` when the taker sold. Book the execution once, on this push. * **`finality`** repeats the fill object with `finality` changed from `confirmed` to `finalized`, or to `reverted` if the chain dropped the transaction. A `reverted` fill means the booked positions and cash did not happen; treat it as a manual correction. ```json theme={null} { "jsonrpc": "2.0", "method": "subscription", "params": { "channel": "rfq_maker", "data": { "type": "fill", "fill": { "rfq_id": "0192f1a3b4c5d6e7f8091a2b3c4d5e6f", "quote_id": "<64 hex characters>", "legs": [ { "instrument": "BTC_USDC-2MAY26-77000-C", "quantity": "1.000000" }, { "instrument": "BTC_USDC-2MAY26-80000-C", "quantity": "-2.000000" } ], "maker_side": "sell", "bid": "440.000000", "ask": "500.000000", "premium": "500.000000", "maker_fee": "0.000000", "writer": true, "tx_signature": "", "finality": "confirmed", "filled_at": "" } } } } ``` **Losing and late quotes.** A losing or late quote receives no push. If another quoter's respond reached the venue first, the later respond returns `RFQ_STATE_CONFLICT` and nothing further is sent about the request. The error means another quoter won, and any reserved risk can be released. A respond that arrives after `request_deadline` returns the same error, because the request has drained. The only push every quoter receives after `open` is the broadcast `state` with `declined` and `quote_id: null` when the window closes with no quote. Reserved risk should be released on local timers, one per request from `request_deadline` and one per delivered quote from `expires_at`, not on the arrival of a message. ## Reconciliation On reconnect, run the connect sequence above: `rfq.maker.subscribe`, then page `rfq.maker.poll`, which returns every request still inside its window. Missed fills are not replayed over the socket, so read them from the query API with the session token as a bearer token, match rows to delivered quotes by `rfq_id`, book any fill not yet booked, and skip rows whose `finality` is `reverted`. The row schema and filters are in the REST API reference under Builders. ```http theme={null} GET /api/query/rfq-fills?role=maker&status=finalized&limit=100 Authorization: Bearer ``` ## Errors Every `rfq.maker.*` failure is JSON-RPC error `1200`. `data.error` carries the RFQ code, `data.retryable` states whether the same request can succeed later, and `data.operation_id` identifies the call for [support@joyride.exchange](mailto:support@joyride.exchange). `RFQ_NOT_READY` has five causes, distinguished by `message` and listed on [Becoming an RFQ Quoter](/market-makers/becoming-a-quoter). | `data.error` | Meaning | Action | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `RFQ_NOT_READY` | The venue cannot admit the call yet; the five causes are on [Becoming an RFQ Quoter](/market-makers/becoming-a-quoter) | Retry with backoff inside the window; escalate the Joyride-owned causes if they persist | | `RFQ_WRONG_ROLE` | The session is not authenticated, the account lacks `rfq_quoter`, or the caller acted on its own request | Check `roles` from `public/auth`; do not retry the same call | | `RFQ_STATE_CONFLICT` | The request already holds a quote, was declined, drained at its deadline, or is otherwise past `pending`; or `quote_nonce` is at or below the account's high-water mark | On respond, treat it as another quoter won and release risk; on a nonce conflict, re-seed above the mark | | `RFQ_CANONICAL_MISMATCH` | The payload the venue rebuilt from the submitted fields differs from the bytes the signature covers | Fix the payload construction against [The Signed Quote](/market-makers/quote-payload); do not retry unchanged | | `RFQ_INVALID_SIGNATURE` | `maker_signature` is not a canonical base64 encoding of 64 bytes, or does not verify under the bound wallet key | Sign with the provisioned wallet key; check the encoding | | `RFQ_QUOTE_EXPIRED` | Fewer than 2 seconds of the quote's life remained when it landed | Re-issue with a later `expires_at` while the request is still `pending` | | `RFQ_VALUE_OUT_OF_RANGE` | A decimal is outside the signed 64-bit 6-decimal range, `bid > ask`, `issued_at >= expires_at`, the TTL exceeds 3600 seconds, or `expires_at` is after `max_quote_expires_at` | Fix the value; the venue never clamps | | `RFQ_PRECISION_UNSUPPORTED` | A decimal carries more than 6 fractional digits or is not a canonical string | Round to 6 decimals and send a string, never a JSON number | | `RFQ_INVALID_INSTRUMENT` | A leg closed, halted, or went into settlement since the request opened | Do not retry; the request cannot fill | | `RFQ_NOT_FOUND` | `rfq_id` names no request the venue knows | Check the id; a request purged at settlement is gone | | `RFQ_RATE_LIMITED` | The 100 requests per second account budget is spent | Back off for the rest of the second; the call was not admitted | ## What is not supported * There is no cancel and no replace of a delivered quote. A quote is firm until `expires_at`, and a second `rfq.maker.respond` on the same request returns `RFQ_STATE_CONFLICT`. Re-pricing means letting the quote expire, so the TTL is the only control over firmness. * The first firm quote wins and every later quote is refused. `rfq.maker.decline` with a reason ends the request at once instead of at the 5-second timeout. * A losing or late quote receives no push. Reserved risk is released on local timers per request and per delivered quote. * There are no partial fills. A package fills for its exact `legs[]` or not at all; a request that cannot be filled whole is declined with `size_limit`. * There is no maker read of live quotes over the socket. Each delivered quote must be recorded locally. `rfq.maker.poll` on reconnect returns what is still open, and `GET /api/query/rfq-fills?role=maker` is the record of fills. Next: [The Signed Quote](/market-makers/quote-payload) # Devnet Sandbox Source: https://docs.joyride.exchange/market-makers/sandbox Endpoints, account provisioning, funding, and on-chain identity for testing a market-making bot against Joyride on Solana Devnet ## Devnet endpoints The sandbox is a full Joyride deployment at `https://devnet.joyride.exchange`, backed by a vault program on Solana Devnet. Paths are the same as on Mainnet; only the host differs. | Surface | URL | | ------------------------------------------------- | ------------------------------------------------- | | Trading WebSocket (authentication, RFQ) | `wss://devnet.joyride.exchange/api/client` | | Oracle WebSocket (spot price) | `wss://devnet.joyride.exchange/api/oracle` | | Query HTTP (self-scoped reads) | `https://devnet.joyride.exchange/api/query/*` | | Public market HTTP (config, instruments, mark IV) | `https://devnet.joyride.exchange/api/v1/market/*` | Authentication, session rules, rate budgets, and error codes are as described on [Connectivity for Bots](/market-makers/connectivity). Moving a bot from Devnet to Mainnet means changing the origin and using a wallet provisioned on Mainnet. ## Requesting access Email [support@joyride.exchange](mailto:support@joyride.exchange) with: * The base58 ed25519 public key the bot will sign in with. Generate a separate key for the sandbox; the Mainnet key is provisioned separately and need not match. * A request for the quoter role. The role and the on-chain subaccount are created in the same pass as the account. Provisioning returns: * An account bound to the public key, with a margin policy and fee tier. `public/auth` on the Devnet trading WebSocket succeeds once it exists. * A subaccount on the Devnet vault owned by the key. `GET /api/query/vault-subaccount` returns one row for the account when it is ready. There is no self-serve signup for bots on Devnet or Mainnet. ## Funding Deposits are Devnet USDC sent to the Joyride vault through its deposit instruction. Transaction fees need Devnet SOL (`solana airdrop` on the Devnet cluster). The USDC mint is listed under [Devnet identity](#devnet-identity); a Devnet USDC faucet can supply it. | Parameter | Value | Applies to | | ------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Deposit asset | USDC on Solana Devnet (6 decimals) | every vault deposit | | Deposit cap | 100,000 USDC | the vault's net custody across every account (total deposited minus total withdrawn); a deposit that would push it above the cap is rejected on chain | | Withdrawal cap | 100,000 USDC | the vault's lifetime total withdrawn across every account; a withdrawal that would push it above the cap is rejected on chain | | Outflow window cap | 10,000 USDC | the vault's total withdrawn in any 24-hour window across every account; a withdrawal that would push the window above the cap is rejected on chain until the window rolls | The caps are set in the vault's on-chain configuration and are shared by every sandbox account, so other users' activity can cause a rejection. Email [support@joyride.exchange](mailto:support@joyride.exchange) if a cap blocks testing. A deposit is credited once its transaction is finalized on Devnet. `GET /api/query/deposits` lists it, and `GET /api/query/vault-subaccount` shows the funded `free_balance`. Only deposits made through the vault's deposit instruction are credited; a plain token transfer to the vault is not. Devnet USDC has no value. The caps bound what the sandbox vault holds and releases. Mainnet collateral is agreed per maker, as described on [Becoming an RFQ Quoter](/market-makers/becoming-a-quoter). ## Devnet identity Quotes bind to the deployment they are signed for, so a quote signed for Devnet is rejected on Mainnet and the reverse. The `deployment` object on every RFQ `open` event carries the identity of the connected environment; comparing it with the configured value at startup catches a wrong-environment deployment before the first quote. | Field | Value | | ---------------- | ---------------------------------------------- | | Cluster | Solana `devnet` | | Vault program id | `4BecHK2cEh6DgfpCdxbQAm812audv4ZXxTQB12RkMWB8` | | Config PDA | `ECJGDQXPnzjoaaPveifT4FSKmYgSbNf1GeiHbtdi626u` | | USDC mint | `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU` | The deployment id and the Mainnet values are on [Program Addresses](/exchange/programs). ## Verifying readiness Readiness is checked from the account's own session once provisioning is complete. Run the checklist on [Becoming an RFQ Quoter](/market-makers/becoming-a-quoter) against the Devnet origin: `public/auth` roles include `rfq_quoter`, `rfq.maker.subscribe` succeeds, `GET /api/query/vault-subaccount` returns a row, and `rfq.maker.poll` returns without `RFQ_NOT_READY`. RFQs arrive only when a Devnet taker sends one, so testing the quoting path requires driving the taker side as well. A quiet sandbox does not indicate an outage. ## What Devnet shares with Mainnet The sandbox runs the same software as Mainnet. | Identical on both | Different on Devnet | | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | The JSON-RPC protocol on every WebSocket and the HTTP query surface | Origin: `devnet.joyride.exchange` instead of `joyride.exchange` | | The signed quote and every gateway frame | Program identity: vault program id, deployment id, config PDA, USDC mint | | Fee mechanics: maker and taker fees, settlement fees, and where each is debited | Funds: Devnet USDC with no value, and vault-wide caps of 100,000 USDC on net deposits, 100,000 USDC on lifetime withdrawals, and 10,000 USDC on withdrawals per 24-hour window | | Error codes and close codes, including `1007`, `1200`, and `4001` | Provisioned accounts: Devnet and Mainnet accounts are separate records | | Rate budgets, session limits, and quote TTL bounds | Flow: RFQs are whatever Devnet takers send | A bot that passes readiness on Devnet needs only its origin, wallet key, and identity configuration changed for Mainnet. Next: [Fees and Limits](/market-makers/fees-and-limits) # Settlement Source: https://docs.joyride.exchange/market-makers/settlement How daily options on Joyride expire and settle, from the settlement price and payoff through the expiry sequence and the records delivered This page covers the settlement price, the payoff calculation, the sequence of events at the expiry boundary, and the records delivered. Settlement is an on-chain transaction against the margin-vault program, and most of what follows is a consequence of that. ## Contract summary | Parameter | Value | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | Exercise style | European. Settlement is automatic at expiry. | | Delivery | Cash-settled in USDC. No physical delivery of the underlying. | | Expiry | Daily at `08:00:00 UTC` | | Contract multiplier | One (1), i.e. one contract represents one unit of the underlying. | | Settlement price source | Time-weighted average price (TWAP) of the underlying index, derived from Pyth oracle prices. One price per underlying per expiry. | | Settlement fee | Charged at expiry to ITM holders. See [Settlement fee](#settlement-fee) below. | ## Settlement price At the expiry instant, the exchange freezes the current TWAP of the underlying index into a canonical settlement price. The TWAP window opens 30 minutes before expiry (`07:30:00 UTC` for the standard 24-hour round). Every account on the same underlying and expiry settles against that price. The recorded value is authoritative and replayable. If no settlement price is available at the boundary, settlement is deferred. There is no fallback price. Trading in the expiring round remains closed, the next round does not open early, and settlement executes once a valid price is available. ## Payoff Each position settles at intrinsic value against the settlement price `S` and strike `K`: ```text theme={null} call_intrinsic = max(S − K, 0) put_intrinsic = max(K − S, 0) gross_cashflow = intrinsic × position_quantity (signed: long +, short −) net_cashflow = gross_cashflow − settlement_fee ``` Longs are credited and shorts are debited directly to the account's vault subaccount. There is no separate claims or payout process. An option exactly at the money (`S = K`) settles worthless for both calls and puts. Out-of-the-money positions settle at zero, i.e. the position is still closed and a settlement record is still issued, with a cashflow of zero. ### What counts as one settled unit Settlement acts on each fill. Every finalized, non-reverted fill at that expiry is valued independently and the results are then netted into one signed balance adjustment per account for the whole expiry, applied on chain to that account's vault subaccount. A strategy (multi-leg) fill settles as its legs, each valued independently and netted per account. Because a fee is assessed per in-the-money leg, an offsetting pair of fills nets to zero cash but still contains one in-the-money leg. The clamp under [Settlement fee](#settlement-fee) bounds what that leg is charged. ## Settlement fee The holder of an in-the-money (ITM) position pays a fee at expiry. The writer pays nothing at settlement. Out-of-the-money (OTM) positions pay nothing since intrinsic value is zero. The fee is the lesser of a rate on index notional and a cap on intrinsic value: ```text theme={null} fee = min( ppm × settlement_price × quantity, cap_bps × intrinsic × quantity ) ``` `ppm` is the rate in parts per million of index notional. `cap_bps` is the cap in basis points of intrinsic value. Both come from your account's fee tier; the standard tier sets the rate to 0.015% of index notional and the cap to 12.5% of intrinsic value. The tier values and their units are set out on [Fees and Limits](/market-makers/fees-and-limits). The cap keeps a barely-in-the-money position from owing more than it receives. It also covers the out-of-the-money case, since zero intrinsic gives a zero cap and therefore a zero fee. ### What the fee is netted against An account can hold in-the-money longs and owe on shorts settling together. The fee is clamped against the account's net: an account that nets negative pays no settlement fee, and one whose net is smaller than its assessed total pays only the net. ```text theme={null} charged = min( sum(assessed fees), max(net_cashflow, 0) ) ``` The clamp is applied per expiry. An expiry is planned as a single settlement across every underlying, so the clamp sees the account's whole expiry in one number. A settlement fee never leaves an account worse off than it stood before the settlement. ### Which rate applies The rate and the account's tier assignment are frozen when the expiry's settlement price is recorded, and the whole expiry settles against that frozen pair. Settlement can span several transactions over several minutes; without the freeze, two accounts with identical positions could be charged different rates depending on which batch landed first. A rate change never unwinds fees already collected. ### Where the money lands The fee is an on-chain transfer into the fee subaccount, carried inside the settlement transaction itself as the entry that makes the batch balance. ## Expiry sequence Settlement is a transaction against the margin-vault program and waits for its inputs to be final on chain before moving any balance. 1. **A settlement delay elapses** after the expiry instant (default 60 seconds), giving the settlement price and the last on-chain fill finalizations time to land. 2. **Preconditions are checked.** Every fill at that expiry must be finalized on chain (confirmed is not sufficient), and a settlement price must exist for every underlying involved. If either is missing, settlement waits and retries. There is no fallback price, and a fill that could still be reorged away is not settled. 3. **The expiry is planned.** Across every fill at that expiry and every underlying, the planner computes for each account: its net signed balance adjustment, the collateral lock to release, how many written positions close, and any shortfall. Fees are assessed per in-the-money leg and then clamped to the account's expiry net. 4. **Entries are batched.** Entries are ordered deterministically, payers first and then receivers, and chunked into `apply_settlement` batches sized to one Solana transaction. Payers land first so the fee subaccount always holds what the later receiver batches pay out. 5. **Each batch is made self-balancing.** A single entry against the fee subaccount closes the batch so that its balance adjustments sum exactly to its insurance draw. This is the same entry that collects the settlement fees. 6. **Batches are sent in order**, each recorded durably as an intent before it is submitted. A batch is keyed `(expiry, batch_id)` and guarded by an on-chain receipt, so re-submitting one is a no-op rather than a double settlement. 7. **Landed batches are ingested.** The chain watcher reads the per-entry effects and the batch summary back off chain: positions clear, collateral locks release, and fee attribution is marked confirmed. 8. **Anything that did not land is re-planned.** A re-plan excludes accounts a prior batch already settled and never reuses a batch id, so recovery converges on exactly one settlement per account. ## Settlement records There is no live message stream for settlement. The authoritative record is the `apply_settlement` transaction, which carries, per account, the signed balance adjustment, the lock release, and the position count closed. The exchange mirrors it into its own records as it is ingested; the settled fills are read from `GET /api/query/rfq-fills` and the resulting balance from `GET /api/query/vault-subaccount`. A fee record is written before it is charged. Fee attribution is recorded when the settlement batch is built, ahead of submission, and carries a confirmation timestamp that stays empty until that exact transaction lands. An unconfirmed row records an intent; the charge exists only once the timestamp is set. Revenue and statement figures must filter on confirmation. Confirmation is per attempt. A batch that fails to land is re-planned and re-sent, and the replacement can carry a different fee, or none, if the account's tier changed in between. Each attempt keeps its own record and at most one is ever confirmed, because the on-chain receipt allows only one application to take effect. Superseded attempts stay unconfirmed. The recorded cashflow stays gross, so the payout before fees and the amount taken from it are both recoverable. ## Solvency & loss handling Joyride does not socialize losses. There is no auto-deleveraging, no clawback of settled profits, and no haircut applied to winning positions to fund another account's shortfall. Winning counterparties are paid full intrinsic value less the settlement fee described above. The settlement fee is never the cause of a shortfall draw. It is clamped to what the settlement paid the account, so the debit cannot leave the account worse off than it stood before, and loss-absorbing capital never finances fee revenue.