# Architecture Source: https://docs.flashnet.xyz/architecture Flashnet's design ## Overview Flashnet leverages several key primitives facilitated through [Spark](https://docs.spark.info/spark/spark-tldr) to enable secure, efficient trading. All solutions are built on the foundation of non-custody and the atomic swap primitive. Depending on the product requirements, we implement different approaches: * For trustless flows like our RFQ system, we simply rely on vanilla atomic swaps with no external coordination other than quote discovery * For advanced solutions like our CLOB or AMM, we combine TEEs and validators to enable intent-based execution at settlement -- funds stay completely in the user's control and trust is only required during execution. ## Core Primitives ### Atomic Swap The atomic swap is the fundamental building block of Flashnet. On Spark, atomic swaps are implemented using adaptor signatures, where the exchange is enforced by L1 validation and Bitcoin consensus rules. This creates a trustless exchange mechanism between two parties. ### Intents System Our intent-based execution engine is powered by validators operating within TEEs. All trading interactions are modeled as maker/taker relationships. The engine verifies that both parties have properly expressed their intent to trade and then securely settles the transaction according to the agreed terms. Two key design principles guide our intent-based execution environment: 1. **Minimal exposure window**: Each execution environment is designed with the shortest possible lifecycle, ensuring funds are only exposed during the brief settlement period and not subject to long-term holding risks unless the user explicitly opts to hold. (market makers, LPing, etc. where it makes more sense to have a long-term exposure) 2. **Isolated MPC wallets**: Rather than using a single shared MPC wallet, each user operates with multiple dedicated MPC wallets, ensuring that if one wallet is compromised, the others remain secure and isolated from system-wide risk. This engine is at the core of our trust-minimized products like Flashnet Trade and Flashnet AMM. It distributes trust across multiple validators, who do intent verification before signing off on transactions to be executed natively on Spark. This enables us to have quasi-smart contract execution without the overhead of a full-fledged smart contract VM, while still providing similar security guarantees. You can think about this as an application specific validation layer for Spark. ### AMM Our AMM operates within our intent based execution environment, where liquidity providers interact through MPC wallets. Users can swap tokens/BTC or contribute liquidity to these pools. The AMM supports different pricing mechanisms to suit different trading needs, including constant product, custom bonding curves, and concentrated liquidity positions. ### Centralized Limit-Orderbook Our CLOB powers the Flashnet Trade product with a state-of-the-art matching engine which matches orders based on price / time priority. Key components include: * Advanced matching engine for efficient order execution * Settlement via our intent-based execution engine with validator oversight * User-controlled delegated trading wallets using threshold signature schemes * Safety mechanisms allowing users to: * Exit unilaterally to L1 * Coordinate exit via Spark transaction * Cancel orders at any time to revoke approval and recover locked funds You can learn more about how Flashnet markets work in the [Architecture](/architecture) overview. ### Execution [Flashnet Execution](https://build.flashnet.xyz/products/execution/overview) is the runtime where the intent-based engine composes Spark settlements with deployed contract code. The runtime is EVM-compatible, so contracts compile with Solidity and standard tooling. Integrators sign one intent that moves assets in from Spark, runs an action (a market swap or your own contract), and dispatches the result back. See [How it works](https://build.flashnet.xyz/products/execution/how-it-works) for the sequencer, validator, and TEE design. # Trust Model Source: https://docs.flashnet.xyz/architecture/trust-model How TEE, validators, and user custody split authority so no single party controls funds Flashnet executes complex actions (AMM pool creation, swaps, CLOB, escrow, etc.) without a general-purpose VM by combining three independent actors. The model deliberately splits authority so that **no single party can move funds or mutate state**. ## Why the Split Works 1. **Custody stays with the user** until the very moment all validators agree the intent is valid; the TEE cannot act without the shards, and validators cannot act without the enclave. 2. **m-of-n secret sharing** permits liveness with up to `n − m` offline or malicious validators while preventing sub-threshold collusion. 3. **Deterministic enclave code + remote attestation** constrains the TEE to a publicly auditable state machine. 4. **Accountability** means that any validator who withholds shards or signs a bad intent can be proven dishonest and penalised. ## Security Assumptions 1. The enclave’s hardware isolation (e.g. SGX or Nitro) prevents key extraction; compromised hardware would be detected via failed remote attestation. 2. At least **m** validators are honest and responsive; liveness requires this quorum. 3. Spark finality ensures that once the tx is signed the state transition is immutable and can be sequenced to Bitcoin. ## Failure Scenarios # Introducing Flashnet Source: https://docs.flashnet.xyz/introduction Bringing Bitcoin markets back onto Bitcoin ## What is Flashnet? Flashnet is a modular exchange stack for Bitcoin. It is designed to rival the performance of TradFi exchange systems without any of the custody, and without introducing blockchain-related innefficiencies to execution. This is facilitated by [Spark](https://spark.money/), a UTXO scaling solution we helped build that enables near-instant, zero-fee settlement of Bitcoin and other assets. Flashnet powers any type of market with native Bitcoin settlement. ### What is Spark? Spark scales Bitcoin using pure cryptographic messaging. Learn more [here](https://docs.spark.money/spark/spark-tldr). Spark is not a blockchain, and as such does not suffer from the same bottlenecks as most L2's such as bridging, block creation, transaction ordering, or consensus. The user retains the power to unilaterally exit at all times, even with open orders on Flashnet. # Approval Flows Source: https://docs.flashnet.xyz/products/orchestration/api/approval-flows When an eligible Bitcoin L1 deposit receives an instant-credit offer, the order enters `awaiting_approval` with a pending `zeroconfOffer`. Use the endpoints below to accept or decline. See [Order Lifecycle](/products/orchestration/order-lifecycle) for the full state machine and [ZeroConf](/products/orchestration/zeroconf) for how instant credit works. Market-move repricing is handled automatically. If post-deposit execution would exceed the quote's `slippageBps`, the order refunds without partner action. There is no accept/decline step. See [Market moves and refunds](#market-moves-and-refunds) below. ## POST /v1/orchestration/zeroconf/accept Accept a pending ZeroConf offer and resume execution with instant credit. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/orchestration/zeroconf/accept" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: zeroconf-accept:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "orderId": "ord_..." }' ``` Rules: * Order must be in `awaiting_approval` with a pending `zeroconfOffer`. * The offer must not be expired. * The API key must belong to the same partner as the order. * Retrying the same request with the same `X-Idempotency-Key` is safe. A new accept request after the offer has already resolved returns an invalid state error. Response: ```json theme={null} { "orderId": "ord_...", "status": "processing" } ``` ## POST /v1/orchestration/zeroconf/decline Decline a pending ZeroConf offer. The order waits for 1 on-chain confirmation before proceeding. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/orchestration/zeroconf/decline" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: zeroconf-decline:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "orderId": "ord_...", "reason": "Optional partner reason" }' ``` Rules: * Order must be in `awaiting_approval` with a pending `zeroconfOffer`. * The API key must belong to the same partner as the order. Response: ```json theme={null} { "orderId": "ord_...", "status": "confirming" } ``` After declining, the engine waits for 1 on-chain confirmation before proceeding. ## ZeroConf offer fields This is the canonical `zeroconfOffer` schema. Status responses and webhook payloads use the same projection, so the object is identical wherever it appears. When a Bitcoin L1 deposit receives a ZeroConf quote, the order includes a `zeroconfOffer` object: ```json theme={null} { "zeroconfOffer": { "version": 1, "status": "pending", "quoteId": "", "sparkAddress": "spark1...", "txid": "", "vout": 0, "depositSats": "250000", "instantSats": "245000", "feeSats": "5000", "expiresAt": "2026-02-04T01:31:00.000Z", "offeredAt": "2026-02-04T01:30:00.000Z", "resolvedAt": null, "sparkTxid": null, "declineReason": null } } ``` Always present: | Field | Type | Notes | | --------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `version` | `number` | Always `1` | | `status` | `string` | `pending`, `accepted`, `declined`, `expired`, or `confirmed`. `confirmed` means the Bitcoin tx reached 1-conf before the offer was resolved, bypassing the approval window. | | `quoteId` | `string` | Spark static-deposit quote id | | `sparkAddress` | `string` | Spark address for the instant credit | | `txid` | `string` | Bitcoin transaction id of the deposit | | `vout` | `number` | Deposit output index | | `depositSats` | `string` | Total BTC deposited (sats) | | `instantSats` | `string` | Full net amount credited on acceptance (sats) | | `feeSats` | `string` | Difference between `depositSats` and `instantSats` from Spark's static-deposit quote | | `expiresAt` | `string` | Offer expiry (ISO 8601) | | `offeredAt` | `string` | When the offer was generated (ISO 8601) | | `resolvedAt` | `string \| null` | When the offer was accepted, declined, or expired. `null` while pending. | | `sparkTxid` | `string \| null` | Spark transfer id once the instant credit has executed. `null` before that. | | `declineReason` | `string \| null` | The `reason` supplied on decline. `null` otherwise. | Present only when set (omitted otherwise): `quoteNetwork`, `quoteSignature`, `planId`, `planAmountSats`, `planConfirmations`, `planStatus`, `transferSparkId`, and `claimId`. These carry Spark static-deposit quote and fulfillment-plan details. Treat them as informational; the fields above are the ones to act on. `feeSats` is not the Flashnet orchestration platform fee. Platform pricing stays in the normal quote and order fields such as `feeBps`, `feeAmount`, and `totalFeeAmount`. Read `feeSats` from each offer. Do not hard-code a fixed percentage or fixed sat amount. The order status is `awaiting_approval` while `zeroconfOffer.status` is `pending`. Resolve with: * `POST /v1/orchestration/zeroconf/accept` to accept and receive instant credit * `POST /v1/orchestration/zeroconf/decline` to wait for 1 on-chain confirmation before proceeding If the offer expires without a response, it is marked `expired` and the engine waits for 1 on-chain confirmation before proceeding. ## Market moves and refunds If the pool moves against the quote between the deposit and execution, Orchestra refunds the order automatically. No partner action is required. The resulting order will have `status=refunding` and `errorCode=slippage_exceeded` before finalizing as `refunded`. Late deposits (arriving after quote expiry) are always accepted and repriced at the live market rate at detection time, then executed against `slippageBps`. If the pool has moved too far, the normal `slippage_exceeded` refund applies. Exact-out orders that can't be satisfied from the received amount refund with one of: * `exact_out_insufficient_input`: the deposit was below `requiredAmountIn` * `exact_out_input_above_max`: the deposit exceeded `maxAcceptedAmountIn` * `exact_out_target_not_met`: the pool couldn't produce `targetAmountOut` In all cases the webhook event is `order.refunding`. Partners should treat these as normal refund flows. # Client Keys Source: https://docs.flashnet.xyz/products/orchestration/api/client-keys Orchestra issues two kinds of API keys. Server keys stay secret. Client keys are public. | | Server key | Client key | | -------------------------------------------- | ---------------------- | ---------------------------------------- | | Prefix | `fn_` | `fnp_` | | Secrecy | Secret, hashed at rest | Public, visible anytime in the dashboard | | Capabilities | Everything | Quote, submit, onramp, address creation | | Dashboard access | Yes | No | | Webhooks, affiliates, transaction history | Yes | No | | Embedding in open-source SDKs / browser code | No | Yes | Use client keys when the key will ship inside something your users can inspect: a mobile app, a web bundle, an open-source SDK. Use server keys for anything running on a backend you control. ## When to pick which If you own the environment the key runs in (your servers, a VPC, a private container), use a server key. If end users can extract the key by opening devtools, decompiling an APK, or reading a git repo, use a client key. ## Getting a key Every new partner is provisioned with both keys automatically. Find them in the dashboard under **API Keys**. Client keys can be revealed at any time (they're public). Server keys are shown once at creation and hashed afterward. Additional keys of either type can be created from the same page. ## Scopes Client keys are scope-gated. Only these actions are permitted: | Scope | Endpoint | | --------------------- | ------------------------------------- | | `orders:quote` | `POST /v1/orchestration/quote` | | `orders:submit` | `POST /v1/orchestration/submit` | | `orders:onramp` | `POST /v1/orchestration/onramp` | | `orders:read` | `GET /v1/orchestration/status?id=...` | | `orders:sse` | `GET /v1/sse/operations/:id` | | `accumulation:create` | `POST /v1/accumulation-addresses` | | `liquidation:create` | `POST /v1/liquidation-addresses` | Any request outside this allowlist returns `403 forbidden` with `"This route is not available for client keys"`. There is no opt-in: client keys cannot read transaction history, manage webhooks, or touch affiliate data, regardless of what scopes are assigned. ## Modes When you create a client key you choose a mode. The mode determines how the `Origin` header is enforced. | Mode | Origin check | | --------- | ------------------------------------------------------------------------ | | `server` | Not checked. Use from backends that don't send `Origin`. | | `browser` | `Origin` header required, must match the key's allowed origins. | | `both` | If `Origin` is present it must match; if absent, the request is allowed. | Allowed origins are a per-key list configured at creation time. Leave the list empty to accept any origin (useful when you can't enumerate SDK consumers up front). Global CORS (`CORS_ALLOWED_ORIGINS`) still applies in every case. ## Read-tokens Client keys are shared across many end users. If user A calls `submit`, user B must not be able to read user A's order by guessing the ID. Orchestra enforces this with short-lived HMAC read-tokens. ``` # Submit under a client key: response contains a readToken { "orderId": "ord_...", "readToken": "eyJ...ABC" } # Read the order using the token, via X-Read-Token header or ?readToken= GET /v1/orchestration/status?id=ord_... HTTP/1.1 Authorization: Bearer fnp_... X-Read-Token: eyJ...ABC ``` The token is bound to `(partnerId, apiKeyId, orderId, expiry)`. Tokens for other orders, other keys, or expired tokens return `403 read_token_required` or `403 invalid_read_token`. Server keys don't need a read-token; they can read any order under the partner. SSE subscriptions (`/v1/sse/operations/:id`) follow the same rule. Because `EventSource` can't set headers, pass the read-token as a query param: ``` GET /v1/sse/operations/ord_...?token=fnp_...&readToken=eyJ... HTTP/1.1 ``` ## Rate limits Client keys are rate-limited at two layers. Server keys are not limited by these buckets. | Route | Per-key/min | Per-(key, IP)/min | | ------------------------- | ----------- | ----------------- | | `orders:quote` | 600 | 60 | | `orders:submit` | 120 | 10 | | `orders:onramp` | 120 | 10 | | `orders:read` (`/status`) | 6000 | 1200 | | `accumulation:create` | 60 | 5 | | `liquidation:create` | 60 | 5 | The per-(key, IP) layer stops a single abusive user from exhausting the partner's aggregate budget. The per-key layer is the DoS ceiling. Both return `429 rate_limited` with `Retry-After` when tripped. ## Privileged fields are stripped Request bodies passed with a client key have privileged fields silently removed before validation. This includes anything that would let a caller redirect fees, override tiers, or inject admin flags. The request continues as if those fields were never sent; no error surfaces so the schema stays opaque. ## Revocation and disable Client keys support two lifecycle actions: * **Disable** (`POST /v1/partner/dashboard/api-keys/:id/disable`): the key starts failing auth immediately, but in-flight operations continue to completion. Reversible: an operator can re-enable the key at any time. * **Revoke** (`DELETE /v1/partner/dashboard/api-keys/:id`): permanent. Sets `revoked_at`; in-flight operations continue, but no new requests succeed. Disable first when rotating; revoke only when a key will never be used again. ## End-to-end example ```bash theme={null} # 1. Quote curl -sS -X POST "https://orchestration.flashnet.xyz/v1/orchestration/quote" \ -H "Authorization: Bearer fnp_..." \ -H "X-Idempotency-Key: quote:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "sourceChain": "base", "sourceAsset": "USDC", "destinationChain": "spark", "destinationAsset": "BTC", "amount": "100000000", "recipientAddress": "spark1...", "slippageBps": 50 }' # 2. Submit: response includes readToken. txHash is the funding field for # EVM and Solana sources; other chains use sparkTxHash, bitcoinTxid + bitcoinVout, # or lightningReceiveRequestId, with optional sourceAddress / sourceSparkAddress. curl -sS -X POST "https://orchestration.flashnet.xyz/v1/orchestration/submit" \ -H "Authorization: Bearer fnp_..." \ -H "X-Idempotency-Key: submit:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "quoteId": "q_...", "txHash": "0x..." }' # -> { "orderId": "ord_...", "readToken": "eyJ..." } # 3. Poll status: read-token required (pass as ?readToken= or X-Read-Token header) curl -sS "https://orchestration.flashnet.xyz/v1/orchestration/status?id=ord_..." \ -H "Authorization: Bearer fnp_..." \ -H "X-Read-Token: eyJ..." ``` The rest of the Orchestra flow (deposit handling, webhook delivery, repricing) is identical to the server-key flow documented in [Quickstart](/products/orchestration/integration). # Error codes Source: https://docs.flashnet.xyz/products/orchestration/api/error-codes Orchestra returns errors in a standard envelope. For the envelope shape, see [API Overview](/products/orchestration/api/overview#errors). ## What error codes can the API return? | Code | HTTP | Endpoints | Description | | ---------------------------- | ---- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `unauthorized` | 401 | All authenticated endpoints | Missing or invalid API key. | | `auth_required` | 401 | Deposit address resolution | Deposit address derivation requires an authenticated partner context. | | `origin_required` | 403 | All client-key endpoints | Client key in `browser` mode sent no `Origin` header. | | `origin_not_allowed` | 403 | All client-key endpoints | `Origin` header is not on the client key's allowed list. | | `scope_required` | 403 | All client-key endpoints | The client key lacks the scope for this endpoint. | | `read_token_required` | 403 | `/status`, SSE | Client key read a shared resource without `X-Read-Token` / `?readToken=`. | | `invalid_read_token` | 403 | `/status`, SSE | Read-token is malformed, expired, or not bound to this order and key. | | `missing_idempotency_key` | 400 | `/quote`, `/submit`, `/onramp`, `/zeroconf/*`, all mutating resource endpoints | `X-Idempotency-Key` header missing on a partner-authenticated mutating request. | | `idempotency_conflict` | 409 | `/quote`, `/submit`, `/onramp`, `/zeroconf/*`, all mutating resource endpoints | Same idempotency key reused with a different JSON body. | | `idempotency_in_progress` | 409 | `/quote`, `/submit`, `/onramp`, `/zeroconf/*`, all mutating resource endpoints | A request with the same idempotency key is still executing. Retry after it completes. | | `unsupported_route` | 400 | `/estimate`, `/quote`, `/onramp` | The `(sourceChain, sourceAsset, destinationChain, destinationAsset)` combination is not supported. | | `unsupported_amount_mode` | 400 | `/quote`, `/onramp` | `amountMode` is not supported for the selected route. `/quote` exact-out support includes stablecoin `lightning:BTC` sell routes, `bitcoin:BTC -> (base\|solana):USDC`, and supported source routes to `lightning:BTC`. `/onramp` exact-out is not supported for direct BTC passthrough routes. | | `unsupported_fee_plan` | 400 | `/estimate`, `/quote`, `/onramp` | `appFees` or `affiliateIds` on a route with no supported fee settlement (for example pure-BTC routes with no stablecoin settlement leg), or fees on an `amountFiatUsd` exact-out onramp. | | `unsupported_delivery_mode` | 400 | `/quote`, `/estimate`, `/onramp` | `deliveryMode=fixed` combined with `amountMode=exact_out`, or requested on a route that does not support fixed delivery. | | `fixed_delivery_unavailable` | 400 | `/quote`, `/estimate` | Fixed delivery requested but not currently available for this partner or route. The message states the reason. | | `unsupported_asset` | 400 | `/onramp` | `amountFiatUsd` exact-out on a destination asset that is not a USD stablecoin. | | `invoice_required` | 400 | `/onramp` | `amountFiatUsd` with `exact_out` is limited to invoice-billing partners. | | `spot_unavailable` | 503 | `/onramp`, pay links | BTC/USD spot price is stale or unavailable, so `amountFiatUsd` cannot be converted. Retry later. | | `invalid_request` | 400 | `/estimate`, `/quote`, `/submit`, `/onramp`, `/zeroconf/*` | Request body failed business validation. Check field constraints. Stablecoin exact-out amounts must be whole-cent values. | | `invalid_query` | 400 | `/status`, `/order`, `/history` | Invalid query parameters. | | `invalid_address` | 400 | `/quote`, `/onramp` | Recipient or refund address is invalid for the target chain. | | `invalid_state` | 400 | `/submit`, `/zeroconf/*` | Endpoint was called in the wrong order state. For example, calling `/zeroconf/accept` on an order that is not `awaiting_approval`. | | `invalid_payout_destination` | 400 | `PUT /v1/affiliates/:affiliateId` | The `(payoutChain, payoutAsset)` combination is not in the live payout route table. | | `conflict` | 409 | `/submit`, `/zeroconf/*` | State changed concurrently. Retry the request. | | `quote_expired` | 409 | `/submit` | Destination invoice has expired (Lightning). Standard quote expiry does not return this error; expired quotes are always repriced at live market rates on submit. | | `not_found` | 404 | `/status`, `/order`, `/submit`, `/zeroconf/*` | Quote or order not found. | | `amount_too_small` | 400 | `/estimate`, `/quote`, `/onramp` | Swap input is below the Flashnet pool minimum. | | `amount_too_large` | 400 | `/estimate`, `/quote`, `/onramp` | Amount exceeds the maximum for the selected route. | | `price_impact_too_high` | 400 | `/estimate`, `/quote`, `/onramp` | Quote exceeds configured price impact constraints. | | `rate_limited` | 429 | `/routes`, `/estimate`, `/quote`, `/order`, `/status` | Too many requests. Back off and retry. See [rate limits](/products/orchestration/api/overview#rate-limiting) for per-endpoint thresholds. | | `flashnet_error` | 502 | `/submit` | Swap simulation or execution failed inside Flashnet. | | `internal_error` | 500 | Any | Unhandled server error. Some malformed JSON or schema violations surface as `internal_error`. Treat it as a request bug and retry only after fixing the payload. | | `validation_error` | 400 | Any | Request body or query parameters failed schema validation (Zod). | | `expired` | 409 | `/zeroconf/accept`, `/zeroconf/decline` | ZeroConf offer has expired. | | `route_disabled` | 400 | `/estimate`, `/quote`, `/onramp` | The selected route is temporarily disabled. | | `service_unavailable` | 503 | Any | Global kill switch is active. | | `partner_disabled` | 403 | All authenticated endpoints | Partner account is disabled. | | `forbidden` | 403 | `/status`, `/order`, `/submit` | Cross-partner access attempt. | | `ambiguous_query` | 400 | `/status` | Multiple orders match the provided `txHash`. Narrow the query. | | `invalid_tx_hash` | 400 | `/submit` | Transaction hash format is invalid for the source chain. | | `payload_too_large` | 413 | All JSON endpoints | Request body exceeds 1 MiB. | | `unsupported_media_type` | 415 | All JSON endpoints | Content type is not JSON on a JSON endpoint. | Endpoint paths are abbreviated. Full paths use the `/v1/orchestration/` prefix (e.g. `/v1/orchestration/quote`). Resource endpoints (`/v1/affiliates/*`, `/v1/webhooks/*`, `/v1/accumulation-addresses/*`, `/v1/liquidation-addresses/*`) are listed as "all mutating resource endpoints" where applicable. ## Order-lifecycle error codes These surface on order records (`order.errorCode`) and webhook payloads, not as HTTP responses. They indicate why an order moved to `refunding` or `failed`. Order error messages are sanitized before they are stored or delivered publicly. Use `errorCode` for programmatic handling. `errorMessage` is partner-safe copy and does not include provider diagnostics, database errors, or stack details. | Code | Terminal status | Description | | ------------------------------ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | `slippage_exceeded` | `refunding` → `refunded` | Pool price moved past the quote's `slippageBps` between deposit and execution. Order refunds automatically; no partner action required. | | `exact_out_insufficient_input` | `refunding` → `refunded` | The deposit was below `requiredAmountIn` for an exact-out strict order. | | `exact_out_input_above_max` | `refunding` → `refunded` | The deposit exceeded `maxAcceptedAmountIn` for an exact-out strict order. | | `exact_out_target_not_met` | `refunding` → `refunded` | The pool could not deliver `targetAmountOut` at execution time. | | `duplicate_lightning_invoice` | `failed` | A destination Lightning BOLT11 invoice was reused. Single-use invoices cannot settle twice. | ## What are common error scenarios? **Submitting against an expired quote.** Quotes expire after 2 minutes. If you call `POST /submit` after the TTL, the engine reprices the order at live market rates and proceeds. Execution is still bounded by the quote's `slippageBps`. If the pool has moved past that tolerance, the order refunds automatically with `slippage_exceeded`. The `quote_expired` HTTP error code applies only to expired Lightning destination invoices (BOLT11), not to general quote expiry. **Duplicate idempotency key with a different body.** You reuse an `X-Idempotency-Key` value but change the request payload. The API returns `409 idempotency_conflict`. Use a new key for each distinct request. Replaying the exact same request with the same key returns the stored response and sets `X-Idempotency-Replayed: true`. **Unsupported fee plan on a pure-BTC route.** You request `appFees` or `affiliateIds` on a `spark:BTC -> bitcoin:BTC` quote. The API returns `400 unsupported_fee_plan`. Fee plans require a stablecoin settlement leg. Routes between BTC endpoints with no stablecoin conversion do not support app fees. **Market moved against the quote.** The deposit arrives and the pool has shifted past `slippageBps`. The order moves to `refunding` with `errorCode=slippage_exceeded`. No action is needed. The refund is automatic. **ZeroConf offer expired before response.** You call `POST /zeroconf/accept` after the offer's `expiresAt` has passed. The API returns `409 expired`. The engine falls back to waiting for 1 on-chain confirmation automatically. # API Overview Source: https://docs.flashnet.xyz/products/orchestration/api/overview Base URL: `https://orchestration.flashnet.xyz` OpenAPI: * Swagger UI: `GET /docs` * OpenAPI JSON: `GET /openapi.json` Orchestra is the product name. API paths use the `/v1/orchestration/` prefix. The OpenAPI spec covers the core orchestration surface but is not exhaustive; these docs are authoritative where they differ. Start here: * Product overview: [Orchestra](/products/orchestration/overview) * End-to-end integration flow: [Quickstart](/products/orchestration/integration) ## How does authentication work? Authenticated endpoints require an API key: ```bash theme={null} Authorization: Bearer fn_... # server key (secret) Authorization: Bearer fnp_... # client key (public, scoped) ``` Use `GET /v1/orchestration/estimate` for unauthenticated price previews. `POST /v1/orchestration/quote` is the start of a submit-able flow and should be called with `Authorization`. Quotes created without `Authorization` (if accepted) cannot be submitted. A quote is bound to your partner account and can only be submitted by an API key for the same partner. If a quote uses `affiliateId` or `affiliateIds`, `Authorization` is required because affiliate profiles are partner-scoped. ### Server keys vs client keys Orchestra issues two key types. Server keys (`fn_...`) are secret and have full access. Client keys (`fnp_...`) are public, scope-gated, and safe to embed in open-source SDKs or browser code. Use a server key when the key runs in an environment you control. Use a client key when the key will ship inside something your users can inspect. See [Client Keys](/products/orchestration/api/client-keys) for scopes, modes, read-tokens, and rate limits. ## How does idempotency work? Partner-authenticated mutating endpoints require `X-Idempotency-Key`. Rules: * Key scope is `(partnerId, endpoint, key)`. * Replaying the same request returns the stored response and sets `X-Idempotency-Replayed: true`. * Reusing a key with a different JSON body returns `409 idempotency_conflict`. ## Errors Errors use a single envelope: ```json theme={null} { "error": { "code": "string", "message": "string" } } ``` Illustrative `error.code` values: * `unauthorized`: Missing or invalid API key * `forbidden`: Route or action is not permitted for the key type presented * `idempotency_conflict`: Same idempotency key reused with a different payload * `invalid_request`: Request body failed business validation * `unsupported_route`: Route is not supported * `rate_limited`: Too many requests * `internal_error`: Unhandled error The full catalog, including HTTP statuses, affected endpoints, client-key auth codes, and order-lifecycle codes, lives in [Error codes](/products/orchestration/api/error-codes). ## What chains, assets, and amount formats are supported? Routes are expressed as `(sourceChain, sourceAsset) -> (destinationChain, destinationAsset)`. The supported set changes as chains and assets are added, so discover it at runtime instead of hardcoding it: `GET /v1/orchestration/routes` returns every live pair with contract addresses, decimals, chain ids, and eligibility flags, and `GET /v1/orchestration/limits` returns per-route amount bounds. Amounts are integer strings in smallest units. Do not send floats. Read each asset's decimals from its `/routes` detail object; see [Amounts](/products/orchestration/concepts/amounts) for the conventions. ## Rate limiting Some public endpoints are IP rate-limited when rate limiting is enabled in the deployment: * `GET /v1/orchestration/routes`: 60 requests per minute * `GET /v1/orchestration/limits`: 60 requests per minute * `GET /v1/orchestration/estimate`: 120 requests per minute * `GET /v1/orchestration/order`: 120 requests per minute * `GET /v1/orchestration/status` (unauthenticated): 120 requests per minute When enabled, responses include `X-RateLimit-*` headers and the API returns `429 rate_limited` on exhaustion. Client keys have an additional two-layer rate limit that server keys don't: | Route | Per-key/min | Per-(key, IP)/min | | --------------------------------- | ----------- | ----------------- | | `POST /v1/orchestration/quote` | 600 | 60 | | `POST /v1/orchestration/submit` | 120 | 10 | | `POST /v1/orchestration/onramp` | 120 | 10 | | `GET /v1/orchestration/status` | 6000 | 1200 | | `POST /v1/accumulation-addresses` | 60 | 5 | | `POST /v1/liquidation-addresses` | 60 | 5 | See [Client Keys](/products/orchestration/api/client-keys) for the full model. ## Reference pages * [Client Keys](/products/orchestration/api/client-keys): Public, scope-gated keys for SDK and browser embedding * [Quotes and Orders](/products/orchestration/api/quotes-and-orders): Create quotes, submit deposits, and track order status * [Approval Flows](/products/orchestration/api/approval-flows): Resolve repricing requests and ZeroConf offers * [Resource Management](/products/orchestration/api/resources): Manage affiliates, webhooks, accumulation addresses, and liquidation addresses # Quotes and Orders Source: https://docs.flashnet.xyz/products/orchestration/api/quotes-and-orders For authentication, error codes, and request conventions, see [API Overview](/products/orchestration/api/overview). ## GET /v1/orchestration/routes List all supported trading pairs. No authentication required. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/orchestration/routes" ``` Response: ```json theme={null} { "routes": [ { "sourceChain": "base", "sourceAsset": "USDC", "destinationChain": "spark", "destinationAsset": "BTC", "exactOutEligible": false, "fixedEligible": true, "source": { "chain": "base", "asset": "USDC", "chainDisplayName": "Base", "chainIcon": "/chain-base.svg", "contractAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "decimals": 6, "chainId": "8453" }, "destination": { "chain": "spark", "asset": "BTC", "chainDisplayName": "Spark", "chainIcon": "/chain-spark.svg", "contractAddress": null, "decimals": 8, "chainId": null } }, { "sourceChain": "spark", "sourceAsset": "BTC", "destinationChain": "base", "destinationAsset": "USDC", "exactOutEligible": false, "fixedEligible": true, "source": { "chain": "spark", "asset": "BTC", "chainDisplayName": "Spark", "chainIcon": "/chain-spark.svg", "contractAddress": null, "decimals": 8, "chainId": null }, "destination": { "chain": "base", "asset": "USDC", "chainDisplayName": "Base", "chainIcon": "/chain-base.svg", "contractAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "decimals": 6, "chainId": "8453" } } ] } ``` Each entry is a valid `(sourceChain, sourceAsset) -> (destinationChain, destinationAsset)` pair that can be used with `/estimate`, `/quote`, and `/submit`. The list is derived from the pipeline route resolver and stays in sync automatically as new pairs are added. It covers both the canonical BTC/USDB routes and the cross-chain (non-BTC) planner routes, sorted BTC-zone first; cross-chain pairs are exact-in only and never fixed-eligible. Each route carries two eligibility booleans: `exactOutEligible` (the pair accepts `amountMode=exact_out`) and `fixedEligible` (the pair can support `deliveryMode=fixed`, subject to per-partner configuration; see [Fixed delivery](#fixed-delivery-deliverymode)). Each route includes `source` and `destination` detail objects: | Field | Type | Notes | | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `chain` | `string` | Chain identifier | | `asset` | `string` | Asset symbol | | `chainDisplayName` | `string` | Human-readable chain name (e.g. `"Base"`, `"Spark"`) | | `chainIcon` | `string \| null` | Chain icon path (e.g. `"/chain-base.svg"`), relative to the API host | | `contractAddress` | `string \| null` | Token contract address (EVM, checksummed), mint address (Solana), or token ID (Spark USDB). `null` for native assets (BTC, ETH, SOL, etc.). | | `decimals` | `number` | Smallest-unit decimals for the asset (e.g. 6 for USDC, 8 for BTC) | | `chainId` | `string \| null` | Chain ID as a string. Numeric for EVM chains (e.g. `"8453"`), CAIP-2 for Solana (`"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"`). `null` for spark, bitcoin, and lightning. | ## GET /v1/orchestration/limits Amount limits per route. Call this before quoting to pre-validate amounts instead of hardcoding bounds in your integration. No authentication required; rate limited to 60 requests per minute per IP. `/limits` covers the canonical routes. Cross-chain planner routes appear on `/routes` but not on `/limits` today. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/orchestration/limits?sourceChain=bitcoin&sourceAsset=BTC&destinationChain=solana&destinationAsset=USDC" ``` Query parameters (all optional, each narrows the returned list): | Param | Notes | | ------------------ | --------------------------- | | `sourceChain` | Filter by source chain | | `sourceAsset` | Filter by source asset | | `destinationChain` | Filter by destination chain | | `destinationAsset` | Filter by destination asset | The response is `{ generatedAt, routes: [...] }`. Each entry extends the `/routes` pair shape with `direction` (`"buy"` or `"sell"`) and a `limits` object. Response (filtered to `bitcoin:BTC -> solana:USDC`): ```json theme={null} { "generatedAt": "2026-07-13T12:00:00.000Z", "routes": [ { "sourceChain": "bitcoin", "sourceAsset": "BTC", "destinationChain": "solana", "destinationAsset": "USDC", "direction": "sell", "exactOutEligible": true, "fixedEligible": true, "source": { "chain": "bitcoin", "asset": "BTC", "chainDisplayName": "Bitcoin", "chainIcon": "/btc.svg", "contractAddress": null, "decimals": 8, "chainId": null }, "destination": { "chain": "solana", "asset": "USDC", "chainDisplayName": "Solana", "chainIcon": "/chain-solana.svg", "contractAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "decimals": 6, "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" }, "limits": { "orderNotionalUsd": { "minCents": "80", "maxCents": "2000000", "source": "runtime_order_bounds" }, "exactIn": { "supported": true, "requestAmount": { "leg": "source", "chain": "bitcoin", "asset": "BTC", "minAmountSmallest": "5149", "maxAmountSmallest": null, "minUsdCents": "80", "maxUsdCents": "2000000" }, "constraints": ["order_notional_usd", "flashnet_btc_swap_input_min"] }, "exactOut": { "supported": true, "requestAmount": { "leg": "destination", "chain": "solana", "asset": "USDC", "minAmountSmallest": null, "maxAmountSmallest": null, "minUsdCents": "80", "maxUsdCents": "2000000" }, "constraints": ["order_notional_usd"] }, "fiatUsd": null, "dynamicProviderLimits": { "possible": false, "components": [], "description": null }, "constraints": [ { "id": "order_notional_usd", "amountMode": "all", "leg": "route", "source": "runtime_order_bounds", "description": "Runtime USD notional bounds apply to estimate, quote, submit, and onramp when the route amount can be priced.", "chain": null, "asset": null, "minAmountSmallest": null, "maxAmountSmallest": null, "minUsdCents": "80", "maxUsdCents": "2000000" }, { "id": "flashnet_btc_swap_input_min", "amountMode": "exact_in", "leg": "source", "source": "flashnet_static_limit", "description": "Bitcoin L1 source quotes must leave at least 5000 sats after the static deposit fee.", "chain": "bitcoin", "asset": "BTC", "minAmountSmallest": "5149", "maxAmountSmallest": null, "minUsdCents": null, "maxUsdCents": null } ] } } ] } ``` Field notes: * `orderNotionalUsd` is the operator-tuned USD notional bound for the route's direction (`source: "runtime_order_bounds"`). Treat this endpoint as authoritative; the values change without notice. * `exactIn` and `exactOut` each report `supported`, a `requestAmount` range for the amount you would send in that mode (source leg for exact-in, destination leg for exact-out), and the IDs of the `constraints` entries that apply to that mode. * `fiatUsd` reports the `amountFiatUsd` band ($1.00 to $50,000.00) and the `surfaces` that accept it (`onramp`, `pay_link`). It is non-null only on `lightning:BTC` source routes. * `dynamicProviderLimits` flags routes where provider-backed legs (`components`) impose additional quote-time limits. A live `/estimate` or `/quote` can still return `amount_too_small`, `amount_too_large`, `amount_exceeds_liquidity`, or `route_unavailable` on these routes even when the static bounds pass. * `constraints` is a typed list of every limit on the route. `source` values: `runtime_order_bounds`, `flashnet_static_limit`, `bitcoin_l1_delivery`, `provider_quote`, `fiat_amount`. The sample above shows one static limit; read the live values from this endpoint instead of hardcoding them. ## GET /v1/orchestration/estimate Lightweight price preview. This does not create a quote and does not create a durable order. Use `/estimate` for price display and browsing: it is public, stateless, allocates no deposit address, and is safe to call on every input change within its 120 requests per minute per IP limit. Use `/quote` only when the user has committed to the swap: it is authenticated and idempotent, persists the quote, allocates a deposit address, and starts a 2 minute expiry. `/estimate` requires no API key. Client keys (`fnp_`) cannot pass the fee fields (`appFees`, `affiliateId`, `affiliateIds`) on this endpoint. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/orchestration/estimate?sourceChain=base&sourceAsset=USDC&destinationChain=spark&destinationAsset=BTC&amount=100000000" ``` Query parameters: | Param | Required | Notes | | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sourceChain` | Yes | One of the supported chains | | `sourceAsset` | Yes | One of the supported assets | | `destinationChain` | Yes | One of the supported chains | | `destinationAsset` | Yes | One of the supported assets | | `amount` | Yes | Integer string. With `amountMode=exact_out`, this is the destination amount. Stablecoin exact-out amounts must be whole-cent values. | | `amountMode` | No | `"exact_in"` (default) or `"exact_out"` | | `deliveryMode` | No | `"variable"` (default) or `"fixed"`. See [Fixed delivery](#fixed-delivery-deliverymode). | | `slippageBps` | No | Preview slippage. Defaults to `100` when omitted. | | `appFees` | No | JSON-encoded array of `{recipient, fee}`. Same shape as the `/quote` body field. | | `affiliateId` | No | Registered affiliate ID (single). Requires `Authorization` header. | | `affiliateIds` | No | Registered affiliate IDs. Accepts either a comma-separated list (`a,b,c`) for the plain-string form, or a JSON-encoded array (`[...]`) when you need the `{affiliateId, feeBps}` override form. Requires `Authorization` header. | Authentication is optional. When an `Authorization` header is provided, `affiliateId` and `affiliateIds` are resolved against partner-scoped affiliate profiles. Response: ```json theme={null} { "estimatedOut": "string", "feeAmount": "string", "feeBps": 5, "totalFeeAmount": "string", "feeAsset": "USDC", "route": ["USDC", "USDB", "BTC"], "source": { "chain": "base", "asset": "USDC", "chainDisplayName": "Base", "chainIcon": "/chain-base.svg", "contractAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "decimals": 6, "chainId": "8453" }, "destination": { "chain": "spark", "asset": "BTC", "chainDisplayName": "Spark", "chainIcon": "/chain-spark.svg", "contractAddress": null, "decimals": 8, "chainId": null } } ``` Three field groups are conditional: * `roundingFeeAmount` appears only when nonzero. * `deliveryMode` appears only when fixed delivery applies (see [Fixed delivery](#fixed-delivery-deliverymode)). * `appFeeAmount`, `appFeePlatformCutAmount`, and `appFees` appear only when `appFees`, `affiliateId`, or `affiliateIds` was provided: ```json theme={null} { "estimatedOut": "string", "feeAmount": "string", "feeBps": 5, "totalFeeAmount": "string", "appFeeAmount": "string", "appFeePlatformCutAmount": "string", "appFees": [ { "recipient": "So1AffiliateOne...", "feeBps": 100, "amount": "string", "platformCutAmount": "string", "recipientAmount": "string" } ], "feeAsset": "USDC", "route": ["USDC", "USDB", "BTC"], "source": { "chain": "base", "asset": "USDC", "chainDisplayName": "Base", "chainIcon": "/chain-base.svg", "contractAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "decimals": 6, "chainId": "8453" }, "destination": { "chain": "spark", "asset": "BTC", "chainDisplayName": "Spark", "chainIcon": "/chain-spark.svg", "contractAddress": null, "decimals": 8, "chainId": null } } ``` * `feeAsset` is `USDC` for most routes, `USDB` for `BTC -> USDB`, and `BTC` for direct Lightning passthrough routes such as `lightning:BTC -> spark:BTC`. * `totalFeeAmount` equals `feeAmount + roundingFeeAmount + appFeeAmount`. Estimate totals never include sweep fees; those appear only on `/quote`. * `estimatedOut` reflects the final amount after all fees (platform + app) are deducted. * `roundingFeeAmount` is stablecoin surplus retained when output is capped to a whole-cent or exact-out target. It appears only when nonzero. * For partner invoice billing, `feeAmount` can be `"0"` because the platform fee is billed after settlement instead of deducted from the transaction. Fees owed are visible through `GET /v1/partner/dashboard/fee-invoices`. * App fees follow the same deduction order as `/quote`: platform fee first, then app fees, before the swap (buy direction) or after the swap (sell direction). * Fee plans follow the same route scope as `/quote`: any route with a supported fee settlement. Routes with no resolvable settlement (pure BTC-to-BTC, `feeAsset` `BTC`) return `400 unsupported_fee_plan`. * The same validation rules apply as `/quote`: `appFees` and `affiliateId`/`affiliateIds` are mutually exclusive, max 16 entries, sum of bps must be `<= 10000`. ## POST /v1/orchestration/quote Request a durable quote with deposit instructions. Quotes expire 2 minutes after creation. `/quote` is the execution step, not the browsing step. Price with [`/estimate`](#get-v1orchestrationestimate) while the user is deciding; call `/quote` once, when they commit. Every `/quote` call persists a quote and allocates a deposit address. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/orchestration/quote" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: quote:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "sourceChain": "base", "sourceAsset": "USDC", "destinationChain": "spark", "destinationAsset": "BTC", "amount": "100000000", "recipientAddress": "spark1...", "slippageBps": 50 }' ``` **Request fields** | Field | Type | Required | Notes | | ------------------ | ---------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sourceChain` | `Chain` | Yes | Source chain identifier | | `sourceAsset` | `Asset` | Yes | Source asset symbol | | `destinationChain` | `Chain` | Yes | Destination chain identifier | | `destinationAsset` | `Asset` | Yes | Destination asset symbol | | `amount` | `string` | Yes | Integer string in smallest units. With `amountMode=exact_out`, this is the destination amount. Stablecoin exact-out amounts must be whole-cent values, for example multiples of `10000` for 6-decimal USDC/USDB. | | `recipientAddress` | `string` | Yes | Address on the destination chain. BOLT11 invoice when `destinationChain=lightning`. | | `amountMode` | `string` | No | `"exact_in"` (default) or `"exact_out"`. `exact_out` is supported on routes with `exactOutEligible: true` on `/routes` (for example `bitcoin:BTC -> base:USDC`). | | `refundAddress` | `string` | Conditional | Required when `amountMode=exact_out` or `destinationChain=lightning`. | | `refundChain` | `Chain` | No | Refund chain. When provided without `refundAddress`, the request is rejected. | | `deliveryMode` | `string` | No | `"variable"` (default) or `"fixed"`. See [Fixed delivery](#fixed-delivery-deliverymode). | | `slippageBps` | `number` | No | `0` to `1000`. Default `50` for most routes, `200` for NEAR-ingress sources. On routes that swap through the Flashnet AMM, a runtime floor (default 10 bps, operator-tunable) applies; a below-floor value returns `400 invalid_request`. | | `appFees` | `AppFee[]` | No | Inline fee recipients. Each entry: `{ recipient: string, fee: number }`. Max 16 entries. Mutually exclusive with `affiliateId`/`affiliateIds`. | | `affiliateId` | `string` | No | Single registered affiliate ID. Requires `Authorization`. Mutually exclusive with `appFees`. | | `affiliateIds` | `AffiliateRef[]` | No | Multiple affiliate IDs. Each entry is a plain string or `{ affiliateId: string, feeBps: number }`. Max 16 entries. Requires `Authorization`. Mutually exclusive with `appFees`. | ### Fixed delivery (deliveryMode) `deliveryMode` accepts `"variable"` (default) or `"fixed"`. Fixed delivery makes the quoted `estimatedOut` a delivery commitment rather than an estimate: the order delivers the quoted amount. * Requires `amountMode=exact_in`. Any other mode returns `400 unsupported_delivery_mode`. * Only routes with `fixedEligible: true` on `/routes` support it, and availability additionally depends on per-partner operator configuration. Cross-chain routes never support it. * When fixed delivery does not apply, `/quote` and `/estimate` reject with `fixed_delivery_unavailable` (the error message states the reason), while `/onramp` silently proceeds as a normal variable onramp. * The response echoes `deliveryMode: "fixed"` only when fixed delivery applies. Treat its absence as a variable quote. `/estimate` and `/onramp` accept the same field with the same semantics. Exact-out payment-intent example (`bitcoin:BTC -> base:USDC`): ```json theme={null} { "sourceChain": "bitcoin", "sourceAsset": "BTC", "destinationChain": "base", "destinationAsset": "USDC", "amount": "100000000", "amountMode": "exact_out", "recipientAddress": "0xYourBaseUsdcAddress", "refundAddress": "bc1qyourrefundaddress...", "slippageBps": 0 } ``` Exact-out Lightning example (any buy-side source `-> lightning:BTC`): ```json theme={null} { "sourceChain": "base", "sourceAsset": "USDC", "destinationChain": "lightning", "destinationAsset": "BTC", "amount": "100000", "amountMode": "exact_out", "recipientAddress": "lnbc1u1...", "refundAddress": "0xYourBaseUsdcRefundAddress", "slippageBps": 0 } ``` Exact-in affiliate-fee example (`spark:BTC -> solana:USDC`): ```json theme={null} { "sourceChain": "spark", "sourceAsset": "BTC", "destinationChain": "solana", "destinationAsset": "USDC", "amount": "100000", "recipientAddress": "So1RecipientAddress...", "appFees": [ { "recipient": "So1AffiliateOne...", "fee": 100 }, { "recipient": "So1AffiliateTwo...", "fee": 50 } ] } ``` Exact-in affiliate-registry example (`spark:BTC -> solana:USDC`): ```json theme={null} { "sourceChain": "spark", "sourceAsset": "BTC", "destinationChain": "solana", "destinationAsset": "USDC", "amount": "100000", "recipientAddress": "So1RecipientAddress...", "affiliateId": "flashpartner" } ``` Exact-in with multiple affiliates (`spark:BTC -> solana:USDC`): ```json theme={null} { "sourceChain": "spark", "sourceAsset": "BTC", "destinationChain": "solana", "destinationAsset": "USDC", "amount": "100000", "recipientAddress": "So1RecipientAddress...", "affiliateIds": ["referrer-alice", "platform-fee"] } ``` Exact-in with per-quote feeBps override (`spark:BTC -> solana:USDC`): ```json theme={null} { "sourceChain": "spark", "sourceAsset": "BTC", "destinationChain": "solana", "destinationAsset": "USDC", "amount": "100000", "recipientAddress": "So1RecipientAddress...", "affiliateIds": [ "referrer-alice", { "affiliateId": "platform-fee", "feeBps": 25 } ] } ``` Entries in `affiliateIds` can be either plain strings or `{ affiliateId, feeBps }` objects. String entries resolve fee bps from the affiliate's registered profile. Object entries override the profile's fee bps for that quote only, without mutating the stored profile. Both forms can be mixed in the same request. Exact-in source-side affiliate-fee example (`solana:SOL -> spark:BTC`): ```json theme={null} { "sourceChain": "solana", "sourceAsset": "SOL", "destinationChain": "spark", "destinationAsset": "BTC", "amount": "100000000", "recipientAddress": "spark1...", "appFees": [ { "recipient": "Dtkxt55zEUDj6NTXGRtH8uQsACsTjoSVkXfYBEF8xkkT", "fee": 100 } ] } ``` Notes: * `amountMode` defaults to `exact_in`. * `amountMode=exact_out` is supported on routes with `exactOutEligible: true` on `/routes`. * Stablecoin exact-out amounts must be whole-cent values. A non-cent value returns `400 invalid_request`. * `refundAddress` is required when `amountMode=exact_out`. * If `Authorization` is included, send `X-Idempotency-Key`. * `appFees`, `affiliateId`, and `affiliateIds` are accepted in both amount modes on any route with a supported fee settlement (see the settlement resolution rules under "Affiliate fee models" below). Routes with no resolvable settlement, meaning pure BTC-to-BTC routes such as `lightning:BTC -> bitcoin:BTC`, reject fee plans with `400 unsupported_fee_plan` ("appFees require a route with supported fee settlement"). * `appFees` max length is `16`. * Each `appFees[i].fee` is fee bps (`1..10000`). * Sum of all app fee bps must be `<= 10000`. * `affiliateId` and `affiliateIds` must match `^[a-z0-9][a-z0-9_-]{0,63}$` after trimming and lowercasing. * Use either `appFees` or `affiliateId`/`affiliateIds`, not both; sending both returns `400 invalid_request`. `affiliateId` and `affiliateIds` can both be provided; they are merged (with `affiliateId` treated as a single-element list). * `affiliateIds` accepts up to 16 entries per quote. Each entry is either a plain affiliate ID (fee bps comes from the registered profile) or an object `{ affiliateId, feeBps }` that overrides the profile's fee bps for that quote only. The sum of all effective affiliate fee BPS must be `<= 10000`. * `affiliateId` and `affiliateIds` require authenticated quote requests. * `slippageBps` defaults to `50` when omitted (`200` for NEAR-ingress sources). * ZeroConf offers are not quote response fields. For eligible Bitcoin L1 exact-in orders, a `zeroconfOffer` may appear on the order after the Bitcoin transaction is detected. * For `destinationChain = lightning`, `recipientAddress` must be a BOLT11 invoice. In `exact_in` mode, the invoice must be amountless (0-amount). In `exact_out` mode, the invoice can be amountless or encode an amount matching the requested `amount` in sats. Affiliate fee models: * Inline recipients: * `appFees: [{ recipient: string, fee: number }]` * `fee` is fee bps per recipient (`1..10000`). * Registry by id (single): * `affiliateId: string` * profile is managed through `PUT /v1/affiliates/:affiliateId` * quote resolves recipient + fee bps from your partner-scoped profile * Registry by ids (multiple): * `affiliateIds: Array` * string entries resolve recipient and fee bps from the registered profile * object entries resolve recipient from the registered profile but use the per-quote `feeBps` override instead of the profile's bps; the stored profile is not mutated * fees from all affiliates are applied to the order; each affiliate accumulates their share independently and claims separately * Settlement chain and asset are determined by the route via `resolveFeeSettlement`: * Settles on `solana:USDC` when either side is Solana, an EVM/relay chain, or a provider-bridged destination such as TON. * Settles on `spark:USDB` when either side is `spark:USDB` and no chain from the `solana:USDC` rule is involved. * Pure BTC-to-BTC routes (e.g. `lightning:BTC -> bitcoin:BTC`) have no resolvable settlement and reject fee plans with 400 `unsupported_fee_plan`. * Execution point: * If the source chain matches the settlement chain (e.g. solana source on `solana:USDC` settlement), fees are deducted on the source-side stablecoin leg before bridge/swap continuation. * If the source chain does not match the settlement chain, fees are deducted on whichever stablecoin leg the route exposes: typically the destination-side USDC payout leg for sells, or the bridge leg for supported chain payouts. * For source-native routes like `SOL -> BTC` and `ETH -> BTC`, fees are deducted on the stablecoin leg after ingress conversion (`SOL|ETH -> USDC`). * Quote math order: * platform fee (`feeAmount`) * optional sweep fee (`sweepFeeAmount`, USDC-source routes only) * app fee allocation (`appFeeAmount`, `appFees`) * 20% platform cut retained from each app fee (`appFeePlatformCutAmount`); remaining 80% goes to the fee recipient * net amount proceeds to route execution * Flashnet retains 20% of each app/affiliate fee. The quote response shows the split per recipient: `amount` (gross fee charged to the user), `platformCutAmount` (20% retained by Flashnet), and `recipientAmount` (80% paid to the fee recipient). * `appFees` recipients are validated against the settlement-chain address format. * For `affiliateId` and `affiliateIds`, the affiliate profile must have a `payoutAddress` configured (set via `PUT /v1/affiliates/:affiliateId`). The payout chain/asset can be any destination supported by the live route table, not just base or solana. Response (exact-out sample): ```json theme={null} { "quoteId": "q_...", "depositAddress": "bc1q...", "amountIn": "250000", "estimatedOut": "100000000", "feeAmount": "100000", "totalFeeAmount": "100000", "feeAsset": "USDC", "feeBps": 5, "route": ["BTC", "USDB", "USDC"], "expiresAt": "2026-02-04T02:00:00.000Z", "amountMode": "exact_out", "targetAmountOut": "100000000", "requiredAmountIn": "250000", "maxAcceptedAmountIn": "250050", "inputBufferBps": 2 } ``` Response (exact-in with appFees sample): ```json theme={null} { "quoteId": "q_...", "depositAddress": "spark1...", "amountIn": "100000", "estimatedOut": "24825000", "feeAmount": "25000", "totalFeeAmount": "62125", "appFeeAmount": "37125", "appFeePlatformCutAmount": "7425", "appFees": [ { "affiliateId": "flashpartner", "recipient": "So1AffiliateOne...", "feeBps": 50, "amount": "24750", "platformCutAmount": "4950", "recipientAmount": "19800" }, { "recipient": "So1AffiliateTwo...", "feeBps": 50, "amount": "12375", "platformCutAmount": "2475", "recipientAmount": "9900" } ], "feeAsset": "USDC", "feeBps": 5, "route": ["BTC", "USDB", "USDC"], "expiresAt": "2026-02-04T02:00:00.000Z" } ``` Response (exact-in source-side appFees sample): ```json theme={null} { "quoteId": "q_...", "depositAddress": "So1DepositAddress...", "amountIn": "100000000", "estimatedOut": "11643", "feeAmount": "7893", "totalFeeAmount": "86749", "appFeeAmount": "78856", "appFeePlatformCutAmount": "15771", "appFees": [ { "recipient": "Dtkxt55zEUDj6NTXGRtH8uQsACsTjoSVkXfYBEF8xkkT", "feeBps": 50, "amount": "78856", "platformCutAmount": "15771", "recipientAmount": "63085" } ], "feeAsset": "USDC", "feeBps": 5, "route": ["SOL", "USDC", "USDB", "BTC"], "expiresAt": "2026-02-11T23:55:36.296Z" } ``` Field notes: * `depositAddress` depends on `sourceChain`: * EVM and Solana sources: chain address that receives the source asset * `spark`: Spark address that receives BTC or USDB * `bitcoin`: Bitcoin L1 address * `lightning`: BOLT11 invoice to pay * `totalFeeAmount` equals `feeAmount + roundingFeeAmount + appFeeAmount + sweepFeeAmount`. `roundingFeeAmount` and `sweepFeeAmount` appear in the response only when nonzero. * `sweepFeeAmount` is returned on some USDC-source routes when a sweep fee is configured. * `roundingFeeAmount` is returned when stablecoin surplus is retained instead of delivered. * `appFeeAmount` and `appFees` are returned when `appFees` or `affiliateId` was requested. * `appFeePlatformCutAmount` is the sum of Flashnet's 20% cut across all app fees. * `appFees[*].amount` is the gross fee in settlement-chain USDC smallest units (what the user pays). * `appFees[*].platformCutAmount` is Flashnet's 20% cut of that fee. * `appFees[*].recipientAmount` is the 80% paid to the fee recipient. * `appFees[*].affiliateId` is present when the quote used `affiliateId`. * `lightningReceiveRequestId` is only present for `sourceChain = lightning` quotes. * `feeAsset` is `USDB` for `BTC -> USDB` quotes, `BTC` for direct Lightning passthrough quotes, and `USDC` otherwise. * `targetAmountOut`, `requiredAmountIn`, `maxAcceptedAmountIn`, and `inputBufferBps` are present for exact-out quotes. * `inputBufferBps` is currently `2` when `slippageBps=0`, otherwise `0`. ## POST /v1/orchestration/submit Create an order from a quote after you have initiated the source deposit. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/orchestration/submit" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: submit:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "quoteId": "q_...", "txHash": "0x..." }' ``` **Request fields** | Field | Type | Required | Notes | | --------------------------- | -------- | ----------- | ----------------------------------------------------------------------------------------------------------------------- | | `quoteId` | `string` | Yes | Quote ID (`q_...`) | | `txHash` | `string` | Conditional | Required for EVM and Solana sources. Format varies by chain (EVM: `0x`-prefixed 64-char hex; Solana: base58 signature). | | `sourceAddress` | `string` | No | Sender address for EVM and Solana sources. Required when depositing to a shared address. | | `sparkTxHash` | `string` | Conditional | Required for `sourceChain=spark`. UUID, 32-char hex, or 64-char token tx hash. | | `sourceSparkAddress` | `string` | No | Sender Spark address. Required when depositing to a shared Spark address. | | `bitcoinTxid` | `string` | Conditional | Required for `sourceChain=bitcoin`. 64-character hex transaction ID. | | `bitcoinVout` | `number` | No | Output index. Auto-resolved from transaction outputs when omitted. | | `lightningReceiveRequestId` | `string` | No | For `sourceChain=lightning`. Auto-populated from quote when omitted. | The request body shape depends on `sourceChain`. Each source chain uses a different subset of these fields. See the examples below. Headers: ```bash theme={null} Authorization: Bearer fn_... X-Idempotency-Key: ``` Request body shape depends on the quote `sourceChain`. Base/Solana sources (`sourceChain = base|solana`): ```json theme={null} { "quoteId": "q_...", "txHash": "0x...", "sourceAddress": "0x..." } ``` `sourceAddress` is optional but recommended. It is required when depositing to a shared address. When present, deposit verification requires the sender to match. Spark sources (`sourceChain = spark`): ```json theme={null} { "quoteId": "q_...", "sparkTxHash": "spark_transfer_id_or_token_tx_hash", "sourceSparkAddress": "spark1..." } ``` `sourceSparkAddress` is optional but recommended. It is required when depositing to a shared Spark address. When present, deposit verification requires the sender to match. Bitcoin L1 sources (`sourceChain = bitcoin`): ```json theme={null} { "quoteId": "q_...", "bitcoinTxid": "txid" } ``` `bitcoinVout` is optional. When omitted, the engine resolves it by scanning the transaction outputs for the one that pays the quote's deposit address. You can still provide it explicitly if needed. Lightning sources (`sourceChain = lightning`): ```json theme={null} { "quoteId": "q_...", "lightningReceiveRequestId": "string" } ``` `lightningReceiveRequestId` can be omitted. When omitted, the API uses the value embedded in the quote. The `txHash` field is validated per source chain: * **EVM chains**: `0x`-prefixed 64-character hex string * **Solana**: Base58-encoded signature (64-90 characters) * **Bitcoin**: 64-character hex transaction ID * **Spark**: UUID format, 32-character hex, or 64-character token transaction hash * **Lightning**: Automatically populated from the quote's receive request ID Response: ```json theme={null} { "orderId": "ord_...", "status": "processing" } ``` If an order already exists for the same `(sourceChain, sourceTxHash[, sourceTxVout])`, `submit` returns the existing `{ orderId, status }`. ## POST /v1/orchestration/onramp Combined quote and submit for Lightning sell flows. Creates a Lightning invoice and order in a single call. Designed for fiat onramp integrations where the user pays via a Lightning-compatible app. The source is always `lightning:BTC`. Supported destinations are the `lightning:BTC` source pairs on `GET /v1/orchestration/routes`. The `amount` field is BTC in sats. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/orchestration/onramp" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: onramp:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "destinationChain": "spark", "destinationAsset": "USDB", "recipientAddress": "spark1...", "amount": "100000" }' ``` Headers: ```bash theme={null} Authorization: Bearer fn_... X-Idempotency-Key: ``` **Request fields** | Field | Type | Required | Notes | | ------------------ | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `destinationChain` | `Chain` | Yes | Target chain | | `destinationAsset` | `Asset` | Yes | Target asset | | `recipientAddress` | `string` | Yes | Address on the destination chain | | `amount` | `string` | Yes | Integer string. With `exact_in`, BTC in sats. With `exact_out`, the destination amount in smallest units. Stablecoin exact-out amounts must be whole-cent values. | | `amountMode` | `string` | No | `"exact_in"` (default) or `"exact_out"`. `exact_out` requires the pair to have `exactOutEligible: true` on `/routes`; the direct BTC passthrough destinations do not. | | `slippageBps` | `number` | No | Minimum, maximum, and default `1000` (10%). Values below 1000 are clamped up, so the effective value is always 1000. | | `deliveryMode` | `string` | No | `"variable"` (default) or `"fixed"`. When fixed delivery does not apply, the onramp proceeds as a normal variable onramp. See [Fixed delivery](#fixed-delivery-deliverymode). | | `appFees` | `AppFee[]` | No | Inline fee recipients. Not supported on direct BTC passthrough routes. | | `affiliateId` | `string` | No | Registered affiliate ID (single). Requires stablecoin settlement path. | | `affiliateIds` | `AffiliateRef[]` | No | Registered affiliate IDs (multiple, max 16). Requires stablecoin settlement path. | Request body: ```json theme={null} { "destinationChain": "solana", "destinationAsset": "USDC", "recipientAddress": "So1...", "amount": "100000", "slippageBps": 50 } ``` Notes: * `amountMode=exact_out` is supported where the `lightning:BTC` pair has `exactOutEligible: true` on `/routes`. The direct BTC passthrough destinations do not support it. * Stablecoin exact-out amounts must be whole-cent values. For 6-decimal stables, send multiples of `10000`, for example `"50000000"` for \$50.00. * Affiliate fees require a stablecoin settlement path and are not supported on the direct BTC passthrough routes. Response: ```json theme={null} { "orderId": "ord_...", "quoteId": "q_...", "depositAddress": "lnbc1000n1pj...", "paymentLinks": { "cashApp": "https://cash.app/launch/lightning/lnbc1000n1pj...", "shortUrl": "https://orchestration.flashnet.xyz/pay/a3xKm2Rq" }, "amountIn": "100000", "estimatedOut": "96543210", "feeAmount": "38536", "feeBps": 40, "totalFeeAmount": "38536", "feeAsset": "USDC", "route": ["BTC", "USDB", "USDC"], "expiresAt": "2026-01-15T12:04:00.000Z" } ``` The `paymentLinks.cashApp` URL is a deeplink that opens a Lightning-compatible payment app to pay the invoice. On mobile, redirect the user to this URL. On desktop, display it as a QR code. `paymentLinks.shortUrl` is a short redirect URL that 302s to the payment deeplink. Use this when sharing payment links in text messages, emails, or other contexts where the full URL is too long. The short URL is always present when `PUBLIC_BASE_URL` is configured on the server. The Lightning invoice expires at `expiresAt`: 24 hours from creation for exact-in, 5 minutes for exact-out and for fixed-delivery exact-in. If the user doesn't pay in time, create a new onramp order. Field notes: * `feeBps` reflects the platform fee rate for the specific route. * `feeAsset` is `BTC` for the direct BTC passthrough routes, `USDB` for `lightning:BTC -> spark:USDB`, and the destination-side stable asset for most other routes. * `route` is the asset path, not the pipeline step list. * For `lightning:BTC -> bitcoin:BTC`, the final on-chain withdrawal also pays the network withdrawal fee quoted at delivery time. That fee is separate from Flashnet's platform fee. For the full integration guide with code examples, see [Fiat Onramp](/products/orchestration/onramp). ## GET /v1/orchestration/order Look up a quote and its associated order by quote ID. Designed for submissionless flows where partners poll after creating a quote and directing a user to deposit. Returns the quote state alongside the order. When no order exists yet (deposit not detected), `order` is `null` and the quote's `expired` field indicates whether the deposit window is still open. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/orchestration/order?quoteId=q_..." \ -H "Authorization: Bearer fn_..." ``` Headers: ```bash theme={null} Authorization: Bearer fn_... ``` Query parameters: | Param | Required | Notes | | --------- | -------- | ------------------ | | `quoteId` | Yes | Quote id (`q_...`) | Response when no order exists yet: ```json theme={null} { "quote": { "id": "q_...", "kind": "sell", "sourceChain": "spark", "sourceAsset": "BTC", "destinationChain": "solana", "destinationAsset": "USDC", "amountIn": "100000", "estimatedOut": "96500000", "feeBps": 5, "feeAmount": "4830", "depositAddress": "spark1...", "recipientAddress": "So1...", "slippageBps": 50, "expiresAt": "2026-02-04T02:00:00.000Z", "expired": false, "createdAt": "2026-02-04T01:58:00.000Z" }, "order": null, "stages": [] } ``` Response when an order has been created: ```json theme={null} { "quote": { "id": "q_...", "kind": "sell", "sourceChain": "spark", "sourceAsset": "BTC", "destinationChain": "solana", "destinationAsset": "USDC", "amountIn": "100000", "estimatedOut": "96500000", "feeBps": 5, "feeAmount": "4830", "depositAddress": "spark1...", "recipientAddress": "So1...", "slippageBps": 50, "expiresAt": "2026-02-04T02:00:00.000Z", "expired": true, "createdAt": "2026-02-04T01:58:00.000Z" }, "order": { "id": "ord_...", "type": "order", "status": "swapping", "quoteId": "q_...", "sourceChain": "spark", "sourceAsset": "BTC", "destinationChain": "solana", "destinationAsset": "USDC", "recipientAddress": "So1...", "amountIn": "100000", "amountOut": null, "feeBps": 5, "feeAmount": "4830", "createdAt": "2026-02-04T02:01:00.000Z" }, "stages": [ { "name": "deposit_confirmed", "status": "completed", "completedAt": "2026-02-04T02:01:05.000Z" } ] } ``` The `order` object uses the same shape as `GET /v1/orchestration/status`, including optional `swap`, `paymentIntent`, `zeroconfOffer`, `feePlan`, and `feePayouts` metadata when present. Polling pattern for submissionless flows: 1. Create a quote via `POST /v1/orchestration/quote`. 2. Direct the user to deposit to `depositAddress`. 3. Poll `GET /v1/orchestration/order?quoteId=q_...` until `order` is non-null. 4. Once `order` appears, continue polling or switch to webhooks to track execution. When `quote.expired` is `true` and `order` is still `null`, no deposit has been detected yet. Expired quotes that receive a late deposit are always repriced at the live market rate at detection time and create an order; execution is still bounded by the quote's `slippageBps`. ## GET /v1/orchestration/status Check an order's current state. Authentication is optional: * Without `Authorization`: returns a redacted order record. * With `Authorization`: returns the full order record for your partner only. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/orchestration/status?id=ord_..." \ -H "Authorization: Bearer fn_..." ``` Provide exactly one query parameter: | Param | Notes | | --------- | -------------------------------------------------------- | | `id` | Order id (`ord_...`) | | `quoteId` | Quote id (`q_...`) | | `txHash` | Source transaction identifier (see `order.sourceTxHash`) | `txHash` lookup returns the most recently created order with that `sourceTxHash`. For Bitcoin L1 deposits where multiple orders can share the same txid, prefer `id` from webhooks for exact attribution. Possible `order.status` values: * `processing` * `confirming` * `bridging` * `swapping` * `awaiting_approval` * `refunding` * `delivering` * `completed` * `failed` * `expired` * `unfulfilled` * `refunded` Internal `paused` orders are returned as `processing` on partner-visible status responses. Response (authenticated): ```json theme={null} { "order": { "id": "ord_...", "type": "order", "status": "refunding", "quoteId": "q_...", "sourceChain": "bitcoin", "sourceAsset": "BTC", "sourceAddress": null, "sourceTxHash": "", "sourceTxVout": 0, "depositAddress": "bc1q...", "destinationChain": "base", "destinationAsset": "USDC", "recipientAddress": "0x...", "amountIn": "250000", "amountOut": null, "feeBps": 5, "feeAmount": "100000", "slippageBps": 50, "errorCode": "slippage_exceeded", "errorMessage": "Pool moved past slippage tolerance between deposit and execution", "createdAt": "2026-02-04T01:30:00.000Z", "updatedAt": "2026-02-04T01:30:10.000Z", "completedAt": null, "paymentIntent": { "version": 1, "amountMode": "exact_out", "targetAmountOut": "100000000", "requiredAmountIn": "250000", "maxAcceptedAmountIn": "250050", "inputBufferBps": 2, "actualAmountIn": "249900", "refundAddress": "bc1q...", "exactOutExecution": "strict" }, "feePlan": { "version": 1, "settlementChain": "solana", "settlementAsset": "USDC", "appFees": [ { "affiliateId": "flashpartner", "recipient": "So1AffiliateOne...", "feeBps": 50 }, { "recipient": "So1AffiliateTwo...", "feeBps": 50 } ] }, "feePayouts": { "version": 1, "entries": [ { "idempotencyKey": "order:ord_...:full:appfee:0", "leg": "full", "chain": "solana", "role": "app_fee", "affiliateId": "flashpartner", "recipient": "So1AffiliateOne...", "feeBps": 50, "amount": "24750", "platformCutAmount": "4950", "recipientAmount": "19800", "txHash": "5kW...", "recordedAt": "2026-02-04T01:35:00.000Z" }, { "idempotencyKey": "order:ord_...:full:payout", "leg": "full", "chain": "solana", "role": "recipient_payout", "recipient": "So1RecipientAddress...", "feeBps": null, "amount": "2475000", "platformCutAmount": null, "recipientAmount": null, "txHash": "3hN...", "recordedAt": "2026-02-04T01:35:02.000Z" } ] } }, "stages": [ { "name": "deposit_confirmed", "status": "completed", "completedAt": "2026-02-04T01:30:05.000Z" }, { "name": "refund_requested", "status": "completed", "completedAt": "2026-02-04T01:30:10.000Z" } ] } ``` With `Authorization`, the `order` object is the full public operation record. Without it, some fields are omitted. `amountIn` and `feeAmount` reflect the actual processed deposit. When the on-chain deposit differs from the original quote, the engine updates these fields before execution. Any positive deposit amount is accepted. The `stages` array will include `amount_reconciled` when this adjustment occurred. Depending on route and progress, the order can include: * `flashnetRequestId` when a Flashnet swap has executed * `sparkTxHash` when a Spark transfer, withdrawal, or Lightning action is created * `swap` metadata when a Flashnet swap leg has been recorded * `paymentIntent` metadata for exact-out orders * `zeroconfOffer` when a ZeroConf offer has been generated for a Bitcoin L1 deposit (see [ZeroConf offer fields](/products/orchestration/api/approval-flows#zeroconf-offer-fields)) * `feePlan` when quote-level `appFees` or `affiliateId` was requested * `feePayouts` as affiliate and recipient payout legs are executed * `feePayouts.entries[*].role` is `app_fee`, `recipient_payout`, `platform_fee`, or `fee_custody` * `feePayouts.entries[*].affiliateId` is present for app-fee entries derived from `affiliateId` * `feePayouts.entries[*].leg` is `full` for a completed recipient or fee-custody transfer, `holdback` for an affiliate fee retained for a later claim, or legacy `instant` on historical multi-leg records. It is unrelated to ZeroConf. * `supersededByOperationId` on an order left `unfulfilled` whose Bitcoin deposit was replaced (e.g. RBF'd) and recovered by a later order — the id of that successor order. See [Recovered (superseded) orders](/products/orchestration/api/webhook-events#recovered-superseded-orders). * `recoveredFromOperationId` on a successor order that recovered a replaced deposit — the id of the original `unfulfilled` order it took over. ## GET /v1/orchestration/history List your orders filtered by `recipientAddress`. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/orchestration/history?address=So1RecipientAddress...&limit=50" \ -H "Authorization: Bearer fn_..." ``` Headers: ```bash theme={null} Authorization: Bearer fn_... ``` Query parameters: | Param | Required | Notes | | --------- | -------- | ----------------------------------------------------------------------------------------- | | `address` | Yes | Recipient address on the destination chain (or BOLT11 invoice for Lightning destinations) | | `status` | No | Exact match on the order status | | `limit` | No | Default `50`, max `200` | | `offset` | No | Default `0` | Response: ```json theme={null} { "orders": [{ "id": "ord_...", "status": "completed" }] } ``` History entries use the same order shape as `GET /v1/orchestration/status`, including optional `swap`, `paymentIntent`, and `zeroconfOffer` metadata when present. # Resource Management Source: https://docs.flashnet.xyz/products/orchestration/api/resources ## Affiliates Affiliate profiles let you register a partner-scoped app-fee recipient with a payout destination on any supported chain under a stable `affiliateId`, then reference that id in quote requests. ### PUT /v1/affiliates/:affiliateId Create or update an affiliate profile. ```bash theme={null} curl -sS -X PUT "https://orchestration.flashnet.xyz/v1/affiliates/flashpartner" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: affiliate:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "feeBps": 50, "payoutChain": "solana", "payoutAsset": "USDC", "payoutAddress": "So1YourPayoutAddress" }' ``` Request (EVM USDC payout): ```json theme={null} { "feeBps": 50, "payoutChain": "base", "payoutAsset": "USDC", "payoutAddress": "0xYourBaseUsdcAddress" } ``` All four fields are required. `payoutAddress` is where affiliate fee claims are settled, and it is validated against the chosen `payoutChain`'s address format. Any `(chain, asset)` tuple in the live payout route table is accepted. Call [`GET /v1/affiliate-dashboard/payout-destinations`](#get-v1affiliate-dashboardpayout-destinations) for the current set. The server validates the pair on `PUT /v1/affiliates/:affiliateId` and rejects unsupported combinations with `invalid_payout_destination`. Lightning is intentionally excluded. Lightning invoices are single-use and amount-bound, so they do not fit a static payout-address model. Response: ```json theme={null} { "affiliate": { "affiliateId": "flashpartner", "feeBps": 50, "payoutChain": "solana", "payoutAsset": "USDC", "payoutAddress": "So1YourPayoutAddress", "enabled": true, "createdAt": "2026-02-12T18:00:00.000Z", "updatedAt": "2026-02-12T18:00:00.000Z" } } ``` ### GET /v1/affiliates List affiliate profiles for your partner. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/affiliates?includeDisabled=true" \ -H "Authorization: Bearer fn_..." ``` Query params: * `includeDisabled` (`true|false`, default `false`) * `limit` (default `200`, max `1000`) * `offset` (default `0`) ### DELETE /v1/affiliates/:affiliateId Disable an affiliate profile. ```bash theme={null} curl -sS -X DELETE "https://orchestration.flashnet.xyz/v1/affiliates/flashpartner" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: affiliate-disable:$(uuidgen)" ``` Response: ```json theme={null} { "ok": true } ``` ### POST /v1/affiliates/:affiliateId/claim Create a claim for the full available affiliate fee balance. The available balance reflects the affiliate's 80% share after Flashnet's 20% platform cut. Minimum claim is \$1 USDC. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/affiliates/flashpartner/claim" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: claim:$(uuidgen)" ``` No request body. The claim amount is the full available balance. Response: ```json theme={null} { "claimId": "acl_...", "affiliateId": "flashpartner", "amount": "5000000", "status": "processing", "createdAt": "2026-02-27T12:00:00.000Z" } ``` Claims require a payout destination configured on the affiliate profile (`payoutChain`, `payoutAsset`, `payoutAddress`). Claims are settled out of the same ledger where the fees accrued and delivered to the configured payout destination. How an order's fees resolve to a settlement ledger is documented in [Fees](/products/orchestration/concepts/fees). Valid payout destinations come from [`GET /v1/affiliate-dashboard/payout-destinations`](#get-v1affiliate-dashboardpayout-destinations). ### GET /v1/affiliate-dashboard/payout-destinations List every `(chain, asset)` tuple supported as a payout destination. The list is derived live from the orchestrator's pipeline route table, so it stays in sync with backend reality without doc updates whenever new supported chains are added. Use this for discovery instead of `GET /v1/orchestration/routes` (which only returns user-facing orchestration routes, not payout routes). ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/affiliate-dashboard/payout-destinations" \ -H "Authorization: Bearer fn_..." ``` Response: ```json theme={null} { "destinations": [ { "chain": "arbitrum", "asset": "ETH" }, { "chain": "arbitrum", "asset": "USDC" }, { "chain": "bitcoin", "asset": "BTC" }, { "chain": "solana", "asset": "USDC" }, { "chain": "spark", "asset": "USDB" } ] } ``` Entries are sorted alphabetically (case-insensitive) by `(chain, asset)`. Lightning is intentionally excluded. The example above is truncated. Call the endpoint for the current canonical set. ### GET /v1/affiliates/:affiliateId/claims List claims for an affiliate. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/affiliates/flashpartner/claims?limit=50" \ -H "Authorization: Bearer fn_..." ``` Query params: * `limit` (default `50`, max `200`) * `offset` (default `0`) ## Partner webhooks Webhooks deliver order status changes for your partner account. ### POST /v1/webhooks Register an endpoint. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/webhooks" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: webhook:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/flashnet/webhooks" }' ``` Response: ```json theme={null} { "webhookId": "wh_...", "secret": "hex string" } ``` `secret` is only returned at creation time. Store it. ### DELETE /v1/webhooks/:id Disable an endpoint. ```bash theme={null} curl -sS -X DELETE "https://orchestration.flashnet.xyz/v1/webhooks/wh_..." \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: webhook-disable:$(uuidgen)" ``` Response: ```json theme={null} { "ok": true } ``` See [Webhooks](/products/orchestration/webhooks) for signature verification, payload shape, and retry behavior. ## Partner fee invoices Invoice billing is enabled by Flashnet for approved partner-route combinations. Partners cannot opt into it by request. When invoice billing is enabled, the transaction does not deduct the platform fee from the user flow. The billed bps accrue to the partner and can be reviewed from the dashboard API. ### GET /v1/partner/dashboard/fee-invoices Read the partner-scoped fee amount owed, split between uninvoiced accruals and draft or issued invoices. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/partner/dashboard/fee-invoices?limit=25" \ -H "Authorization: Bearer fn_..." ``` Query parameters: | Param | Required | Notes | | ------- | -------- | ------------------------------------------------------------- | | `limit` | No | Number of recent invoices to return. Default `25`, max `100`. | Response: ```json theme={null} { "asOf": "2026-05-04T18:00:00.000Z", "totals": [ { "feeAsset": "USDC", "totalFeeAmount": "1000000", "pendingAccrualAmount": "200000", "pendingAccrualCount": 1, "outstandingInvoiceAmount": "800000", "outstandingInvoiceCount": 1 } ], "pending": [ { "partnerId": "ptnr_...", "partnerName": "Example Partner", "billingPeriod": "2026-05", "feeAsset": "USDC", "accrualCount": 1, "totalVolumeAmount": "25000000", "totalFeeAmount": "200000", "oldestAccruedAt": "2026-05-04T17:30:00.000Z", "newestAccruedAt": "2026-05-04T17:30:00.000Z" } ], "recentInvoices": [ { "id": "pfi_...", "partnerId": "ptnr_...", "partnerName": "Example Partner", "billingPeriod": "2026-05", "feeAsset": "USDC", "totalVolumeAmount": "100000000", "totalFeeAmount": "800000", "accrualCount": 1, "status": "draft", "createdBy": "admin@example.com", "createdAt": "2026-05-04T18:00:00.000Z", "updatedAt": "2026-05-04T18:00:00.000Z", "issuedAt": null, "paidAt": null, "voidedAt": null } ] } ``` All amounts are in the fee asset's smallest units. `totalFeeAmount` in `totals` is `pendingAccrualAmount + outstandingInvoiceAmount`. Paid and void invoices are excluded from outstanding totals. ## Accumulation addresses Accumulation addresses are reusable deposit addresses on Solana and any supported chain. Each deposit automatically creates an order that delivers `BTC` or `USDB` on Spark. ### POST /v1/accumulation-addresses Create an accumulation address. Inline app fee recipients: ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/accumulation-addresses" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: accumulation:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "label": "optional", "sourceChain": "solana", "sourceAsset": "USDC", "destinationAsset": "BTC", "recipientSparkAddress": "spark1...", "feeBps": 5, "slippageBps": 50, "appFees": [ { "recipient": "So1FeeRecipient...", "fee": 50 } ] }' ``` Registered affiliates with per-address bps override: ```json theme={null} { "label": "optional", "sourceChain": "solana", "sourceAsset": "USDC", "destinationAsset": "BTC", "recipientSparkAddress": "spark1...", "feeBps": 5, "slippageBps": 50, "affiliateIds": [ "referrer-alice", { "affiliateId": "platform-fee", "feeBps": 25 } ] } ``` `appFees` and `affiliateIds` are both optional and mutually exclusive (use one or the other, not both). `affiliateIds` entries accept the same `string | { affiliateId, feeBps }` union shape as `POST /v1/orchestration/quote`. The fee plan you submit is validated at creation time and frozen onto the address. Every deposit processed through this address replays the same plan without re-evaluating affiliate profiles. See [Reusable Addresses](/products/orchestration/reusable-addresses#how-do-app-fees-and-affiliates-work) for details. Response: ```json theme={null} { "accumulationAddressId": "acu_...", "sourceChain": "solana", "sourceAsset": "USDC", "destinationAsset": "BTC", "recipientSparkAddress": "spark1...", "feeBps": 5, "slippageBps": 50, "enabled": true, "depositAddress": "So1...", "createdAt": "2026-02-04T01:00:00.000Z", "appFees": [ { "affiliateId": "platform-fee", "recipient": "So1PlatformPayoutAddress...", "feeBps": 25 } ], "subscriptions": [] } ``` The response `appFees` array reflects the frozen fee plan. Each entry always has `recipient` and `feeBps`. `affiliateId` is present for entries backed by a registered affiliate profile (the `recipient` is pulled from the profile's `payoutAddress`); inline `appFees` entries omit `affiliateId`. The array is omitted from the response when no fee plan was configured at creation. ### POST /v1/accumulation-addresses/sync Re-register your enabled Solana-sourced accumulation addresses into the configured Helius deposit-detection webhooks. Relay-backed addresses (EVM and other non-Solana sources) are not touched; use the per-address reindex endpoint for those. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/accumulation-addresses/sync" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: accumulation-sync:$(uuidgen)" ``` ### POST /v1/accumulation-addresses/:idOrAddress/reindex Ask Relay to rescan one Relay-backed deposit address for missed deposits. `:idOrAddress` accepts the `acu_...` id or the reusable deposit address itself. If Relay finds a balance, it queues the sweep and the normal fill-detection path creates the order. Native Solana accumulation addresses return `400`; they have no Relay deposit address to rescan. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/accumulation-addresses/acu_.../reindex" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: accumulation-reindex:$(uuidgen)" ``` Like address creation, this endpoint accepts client keys with the `accumulation:create` scope in addition to server keys. ### GET /v1/accumulation-addresses List accumulation addresses. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/accumulation-addresses" \ -H "Authorization: Bearer fn_..." ``` ### GET /v1/accumulation-addresses/:id Get a single accumulation address, including subscription status. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/accumulation-addresses/acu_..." \ -H "Authorization: Bearer fn_..." ``` ### DELETE /v1/accumulation-addresses/:id Disable an accumulation address. ```bash theme={null} curl -sS -X DELETE "https://orchestration.flashnet.xyz/v1/accumulation-addresses/acu_..." \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: accumulation-disable:$(uuidgen)" ``` ## Liquidation addresses Liquidation addresses are reusable Bitcoin L1 deposit addresses. Each deposit automatically creates an order that delivers to any supported chain and asset. ### POST /v1/liquidation-addresses Create a liquidation address. Inline app fee recipients: ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/liquidation-addresses" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: liquidation:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "label": "optional", "destinationChain": "ethereum", "destinationAsset": "USDC", "destinationAddress": "0x...", "slippageBps": 50, "feeBps": 5, "appFees": [ { "recipient": "So1FeeRecipient...", "fee": 50 } ] }' ``` Registered affiliates with per-address bps override: ```json theme={null} { "label": "optional", "destinationChain": "ethereum", "destinationAsset": "USDC", "destinationAddress": "0x...", "slippageBps": 50, "feeBps": 5, "affiliateIds": [ "referrer-alice", { "affiliateId": "platform-fee", "feeBps": 25 } ] } ``` `appFees` and `affiliateIds` are both optional and mutually exclusive. `affiliateIds` entries accept the same `string | { affiliateId, feeBps }` union shape as `POST /v1/orchestration/quote`. The fee plan is frozen at creation time and replayed into every Bitcoin L1 deposit processed through the address. `appFees` inline recipients must be valid addresses on the fee settlement chain (Solana USDC for most routes, Spark USDB for USDB destinations), not the destination chain. See [Reusable Addresses](/products/orchestration/reusable-addresses#how-do-app-fees-and-affiliates-work) for details. Response: ```json theme={null} { "liquidationAddressId": "liq_...", "sparkAddress": "spark1...", "l1DepositAddress": "bc1...", "destination": { "chain": "ethereum", "asset": "USDC", "address": "0x..." }, "feeBps": 5, "slippageBps": 50, "enabled": true, "createdAt": "2026-02-04T01:00:00.000Z", "appFees": [ { "affiliateId": "platform-fee", "recipient": "So1PlatformPayoutAddress...", "feeBps": 25 } ] } ``` The response `appFees` array reflects the frozen fee plan. Each entry always has `recipient` and `feeBps`. `affiliateId` is present for entries backed by a registered affiliate profile; inline `appFees` entries omit it. The array is omitted when no fee plan was configured. ### GET /v1/liquidation-addresses List enabled liquidation addresses. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/liquidation-addresses" \ -H "Authorization: Bearer fn_..." ``` ### GET /v1/liquidation-addresses/:id Get a single liquidation address (enabled or disabled). ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/liquidation-addresses/liq_..." \ -H "Authorization: Bearer fn_..." ``` ### GET /v1/liquidation-addresses/orders List the orders for a liquidation address. Provide exactly one of `id` or `label`; sending both, neither, or an empty label returns `400`. An unknown or non-owned `id` returns `404`; a `label` matching none of your addresses returns an empty list. `label` matches every address you own with that exact label (labels are not unique). ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/liquidation-addresses/orders?id=liq_...&limit=50" \ -H "Authorization: Bearer fn_..." ``` Query params: * `id`: a single liquidation address id (exactly one of `id` / `label` is required) * `label`: every liquidation address you own with this exact label * `status` (optional): filter to one order status; `paused` orders are hidden unless `status=paused` * `limit` (default `50`, max `200`) * `offset` (default `0`) Returns `{ orders, nextOffset }`. Each order matches the `GET /v1/orchestration/status` order shape, including terminal (completed, failed, refunded) orders. Requires a server key; client keys are rejected. ### DELETE /v1/liquidation-addresses/:id Disable a liquidation address. ```bash theme={null} curl -sS -X DELETE "https://orchestration.flashnet.xyz/v1/liquidation-addresses/liq_..." \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: liquidation-disable:$(uuidgen)" ``` ### POST /v1/liquidation-addresses/sync-blockdaemon Synchronize enabled liquidation addresses into Blockdaemon Streaming watchlists. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/liquidation-addresses/sync-blockdaemon" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: liquidation-sync:$(uuidgen)" ``` ## Pay links Pay links are durable payment links. Each click on the public `/pay/:shortId` URL creates a fresh Lightning order delivered to the link's recipient. All pay-link endpoints require a server key (`fn_...`); client keys are rejected. See [Pay Links](/products/orchestration/pay-links) for the product flow and frontend integration. ### POST /v1/pay-links Create a pay link. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/pay-links" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: paylink:$(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "destinationChain": "solana", "destinationAsset": "USDC", "recipientAddress": "So1YourSolanaAddress...", "amountFiatUsd": "50.00", "label": "Tip jar" }' ``` Request body: | Field | Required | Type | Notes | | ------------------ | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `destinationChain` | Yes | string | `spark`, `solana`, or a supported EVM chain (e.g. `base`, `tron`). The route `lightning:BTC -> chain:asset` must exist, otherwise `unsupported_route`. | | `destinationAsset` | Yes | string | A stablecoin the destination chain supports on a `lightning:BTC` route (e.g. `USDB`, `USDC`). Check `GET /v1/orchestration/routes`. | | `recipientAddress` | Yes | string | Destination address, validated against the chain's address format | | `amountOut` | One of | string | Desired output in smallest units. Stablecoin amounts must be whole cents. Receiver gets exactly this; sender pays it plus fees. | | `amountFiatUsd` | One of | string | Sender's USD amount as a decimal string (e.g. `"50.00"`). Range $1.00 to $50,000.00. | | `affiliateId` | No | string | Must reference an affiliate registered via `PUT /v1/affiliates/:affiliateId` | | `label` | No | string | Partner-facing label, max 255 characters | Exactly one of `amountOut` or `amountFiatUsd` must be set. Response (`201`): ```json theme={null} { "payLink": { "id": "pl_abc123", "shortId": "DNHKySOZ", "destinationChain": "solana", "destinationAsset": "USDC", "recipientAddress": "So1YourSolanaAddress...", "amountOut": null, "amountFiatUsd": "50.00", "amountFiatCurrency": "USD", "affiliateId": null, "label": "Tip jar", "enabled": true, "createdAt": "2026-01-15T12:00:00.000Z", "updatedAt": "2026-01-15T12:00:00.000Z", "shortUrl": "https://orchestration.flashnet.xyz/pay/DNHKySOZ" } } ``` Exactly one of `amountOut` or `amountFiatUsd` is set on every link; the other is `null`. `shortUrl` is the shareable payment URL. ### GET /v1/pay-links List pay links for your partner. Returns `{ "payLinks": [...] }` with the same per-link shape as create. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/pay-links?includeDisabled=true" \ -H "Authorization: Bearer fn_..." ``` Query params: * `includeDisabled` (`true|false`, default `false`) * `limit` (default `200`, max `1000`) * `offset` (default `0`) ### GET /v1/pay-links/:id Get a single pay link by `pl_...` id. Returns `404` when the link does not exist or belongs to another partner. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/pay-links/pl_abc123" \ -H "Authorization: Bearer fn_..." ``` ### DELETE /v1/pay-links/:id Disable a pay link (soft delete). The public URL returns `404` afterward; orders created before disabling continue to process. ```bash theme={null} curl -sS -X DELETE "https://orchestration.flashnet.xyz/v1/pay-links/pl_abc123" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: paylink-disable:$(uuidgen)" ``` Response: ```json theme={null} { "ok": true } ``` # Streaming (SSE) Source: https://docs.flashnet.xyz/products/orchestration/api/sse Server-Sent Events stream of order status changes. One connection per order; the server pushes each status transition as it happens, so you never poll. ## GET /v1/sse/operations/:id `:id` is the order ID (`ord_...`). Connections are rate limited to 30 per minute per IP. ### Authentication `EventSource` cannot set headers, so the API key goes in the `token` query parameter instead of `Authorization`. * **Server keys** (`fn_`): `?token=fn_...` streams any order owned by your partner. Do not embed server keys in browser code. * **Client keys** (`fnp_`): require the `orders:sse` scope and a second query parameter, `readToken`, the order-scoped token returned in the `/submit` or `/onramp` response that created the order with that client key. The readToken is bound to the partner, the client key, and the order ID, and expires 24 hours after issuance. A missing or invalid readToken returns `403`. An order that does not exist or belongs to another partner returns `404`. ### Events | Event | Data | When | | ----------- | ------------------------ | -------------------------------------------------------------------- | | `status` | `{"status": "swapping"}` | Immediately on connect (current status), then on every status change | | `heartbeat` | empty | Every 15 seconds while the stream is open | Status values match [`GET /v1/orchestration/status`](/products/orchestration/api/quotes-and-orders#get-v1orchestrationstatus); internal `paused` states stream as `processing`. ### Close semantics The server ends the stream when the order reaches `completed`, `failed`, or `refunded`. If the order is already terminal at connect time, you get one `status` event and the stream closes. `unfulfilled` does not close the stream: a late deposit can revive the order, so keep listening. If your connection drops, reconnect to the same URL; the first event replays the current status, so no transition is lost. ### Browser example (client key + readToken) ```javascript theme={null} const url = `https://orchestration.flashnet.xyz/v1/sse/operations/${orderId}` + `?token=${clientKey}&readToken=${readToken}`; const es = new EventSource(url); es.addEventListener("status", (e) => { const { status } = JSON.parse(e.data); render(status); if (["completed", "failed", "refunded"].includes(status)) es.close(); }); es.onerror = () => { // EventSource auto-reconnects; close and resubscribe if the readToken expired }; ``` ### curl example (server key) ```bash theme={null} curl -N "https://orchestration.flashnet.xyz/v1/sse/operations/ord_...?token=fn_..." ``` ### When to use SSE Use SSE for end-user progress UIs: the browser holds one connection per visible order and updates the moment the status changes. For server-to-server delivery, use [webhooks](/products/orchestration/webhooks) instead; they are durable, signed, and retried, while an SSE connection only exists while the client holds it open. For turning status updates into a good payment UX, see [deeplink best practices](/products/orchestration/deeplink-best-practices). # Webhook events Source: https://docs.flashnet.xyz/products/orchestration/api/webhook-events Webhooks deliver public order status changes as signed HTTP POST requests. For signature verification, retry logic, and delivery semantics, see [Webhooks](/products/orchestration/webhooks). ## What event types are emitted? Most events correspond to a partner-visible `order.status` transition, where the `event` field in the payload is `order.`. One event, `order.superseded`, is not a status change — see [Recovered (superseded) orders](#recovered-superseded-orders). | Event | Trigger | Key fields | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | `order.processing` | Order created, deposit detected | `amountIn`, `source`, `depositAddress` | | `order.confirming` | Waiting for on-chain confirmation (Bitcoin L1) | `source.txHash` | | `order.bridging` | Cross-chain bridge initiated | `source`, `destination` | | `order.swapping` | Flashnet swap executing | `amountIn`, `feeAmount` | | `order.awaiting_approval` | ZeroConf offer requires partner action | `zeroconfOffer` | | `order.refunding` | Refund initiated (market moved past slippage, exact-out could not be satisfied, or stale-quote deposit) | `refund`, `error` | | `order.delivering` | Final delivery to destination address in progress | `destination`, `amountOut` | | `order.completed` | Order finished, funds delivered | `amountOut`, `destination.txHash`, `completedAt` | | `order.failed` | Terminal failure | `error` | | `order.unfulfilled` | Order cannot proceed (e.g. deposit not received within window) | `error` | | `order.refunded` | Refund delivered | `refund.txHash`, `refund.amount` | | `order.superseded` | A later order recovered this order's deposit (e.g. an RBF-replaced Bitcoin deposit). Not a status transition — the order's status is unchanged. | `supersededByOperationId` | ## What is the payload shape? Every webhook POST contains a `WebhookEvent` envelope. The `data` field is a full order snapshot at the moment the event was emitted. ### Always present | Field | Type | Notes | | -------------------------- | ---------------- | ---------------------------------------------------------------------------------- | | `event` | `string` | `order.` | | `timestamp` | `string` | ISO 8601 delivery timestamp | | `data.id` | `string` | Order ID (`ord_...`) | | `data.type` | `string` | Always `"order"` | | `data.status` | `string` | Partner-visible order status. Internal paused orders are reported as `processing`. | | `data.quoteId` | `string \| null` | Quote ID. `null` for accumulation/liquidation address deposits. | | `data.amountIn` | `string` | Input amount in smallest units | | `data.amountOut` | `string \| null` | Output amount. `null` until delivery. | | `data.feeBps` | `number` | Platform fee rate | | `data.feeAmount` | `string` | Platform fee in smallest units | | `data.slippageBps` | `number` | Slippage tolerance | | `data.source.chain` | `string` | Source chain | | `data.source.asset` | `string` | Source asset | | `data.source.address` | `string \| null` | Sender address when known | | `data.source.txHash` | `string \| null` | Source transaction hash | | `data.source.sweepTxHash` | `string \| null` | Sweep transaction hash (accumulation/liquidation flows) | | `data.destination.chain` | `string` | Destination chain | | `data.destination.asset` | `string` | Destination asset | | `data.destination.address` | `string` | Recipient address | | `data.destination.txHash` | `string \| null` | Delivery transaction hash. `null` until delivered. | | `data.depositAddress` | `string` | Deposit address from the quote | | `data.recipientAddress` | `string` | Recipient address on destination chain | | `data.flashnetRequestId` | `string \| null` | Flashnet swap request ID when a swap has executed | | `data.sparkTxHash` | `string \| null` | Spark transfer or withdrawal hash | | `data.refund.asset` | `string \| null` | Refund asset | | `data.refund.amount` | `string \| null` | Refund amount | | `data.refund.txHash` | `string \| null` | Refund transaction hash | | `data.error.code` | `string \| null` | Error code when status requires action or has failed | | `data.error.message` | `string \| null` | Human-readable error description | | `data.createdAt` | `string` | ISO 8601 order creation time | | `data.updatedAt` | `string` | ISO 8601 last update time | | `data.completedAt` | `string \| null` | ISO 8601 completion time. `null` until completed. | ### Present on exact-out quotes | Field | Type | Notes | | ---------------------------------------- | ---------------- | ------------------------------ | | `data.paymentIntent.version` | `number` | Always `1` | | `data.paymentIntent.amountMode` | `string` | `"exact_out"` | | `data.paymentIntent.targetAmountOut` | `string` | Requested output amount | | `data.paymentIntent.requiredAmountIn` | `string` | Required input | | `data.paymentIntent.maxAcceptedAmountIn` | `string` | Maximum accepted input | | `data.paymentIntent.inputBufferBps` | `number` | Input buffer tolerance | | `data.paymentIntent.actualAmountIn` | `string \| null` | Actual deposited amount | | `data.paymentIntent.refundAddress` | `string` | Refund address for overpayment | | `data.paymentIntent.exactOutExecution` | `string` | `"strict"` | ### Recovered (superseded) orders When a Bitcoin deposit is replaced — for example RBF'd or re-broadcast so the original transaction never confirms — the first order can end `unfulfilled`, and the confirmed replacement is picked up as a **new** order that takes over the swap. The two orders are linked so you can follow a customer from the dead order to the one that actually runs. | Field | Type | Notes | | ------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------- | | `data.supersededByOperationId` | `string \| null` | On the original (superseded) order: the id of the successor order that recovered the deposit. `null` for ordinary orders. | | `data.recoveredFromOperationId` | `string \| null` | On the successor order: the id of the original order it recovered. `null` for ordinary orders. | The dedicated **`order.superseded`** event fires on the original order each time its link is set or advanced to a new successor, carrying `data.supersededByOperationId`. The successor's own status events carry `data.recoveredFromOperationId`. Both ids are also present on the REST order responses (`GET /v1/orchestration/status`, `/order`, `/history`). The successor is created from a replacement deposit that may still be confirming, so it can be in flight when `order.superseded` arrives. Treat `supersededByOperationId` as "where the deposit went," not proof of delivery — open or poll the successor for its status. If that successor is itself replaced before delivering, the link advances and `order.superseded` fires again with the newer id. Webhook delivery is not ordered (fire-and-forget with retries), so do not treat the last-arrived event as current: reconcile by the payload `timestamp`, or re-read the order (`GET /v1/orchestration/status`) and trust its `supersededByOperationId`. ### Present on Bitcoin L1 deposits with ZeroConf `data.zeroconfOffer` carries the offer object. The schema is owned by [ZeroConf offer fields](/products/orchestration/api/approval-flows#zeroconf-offer-fields); webhook payloads use the same projection as the status API. Webhook-specific notes: * The offer appears on every event's order snapshot while it is stored on the order, not only on `order.awaiting_approval`. * Act when the event is `order.awaiting_approval` and `data.zeroconfOffer.status` is `"pending"`. Later events carry the resolved offer (`accepted`, `declined`, `expired`, or `confirmed`) for audit. ### Present when app/affiliate fees are configured | Field | Type | Notes | | ---------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data.feePlan.version` | `number` | Always `1` | | `data.feePlan.settlementChain` | `string` | Chain where fees settle (e.g. `"solana"`) | | `data.feePlan.settlementAsset` | `string` | Asset used for fee settlement (e.g. `"USDC"`) | | `data.feePlan.appFees` | `AppFee[]` | Array of fee entries | | `data.feePlan.appFees[*].affiliateId` | `string \| undefined` | Present for registry-based entries | | `data.feePlan.appFees[*].recipient` | `string` | Payout address | | `data.feePlan.appFees[*].feeBps` | `number` | Fee rate in basis points | | `data.feePayouts.version` | `number` | Always `1` | | `data.feePayouts.entries` | `FeePayoutEntry[]` | Array of payout leg records | | `data.feePayouts.entries[*].idempotencyKey` | `string` | Unique payout key | | `data.feePayouts.entries[*].leg` | `string` | `"full"` for a completed transfer, `"holdback"` for an affiliate fee retained for later claim, or legacy `"instant"` on historical multi-leg records. Unrelated to ZeroConf. | | `data.feePayouts.entries[*].chain` | `string` | Payout chain | | `data.feePayouts.entries[*].role` | `string` | `"app_fee"`, `"recipient_payout"`, `"platform_fee"`, or `"fee_custody"` | | `data.feePayouts.entries[*].affiliateId` | `string \| undefined` | Present for registry-based app-fee entries | | `data.feePayouts.entries[*].recipient` | `string` | Payout address | | `data.feePayouts.entries[*].feeBps` | `number \| null` | Fee rate. `null` for `recipient_payout`. | | `data.feePayouts.entries[*].amount` | `string` | Gross fee amount | | `data.feePayouts.entries[*].platformCutAmount` | `string \| null` | Flashnet's 20% cut. `null` for `recipient_payout`. | | `data.feePayouts.entries[*].recipientAmount` | `string \| null` | 80% paid to recipient. `null` for `recipient_payout`. | | `data.feePayouts.entries[*].txHash` | `string` | Payout transaction hash | | `data.feePayouts.entries[*].recordedAt` | `string` | ISO 8601 when the payout was recorded | `feePayouts` populates incrementally as payout legs execute. Early events may have an empty `entries` array. ## Which status transitions emit events? The event is determined by the status the order lands on: every partner-visible transition into a status below emits `order.`, regardless of the status it came from. The allowed transitions themselves are defined in [Order Lifecycle](/products/orchestration/order-lifecycle); this table maps the destination status to the event your handler receives. | Status reached | Event emitted | | ------------------- | ----------------------------------------------- | | `processing` | `order.processing` | | `confirming` | `order.confirming` | | `bridging` | `order.bridging` | | `swapping` | `order.swapping` | | `awaiting_approval` | `order.awaiting_approval` (ZeroConf offer only) | | `refunding` | `order.refunding` | | `delivering` | `order.delivering` | | `completed` | `order.completed` | | `failed` | `order.failed` | | `unfulfilled` | `order.unfulfilled` | | `refunded` | `order.refunded` | Statuses outside this table emit nothing: * `paused` emits no event and is masked: while an order is paused for operator review, status snapshots and webhook payloads report `processing`. If the order resumes, later public transitions emit normally. If it is condemned after review, the webhook is `order.failed`. * `expired` emits no event. It is visible through status polling and SSE only. `unfulfilled` is terminal but can resume on a late deposit. When it resumes, webhooks fire again for the statuses the order moves through (`confirming`, `swapping`, `delivering`, `completed`). `completed`, `failed`, and `refunded` emit no further events. # BTC to Stablecoin Source: https://docs.flashnet.xyz/products/orchestration/btc-to-stablecoin This flow starts with a BTC (or USDB) deposit and ends with USDC or another supported asset delivered to any supported chain. If you want a reusable Bitcoin address that runs this flow automatically on each deposit, use [reusable addresses](/products/orchestration/reusable-addresses#what-are-liquidation-addresses). Common uses: * Merchant settlement: accept BTC and deliver stablecoins to any supported chain. * Wallet sell flow: users sell BTC and receive the destination asset on their preferred chain. * Exact-out payments: deliver a precise USDC amount for invoices (Bitcoin L1 only). For exact-out BTC delivery to Lightning, see [Stablecoin to BTC](/products/orchestration/stablecoin-to-btc#example-usdc-to-btc-lightning-exact-out). Live pairs, per-asset contract addresses, decimals, and chain ids come from `GET /v1/orchestration/routes`. Per-route amount bounds come from `GET /v1/orchestration/limits`. Never hardcode contract addresses; read them from the route entry's source and destination detail objects. ## Flow 1. Create a quote: `POST /v1/orchestration/quote`. 2. Initiate the source deposit to `depositAddress`. 3. Submit the deposit transaction identifier: `POST /v1/orchestration/submit`. 4. Track status via webhooks or `GET /v1/orchestration/status?id=...`. 5. If status becomes `awaiting_approval`, a ZeroConf offer is pending (`order.zeroconfOffer`). Accept or decline via the zeroconf endpoints. See [ZeroConf](/products/orchestration/zeroconf). Quotes expire 2 minutes after creation. ## Example: BTC (Spark) to USDC (Base) ```ts theme={null} const BASE_URL = 'https://orchestration.flashnet.xyz'; const quote = await fetch(`${BASE_URL}/v1/orchestration/quote`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, 'X-Idempotency-Key': `quote:${Date.now()}`, }, body: JSON.stringify({ sourceChain: 'spark', sourceAsset: 'BTC', destinationChain: 'base', destinationAsset: 'USDC', amount: '1000000', recipientAddress: '0xYourBaseUsdcAddress', slippageBps: 50, }), }).then((r) => r.json()); // Send BTC on Spark to quote.depositAddress for quote.amountIn. // Capture the Spark transfer id. const sparkTransferId = await sendSparkBtc({ toSparkAddress: quote.depositAddress, amountSats: quote.amountIn, }); const submit = await fetch(`${BASE_URL}/v1/orchestration/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, 'X-Idempotency-Key': `submit:${quote.quoteId}:${sparkTransferId}`, }, body: JSON.stringify({ quoteId: quote.quoteId, sparkTxHash: sparkTransferId, // Optional but recommended. When present, the deposit verifier checks the sender Spark address. sourceSparkAddress: 'spark1YourSenderAddress', }), }).then((r) => r.json()); const status = await fetch( `${BASE_URL}/v1/orchestration/status?id=${encodeURIComponent(submit.orderId)}`, ).then((r) => r.json()); console.log(status.order.status); ``` ## Example: BTC (Bitcoin L1) to USDC (exact in) A Bitcoin L1 quote returns a Bitcoin `depositAddress`. After you broadcast the transaction, submit the txid. The engine automatically finds the output that paid the deposit address. Quote request: ```json theme={null} { "sourceChain": "bitcoin", "sourceAsset": "BTC", "destinationChain": "solana", "destinationAsset": "USDC", "amount": "250000", "recipientAddress": "So1...", "slippageBps": 50 } ``` Submit request: ```json theme={null} { "quoteId": "q_...", "bitcoinTxid": "" } ``` `bitcoinVout` is optional. When omitted, the engine resolves it by scanning the transaction outputs for the one that pays the quote's deposit address. You can still provide it explicitly for batched transactions where you want to specify a particular output. ## Example: BTC (Bitcoin L1) to USDC (exact out payment intent) Use exact-out when the destination must receive a precise USDC amount. Quote request: ```json theme={null} { "sourceChain": "bitcoin", "sourceAsset": "BTC", "destinationChain": "base", "destinationAsset": "USDC", "amount": "100000000", "amountMode": "exact_out", "recipientAddress": "0xYourBaseUsdcAddress", "refundAddress": "bc1qyourrefundaddress...", "slippageBps": 0 } ``` Quote response adds: * `targetAmountOut`: requested destination amount * `requiredAmountIn`: minimum BTC input required * `maxAcceptedAmountIn`: highest BTC input auto-accepted * `inputBufferBps`: currently `2` when `slippageBps=0`, else `0` Deposit guidance: * Send `requiredAmountIn` for the cleanest path. * Keep actual input at or below `maxAcceptedAmountIn`. * If actual input is outside bounds, or target output cannot be met at current market conditions, the order refunds automatically to `paymentIntent.refundAddress`. The relevant `errorCode` will be `exact_out_insufficient_input`, `exact_out_input_above_max`, or `exact_out_target_not_met`. See [Error codes](/products/orchestration/api/error-codes#order-lifecycle-error-codes). ## Example: BTC (Spark) to USDC (Solana) with affiliate fees ```json theme={null} { "sourceChain": "spark", "sourceAsset": "BTC", "destinationChain": "solana", "destinationAsset": "USDC", "amount": "100000", "recipientAddress": "So1RecipientAddress...", "appFees": [ { "recipient": "So1AffiliateOne...", "fee": 100 }, { "recipient": "So1AffiliateTwo...", "fee": 50 } ] } ``` ## Example: BTC (Spark) to USDC (Solana) with affiliate registry ```json theme={null} { "sourceChain": "spark", "sourceAsset": "BTC", "destinationChain": "solana", "destinationAsset": "USDC", "amount": "100000", "recipientAddress": "So1RecipientAddress...", "affiliateId": "flashpartner" } ``` Affiliate fees on sell routes settle in USDC on the destination payout side. Flashnet retains a 20% platform cut; the remaining 80% goes to the fee recipient. See [Fees](/products/orchestration/concepts/fees) for the full fee model and [Quotes and Orders](/products/orchestration/api/quotes-and-orders#post-v1orchestrationquote) for the response fields. ## Example: BTC (Spark) to USDT (Tron) ```json theme={null} { "sourceChain": "spark", "sourceAsset": "BTC", "destinationChain": "tron", "destinationAsset": "USDT", "amount": "100000", "recipientAddress": "TYourTronAddress...", "slippageBps": 50 } ``` Delivery to other supported chains uses the same Orchestra routing. The order progresses through swap, bridge, and delivery steps. ## How do deposit flexibility and ZeroConf work? Deposits do not need to match the quoted `amountIn` exactly. See [Deposit Amount Flexibility](/products/orchestration/order-lifecycle#what-happens-if-the-deposit-amount-differs-from-the-quote) for how over/underpayments are handled. For Bitcoin L1 exact-in deposits, [ZeroConf](/products/orchestration/zeroconf) can offer instant credit after the transaction is detected. If no offer is generated, or if the offer is declined or expires, the order continues after 1 on-chain confirmation. Exact-out orders always use confirmation-based processing. ## USDB sell flows For USDB sell flows (USDB to stablecoin), the quote returns a per-partner derived Spark address in `depositAddress`. This address is deterministic for a given partner and recipient combination. The `sourceSparkAddress` field is not required on submit for these flows. ## Example: BTC (Lightning) to USDC Lightning-source routes use custom pricing. USDB destinations get the lowest rates. A Lightning quote returns a BOLT11 invoice in `depositAddress` and a `lightningReceiveRequestId`. Pay the invoice, then submit the receive request id. Quote response fields to pay: * `depositAddress`: BOLT11 invoice * `amountIn`: sats to pay * `lightningReceiveRequestId`: identifier used by `submit` Submit request: ```json theme={null} { "quoteId": "q_...", "lightningReceiveRequestId": "" } ``` ## Next steps * [API Reference](/products/orchestration/api/overview) * [Webhooks](/products/orchestration/webhooks) # Choose Your Integration Source: https://docs.flashnet.xyz/products/orchestration/choosing-a-flow Orchestra supports four integration patterns. Pick the one that fits your use case. ## Patterns Full control per transaction. Get a quote, fund the deposit, submit, track the order. One call to `POST /v1/orchestration/onramp` creates a Lightning-invoice order. The user pays the invoice from any Lightning wallet or Cash App. Shareable payment links with a fixed stablecoin output. Each click creates a fresh Lightning invoice. No expiration. Persistent deposit addresses in two directions: accumulation (any chain to Spark) and liquidation (Bitcoin L1 to any chain). ## Comparison | | Quote/Submit | Pay Links | Reusable Addresses | | ---------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | Per-deposit work | Call `/quote`, fund, call `/submit` | Click a URL | Fund only (auto-detected) | | Source | Any supported chain | Lightning | Accumulation: any supported source chain. Liquidation: bitcoin | | USDB advantage | Same fee | Lower fee tier, fewer pipeline steps | Same fee | | Destination | Any supported pair | Any supported stablecoin | Accumulation: spark (BTC or USDB). Liquidation: any supported chain/asset | | Price control | Per-transaction slippage, exact-out | `amountOut` (sender pays output + fees) or `amountFiatUsd` (sender pays a fixed USD figure). Fresh quote per click. | Fixed slippage per address | | Platform fee | Custom pricing | Custom pricing | Custom pricing | | Fee model | Deducted from output | `amountOut`: added on top of sender's payment. `amountFiatUsd`: deducted from receiver's output. | Deducted from output | | Affiliate fees | Supported (exact-in) | Supported in both modes via `affiliateId`. Fee position follows the chosen mode. | Supported (frozen at creation) | | Expiration | Quote expires in 2 min | Link never expires | Address is permanent | | ZeroConf | Eligible for Bitcoin L1 exact-in deposits | N/A (Lightning) | Liquidation addresses only (Bitcoin L1 deposits are ZeroConf-eligible) | The [Fiat Onramp](/products/orchestration/onramp) is the fourth pattern: `POST /v1/orchestration/onramp` creates an order with a single-use BOLT11 invoice that the end user pays from any Lightning wallet or Cash App. Best for fiat-adjacent flows where the user starts from a Lightning balance. ## Address behavior **Quote deposit addresses** are generated per quote. Each `/quote` call returns a fresh `depositAddress`. Do not reuse a previous quote's deposit address for a new deposit. **Reusable addresses** (accumulation and liquidation) are deterministic. The deposit address is derived from the partner and destination configuration. Creating the same configuration twice returns the same address. Give reusable addresses to end users as permanent deposit targets. Never send a second deposit to a quote's `depositAddress` after that quote has been submitted or expired. Each quote generates its own deposit instruction. Reusable address deposits are detected automatically by server-side webhooks. You do not need to call `/submit` for these flows. Register a [partner webhook](/products/orchestration/webhooks) to receive order updates. ## Which flow should I use? * Need per-transaction price control, custom slippage, or exact-out payments? **Quote/Submit**. * User starts from a Lightning balance or Cash App and you want delivery in one API call? **[Fiat Onramp](/products/orchestration/onramp)**. * Shareable "pay me \$X" links via Lightning? **Pay Links**. Use `amountFiatUsd` when the displayed Cash App figure must match the link, or `amountOut` when the receiver must end up with an exact stablecoin amount. * Auto-converting deposits into BTC on Spark, or Bitcoin L1 deposits into any supported asset? **[Reusable addresses](/products/orchestration/reusable-addresses)** (accumulation for inbound stablecoins, liquidation for inbound BTC). ## Next steps * [Stablecoin to BTC](/products/orchestration/stablecoin-to-btc): Buy Bitcoin with USDC * [BTC to Stablecoin](/products/orchestration/btc-to-stablecoin): Sell Bitcoin for USDC * [Pay Links](/products/orchestration/pay-links): Shareable payment links * [Reusable Addresses](/products/orchestration/reusable-addresses) # Amounts Source: https://docs.flashnet.xyz/products/orchestration/concepts/amounts Every amount in the Orchestra API is an integer string in the asset's smallest units. `"50000000"` is \$50.00 of 6-decimal USDC. `"100000"` is 0.001 BTC in sats. There are no floats and no decimal points anywhere in the API. Decimals per asset come from the `source` and `destination` detail objects on `GET /v1/orchestration/routes`. Read them from there; never hardcode decimals in your integration. ## amountMode One field, `amount`, interpreted by `amountMode`. The mode answers which side of the swap the user is fixing. **`exact_in`** (the default): the user knows their budget. `amount` is what the user sends; `estimatedOut` is what arrives after fees and execution. Use this for "swap \$50 of USDC" flows. **`exact_out`**: the recipient must receive an exact amount. `amount` is what the recipient gets, and the API computes `requiredAmountIn` (what the user must deposit) and `maxAcceptedAmountIn` (the ceiling above which the deposit refunds). Use this for payment intents: "the merchant needs exactly 100 USDC." Exact-out constraints: * It is per-route. Check `exactOutEligible` on `GET /v1/orchestration/routes` before offering it. * Stablecoin exact-out amounts must be whole-cent values: multiples of `10000` for 6-decimal stables. A non-cent value returns `400 invalid_request`. * `refundAddress` is required for `exact_out`, and for any Lightning destination in either mode. ## amountFiatUsd `amountFiatUsd` pins the sender's USD figure instead of a crypto amount. It is available on `/onramp` and pay links only, and is mutually exclusive with `amount`. The server converts USD to sats at request time using a Coinbase and Kraken spot oracle. When the spot price is stale, the request fails closed with `503 spot_unavailable` rather than pinning a wrong dollar figure. The accepted range is $1.00 to $50,000.00. `amountFiatUsd` runs as `exact_in`. Combining it with `amountMode: "exact_out"` is gated to USD-stablecoin destinations and invoice-billing partners, and rejects app and affiliate fees. See [USD-pinned mode](/products/orchestration/onramp#usd-pinned-mode-amountfiatusd) for the full contract. ## Limits `GET /v1/orchestration/limits` is the authoritative per-route min/max source. The bounds are operator-tuned and change without notice, so query the endpoint instead of hardcoding numbers. A few constraints are static and enforced in code: BTC swap inputs need at least 5,000 sats from Bitcoin L1 and 1,200 sats from Spark, and Bitcoin L1 delivery needs at least 10,000 sats after route fees. `/limits` reports these alongside the runtime bounds, typed per constraint. ## When the deposit differs from the quote Deposits do not need to match the quoted amount exactly, and the rules differ by `amountMode`. [Order Lifecycle](/products/orchestration/order-lifecycle#what-happens-if-the-deposit-amount-differs-from-the-quote) owns the full behavior: underpayment, overpayment, and the exact-out refund conditions. ## Where to go next * [Pricing](/products/orchestration/concepts/pricing) for estimates, quotes, and fixed delivery * [Fees](/products/orchestration/concepts/fees) for what gets deducted before delivery * [Quotes and Orders](/products/orchestration/api/quotes-and-orders) for request and response shapes # Fees Source: https://docs.flashnet.xyz/products/orchestration/concepts/fees Two parties can charge fees on an order: Flashnet charges a platform fee, and you can add your own fees on top. This page owns who pays what, in which asset, and in what order. ## Platform fee The platform fee is bps plus a minimum, set per partner and route. Pricing is contractual, so quote responses (`feeAmount`, `feeBps`) are the source of truth for your rate, not the docs. It is charged on the input side for buys and the output side for sells. `feeAsset` is USDC on most routes, USDB on BTC to USDB, and BTC on direct Lightning passthrough routes. Invoice-billing partners see `feeAmount: "0"` on quotes: their platform fees accrue instead of being deducted per transaction, and are billed monthly. Fees owed are visible at `GET /v1/partner/dashboard/fee-invoices`. ## LP pool fee Routes that swap through the BTC/USDB pool on Spark also pay the pool's LP fee, charged in the input asset: BTC when selling, USDB when buying. It goes to liquidity providers, not Flashnet, and varies with pool depth and trade size. Like every other fee, it is already reflected in the quoted output; the user sends `amountIn` and receives `estimatedOut`. ## Your fees Two mechanisms, mutually exclusive per quote. Sending both returns `400 invalid_request`. **Inline (`appFees`)**: an array of `{recipient, fee}` on the quote, where `fee` is bps (`1` to `10000`). Max 16 entries, sum of bps at most 10000, and each recipient is validated against the settlement chain's address format. Use this when recipients vary per quote and you do not need accrual or claims. **Registered (`affiliateIds`)**: recipients registered once via `PUT /v1/affiliates/:affiliateId` with `feeBps`, `payoutChain`, `payoutAsset`, and `payoutAddress` (ids match `^[a-z0-9][a-z0-9_-]{0,63}$`). Each quote entry is a plain id, or `{affiliateId, feeBps}` to override the profile rate for that quote only. The legacy single `affiliateId` field folds into `affiliateIds`. Use this when you want stable recipients, per-order accrual, and claim-based payouts. Flashnet keeps 20% of every app and affiliate fee, surfaced as `appFeePlatformCutAmount`; the recipient gets 80%. ## Route scope Fee plans work on any route with a resolvable fee settlement: * Settlement is `solana:USDC` when either side is Solana or any EVM chain, when the destination is a NEAR-egress asset, or on TON and Monero routes. * Settlement is `spark:USDB` when either side is `spark:USDB`. * Pure BTC-to-BTC routes have no settlement asset and reject fee plans with `400 unsupported_fee_plan`. ## Deduction order Fees come out in a fixed sequence before execution: 1. Platform fee (`feeAmount`) 2. Sweep fee (`sweepFeeAmount`, some USDC-source routes only) 3. App and affiliate fees (`appFeeAmount`) 4. Flashnet's 20% cut of those fees (`appFeePlatformCutAmount`) 5. The net amount executes `totalFeeAmount` on a quote equals `feeAmount + roundingFeeAmount + appFeeAmount + sweepFeeAmount`. `roundingFeeAmount` is stablecoin surplus retained when output is capped to a whole-cent or exact-out target; it appears only when nonzero. ## A worked example A \$1,000.00 USDC exact-in quote (`amount: "1000000000"`) with one affiliate at 100 bps. Assume a 5 bps platform fee for illustration; your contractual rate applies in practice. | Step | Amount | Field | | --------------------------------------- | ---------- | ---------------------------- | | Input | \$1,000.00 | `amountIn` | | Platform fee (5 bps) | \$0.50 | `feeAmount` | | Affiliate fee (100 bps) | \$10.00 | `appFeeAmount` | | Flashnet's 20% cut of the affiliate fee | \$2.00 | `appFeePlatformCutAmount` | | Affiliate accrues (80%) | \$8.00 | `appFees[0].recipientAmount` | | Executes | \$989.50 | | The user pays $10.50 in total fees. The affiliate's $8.00 accrues to their ledger; the remaining \$989.50 proceeds to route execution and determines `estimatedOut`. ## Affiliate payout lifecycle Affiliate fees accrue per order into a ledger. `POST /v1/affiliates/:affiliateId/claim` takes the full available balance in one payout; the minimum claim is \$1.00 USD-normalized. The payout destination is frozen from the oldest unclaimed accrual, so rotating the profile's payout address never redirects fees that already accrued. Discover valid payout destinations at `GET /v1/affiliate-dashboard/payout-destinations`. Affiliates can get read-only dashboard access without touching your server key: mint a client key with the `affiliates:read` scope pinned to their affiliate id. See [Affiliate Fees](/products/orchestration/api/resources) for registration and claim details. ## Client keys Client keys (`fnp_`) cannot set any fee field. A client key can only apply its pinned affiliate, and it cannot claim or manage affiliates. Fee configuration always flows through your server key. ## Where to go next * [Pricing](/products/orchestration/concepts/pricing) for how the quoted output is produced * [Amounts](/products/orchestration/concepts/amounts) for units and amount modes * [Quotes and Orders](/products/orchestration/api/quotes-and-orders) for fee fields on requests and responses * [Order Lifecycle](/products/orchestration/order-lifecycle) for how fees appear on the order record # Pricing Source: https://docs.flashnet.xyz/products/orchestration/concepts/pricing Orchestra produces prices at three levels of firmness. An estimate is indicative. A quote is firm for 2 minutes and gets repriced if the deposit lands late. Fixed delivery turns the quoted output into a delivery commitment. | Level | How you get it | What the price means | | ------------------ | ---------------------------------- | ------------------------------------------------------------------ | | **Estimate** | `GET /v1/orchestration/estimate` | Indicative. Use for display and browsing. | | **Quote** | `POST /v1/orchestration/quote` | Firm for 2 minutes. Late deposits reprice at the live market rate. | | **Fixed delivery** | `deliveryMode: "fixed"` on a quote | Commitment. The order delivers the quoted `estimatedOut`. | ## Estimates `/estimate` is a stateless price preview. It requires no API key, creates no durable order, and allocates no deposit address, so it is safe to call on every input change within its limit of 120 requests per minute per IP. Preview slippage defaults to 100 bps when `slippageBps` is omitted. ## Quotes `/quote` is authenticated and idempotent: send `X-Idempotency-Key` with every request. It persists the quote, allocates a deposit address, and starts a 2 minute expiry (`expiresAt`). Fund the deposit before `expiresAt` and the order executes on the quoted terms. A deposit that arrives after expiry is not refunded. It is repriced at the live market rate at detection time, and execution stays bounded by the quote's `slippageBps`. If you need the quoted price, treat `expiresAt` as a hard deadline for the deposit, not a suggestion. The recommended workflow: 1. Browse with `/estimate`. Call it on every amount or route change. 2. Call `/quote` only when the user commits to the swap. 3. Fund the deposit address before `expiresAt`. This keeps quote creation off your browsing path and reserves it for real intent. ## Fixed delivery `deliveryMode: "fixed"` makes `estimatedOut` a delivery commitment: the order delivers the quoted amount rather than whatever execution produces. * Exact-in only. Any other `amountMode` returns `400 unsupported_delivery_mode`. * The route must have `fixedEligible: true` on `GET /v1/orchestration/routes`, and availability additionally depends on per-partner operator configuration. * When fixed delivery does not apply, `/quote` and `/estimate` reject with `fixed_delivery_unavailable` (the error message states the reason), while `/onramp` proceeds as a normal variable onramp. * The response echoes `deliveryMode: "fixed"` only when it applies. Treat its absence as a variable quote. See [Fixed delivery](/products/orchestration/api/quotes-and-orders#fixed-delivery-deliverymode) for the field-level contract. ## Slippage `slippageBps` bounds how far the execution price can move against the quote before the order refunds instead of filling badly. It accepts `0` to `1000` and defaults to `50` (`200` for NEAR-ingress sources). On routes that swap through the Flashnet AMM, a runtime floor applies (default 10 bps, operator-tunable); a below-floor value returns `400 invalid_request` because it could never fill at real size. Slippage protects the execution leg, not the quote window. If the pool moves past your tolerance between deposit and execution, the order refunds with `slippage_exceeded` and no partner action is required. See [Order Lifecycle](/products/orchestration/order-lifecycle#market-moves-and-refunds) for the refund paths. `/onramp` pins slippage to 1000 bps regardless of input: a human pays a Lightning invoice on their own schedule, so the quote must survive minutes of price drift and reprice at settlement. ## Price impact Price impact (how much your own order moves the pool) is capped separately from slippage. A quote that would move the pool past the cap rejects with `price_impact_too_high`. Slippage caps market movement between quote and execution; the price impact cap rejects orders too large for the pool at quote time. ## Where to go next * [Amounts](/products/orchestration/concepts/amounts) for how `amount` and `amountMode` work * [Fees](/products/orchestration/concepts/fees) for what gets deducted from the quoted output * [Quotes and Orders](/products/orchestration/api/quotes-and-orders) for the full field-level reference * [Order Lifecycle](/products/orchestration/order-lifecycle) for what happens after funding # Deep link best practices Source: https://docs.flashnet.xyz/products/orchestration/deeplink-best-practices Onramp and Pay Links both hand off from a web page to a Lightning wallet on the user's phone through a deep link. The three browser-side patterns the handoff needs: opening the wallet reliably, tracking the order without leaking your API key, and showing the user what stage their payment is in. flashnet-onramp-example.vercel.app github.com/flashnetxyz/pay-link-example ## How do I open the Lightning app? The Cash App payment URL is a deep link. It only works when the browser navigates to it. A `fetch()` call just downloads the HTML response and nothing visible happens. **Wrong:** ```ts theme={null} // This silently fetches HTML, so Cash App never opens await fetch(cashAppPaymentUrl); ``` **Right:** ```ts theme={null} // This navigates the browser, which triggers the Cash App universal link window.location.href = cashAppPaymentUrl; ``` The same rule applies to Strike, Wallet of Satoshi, and any other Lightning app that handles a universal link. Navigate to the URL; do not fetch it. ## How do I track order status with SSE? Use Server-Sent Events for live order tracking. Connect through your proxy so the API key stays server-side. Close the connection only on `completed`, `failed`, or `refunded`. The server keeps the stream open on `unfulfilled` because a late deposit can revive the order; keep the stream open (or resubscribe) so you catch the resumption. Fall back to polling every 3 seconds if SSE disconnects. ```ts theme={null} const es = new EventSource(`/api/proxy/v1/sse/operations/${orderId}`); es.addEventListener("status", (e) => { const { status } = JSON.parse(e.data); updateUI(status); if (["completed", "failed", "refunded"].includes(status)) es.close(); }); ``` Proxy SSE through your backend. The browser should never see your API key. Detect `/v1/sse/` paths in your proxy and stream the response with `text/event-stream` headers. ```ts theme={null} "use client"; import { useEffect, useRef, useState } from "react"; // The server keeps the stream open on `unfulfilled`: a late deposit can revive the order. const CLOSE_STATUSES = new Set(["completed", "failed", "refunded"]); interface UseOrderSSEParams { orderId: string; onStatus: (status: string) => void; enabled?: boolean; } export function useOrderSSE({ orderId, onStatus, enabled = true, }: UseOrderSSEParams): { connected: boolean } { const [connected, setConnected] = useState(false); const onStatusRef = useRef(onStatus); onStatusRef.current = onStatus; useEffect(() => { if (!enabled || !orderId) return; const url = `/api/proxy/v1/sse/operations/${encodeURIComponent(orderId)}`; const es = new EventSource(url); es.addEventListener("status", (e) => { try { const data = JSON.parse(e.data) as { status: string }; onStatusRef.current(data.status); if (CLOSE_STATUSES.has(data.status)) { es.close(); setConnected(false); } } catch { // ignore malformed events } }); es.onopen = () => setConnected(true); es.onerror = () => setConnected(false); return () => { es.close(); setConnected(false); }; }, [orderId, enabled]); return { connected }; } ``` ## How do I proxy the API key? Keep the API key on the server. The browser calls your proxy, the proxy attaches the `Authorization` header, and the response streams back. For SSE paths, the proxy streams the upstream body as-is with `text/event-stream` headers so the browser keeps the connection open. ```ts theme={null} // SSE: stream the response back as-is if (isSSE && upstream.body) { return new Response(upstream.body, { status: upstream.status, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }, }); } ``` ## How do I show pipeline progress? Orders move through a sequence of statuses. Surface the current stage so the user knows the payment is working. | Status | Meaning | | ------------ | -------------------------------------------- | | `confirming` | Payment detected, waiting for confirmation | | `swapping` | Converting BTC to stablecoin | | `bridging` | Sending to destination chain (if applicable) | | `completed` | Funds delivered | Use the SSE status events to animate transitions between stages. The [live example](https://flashnet-onramp-example.vercel.app) shows a step-by-step pipeline visualization with a countdown timer. # Quickstart Source: https://docs.flashnet.xyz/products/orchestration/integration Orchestra is async. You request a quote, accept a deposit, submit it, then track the order to completion. Base URL: `https://orchestration.flashnet.xyz` ## The minimal flow Register a webhook endpoint and store the returned secret. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/webhooks" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: webhook:create:YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://yourapp.example.com/flashnet/webhooks"}' ``` Verify inbound webhooks using `X-Flashnet-Signature` and the raw request body. See [Webhooks](/products/orchestration/webhooks). Show prices with the public `GET /v1/orchestration/estimate` while the user is browsing; call `/quote` only when they commit. Each `/quote` persists a quote and allocates a deposit address. Quotes expire 2 minutes after creation. Late deposits are always repriced at live market rates at detection time and execute against the quote's `slippageBps`. Quote requests that you intend to submit must include `Authorization` and `X-Idempotency-Key`. Example (exact-in, default mode): buy BTC on Spark with USDC on Base. ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/orchestration/quote" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: quote:create:YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "sourceChain": "base", "sourceAsset": "USDC", "destinationChain": "spark", "destinationAsset": "BTC", "amount": "100000000", "recipientAddress": "spark1...", "slippageBps": 50 }' ``` Send the source asset to `depositAddress`. The deposit instruction depends on the route: * Spark source (`sourceChain = spark`): `depositAddress` is a Spark address. * Bitcoin source (`sourceChain = bitcoin`): `depositAddress` is a Bitcoin L1 address. * Lightning source (`sourceChain = lightning`): `depositAddress` is a BOLT11 invoice. * Any other source chain (Solana, Base, other EVM chains, Tron, and the rest of `GET /v1/orchestration/routes`): `depositAddress` is a deposit address on the source chain that receives the source asset. For USDC routes, send exactly `amountIn`. Submitting creates an order. Processing is async. `submit` requires: * `Authorization: Bearer fn_...` * `X-Idempotency-Key` USDC deposit submit: ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/orchestration/submit" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: submit:YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "quoteId": "q_...", "txHash": "0x...", "sourceAddress": "0x..." }' ``` Submit shape depends on the quote `sourceChain`. Examples: ```json theme={null} { "quoteId": "q_...", "sparkTxHash": "spark_transfer_id_or_token_tx_hash" } ``` ```json theme={null} { "quoteId": "q_...", "bitcoinTxid": "", "bitcoinVout": 0 } ``` ```json theme={null} { "quoteId": "q_...", "lightningReceiveRequestId": "" } ``` See [API Reference](/products/orchestration/api/quotes-and-orders) for the full request shapes and optional fields. Preferred: use webhooks. Fallback: poll status. ```bash theme={null} curl -sS "https://orchestration.flashnet.xyz/v1/orchestration/status?id=ord_..." ``` Treat every webhook delivery as at least once. ## Picking a route * Swaps: start with [Stablecoin to BTC](/products/orchestration/stablecoin-to-btc) or [BTC to Stablecoin](/products/orchestration/btc-to-stablecoin). * Reusable deposit addresses: use [reusable addresses](/products/orchestration/reusable-addresses) when you do not want to call `/quote` and `/submit` per deposit. ## Beyond the basics For advanced flows, see the dedicated guides: * [Order Lifecycle](/products/orchestration/order-lifecycle): State machine, deposit flexibility, repricing, field mapping * [ZeroConf](/products/orchestration/zeroconf): Instant Bitcoin L1 credit * [BTC to Stablecoin](/products/orchestration/btc-to-stablecoin#example-btc-bitcoin-l1-to-usdc-exact-out-payment-intent): Exact-out payment intents * [API: Quotes and Orders](/products/orchestration/api/quotes-and-orders#post-v1orchestrationquote): Affiliate fees (`appFees` / `affiliateId`) * [API: Approval Flows](/products/orchestration/api/approval-flows): Repricing and refund endpoints ## Recommended data model Persist these identifiers: * `quoteId` * `orderId` * the source transaction identifier you submitted (`txHash`, `bitcoinTxid` + `bitcoinVout`, Spark transfer id, or Lightning receive request id) Persist these optional workflow fields: * `order.status` * `order.error` (for refund and failure diagnostics) * `order.paymentIntent` * `order.zeroconfOffer` * `order.refund` ## Next steps * [Choose Your Integration](/products/orchestration/choosing-a-flow): Compare quote/submit, accumulation, and liquidation patterns * [Order Lifecycle](/products/orchestration/order-lifecycle): State machine and field mapping * [Stablecoin to BTC](/products/orchestration/stablecoin-to-btc): Buy Bitcoin with USDC * [BTC to Stablecoin](/products/orchestration/btc-to-stablecoin): Sell Bitcoin for USDC * [Webhooks](/products/orchestration/webhooks): Receive order status updates * [API Reference](/products/orchestration/api/overview): Complete endpoint documentation # Fiat Onramp Source: https://docs.flashnet.xyz/products/orchestration/onramp Many popular apps support zero-fee Lightning payments. Orchestra combines zero-fee Lightning with its Lightning-to-Spark pipeline to create a fiat-to-crypto onramp with a single API call. The native destination is USDB on Spark ([details](/usdb/overview)). The onramp to USDB skips all bridging and settles in seconds. Direct BTC destinations on Spark and Bitcoin L1 keep the flow in BTC and charge the platform fee in sats before delivery. Other destinations, such as USDC on Solana or Base, work through the same pipeline but add swap, bridge, and delivery steps. Try it live at [orchestra.flashnet.xyz/onramp](https://orchestra.flashnet.xyz/onramp). The fiat onramp is not available to residents of New York City. ## How it works 1. Your app calls `POST /v1/orchestration/onramp` with the destination chain, asset, recipient address, and an amount. The amount can be: * BTC sats the user sends (`amount` with `amountMode: "exact_in"`, the default) * Destination asset to receive (`amount` with `amountMode: "exact_out"`) * USD the user sees in Cash App (`amountFiatUsd`, runs as `exact_in` after Orchestra fetches BTC/USD spot). 2. Orchestra creates a Lightning invoice, builds the order, and returns payment deeplinks. 3. The user opens the deeplink (or scans a QR code) and pays via their preferred Lightning-compatible app. 4. Orchestra receives the Lightning payment, swaps BTC to the destination asset, and delivers it. The onramp endpoint handles quoting and submission in one call. A separate `/estimate` endpoint exists as an optional preview step (see [Integration](#integration)). ## Why choose USDB or direct BTC? Every onramp route starts from `lightning:BTC`. When you choose USDB, Orchestra swaps BTC to USDB and finishes on Spark. When you choose BTC on Spark or Bitcoin L1, Orchestra stays in BTC and delivers the remaining sats after fees. When you choose USDC on another chain, Orchestra must also bridge and deliver, adding cost and latency. | | USDB on Spark | BTC on Spark / Bitcoin L1 | USDC on Solana/Base | | -------------- | --------------------- | ------------------------------- | -------------------------------------------------------------------------------------------- | | Platform fee | Custom pricing | Custom pricing | Custom pricing | | Fee asset | USDB | BTC | USDC | | Pipeline shape | Verify, swap | Verify, settle BTC fee, deliver | Verify, swap, bridge, deliver | | Delivery time | Seconds | Seconds | Seconds | | Sweep fee | None | None | May apply ([details](/products/orchestration/order-lifecycle#what-are-the-effective-limits)) | | Rewards | 3.5-6% rewards in BTC | None | None | See [USDB Rewards](/usdb/rewards) for reward details. ## What are the fees? Many Lightning-compatible apps charge zero fees on Lightning payments. Orchestra uses route-and-volume-based pricing that undercuts card-based and bank-transfer onramps. No intermediary fees and no card processing fees. The `/estimate` endpoint returns the exact fee for each route. See [Fees](/products/orchestration/concepts/fees) for the full fee model. For `lightning:BTC -> bitcoin:BTC`, the platform fee is taken in BTC and the final on-chain withdrawal also pays the network withdrawal fee quoted by Spark at delivery time. That Bitcoin withdrawal fee is not Flashnet revenue. ## What are the Lightning payment limits? Some Lightning-compatible apps impose their own limits on Lightning payments (for example, up to \$999 per rolling 7-day window). Check your payment app's documentation for specific limits. Orchestra's own per-route amount bounds come from [`GET /v1/orchestration/limits`](/products/orchestration/api/quotes-and-orders#get-v1orchestrationlimits). Validate amounts client-side against your payment app's limits before calling the API. ## Which routes are supported? The source is always `lightning:BTC`. For the current destination list, call [`GET /v1/orchestration/routes`](/products/orchestration/api/quotes-and-orders#get-v1orchestrationroutes) and filter to routes with a `lightning:BTC` source. The set changes as chains and assets are added, so query it rather than hardcoding destinations. ## Integration Use the estimate endpoint to show the user what they'll receive before committing. ```bash theme={null} curl 'https://orchestration.flashnet.xyz/v1/orchestration/estimate?\ sourceChain=lightning&sourceAsset=BTC&\ destinationChain=spark&destinationAsset=USDB&\ amount=100000' ``` Response: ```json theme={null} { "estimatedOut": "96543210", "feeAmount": "19308", "feeBps": 20, "totalFeeAmount": "19308", "feeAsset": "USDB", "route": [ "BTC", "USDB" ], "source": { "chain": "lightning", "asset": "BTC", "chainDisplayName": "Lightning", "chainIcon": "/chain-lightning.svg", "contractAddress": null, "decimals": 8, "chainId": null }, "destination": { "chain": "spark", "asset": "USDB", "chainDisplayName": "Spark", "chainIcon": "/chain-spark.svg", "contractAddress": "btkn1usdb...", "decimals": 6, "chainId": null } } ``` `totalFeeAmount` is `feeAmount` plus `roundingFeeAmount` plus `appFeeAmount`. Estimates never include sweep fees. Some fields appear only when applicable: `roundingFeeAmount` when nonzero, `deliveryMode` when fixed delivery applies, and `appFeeAmount`, `appFeePlatformCutAmount`, and `appFees` when app fees were requested. A single call creates the Lightning invoice, builds the order, and returns payment deeplinks. ```ts theme={null} const BASE_URL = 'https://orchestration.flashnet.xyz'; const onramp = await fetch(`${BASE_URL}/v1/orchestration/onramp`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, 'X-Idempotency-Key': `onramp:${Date.now()}`, }, body: JSON.stringify({ destinationChain: 'spark', destinationAsset: 'USDB', recipientAddress: 'spark1YourSparkAddress...', amount: '100000', // sats }), }).then((r) => r.json()); ``` Response: ```json theme={null} { "orderId": "ord_abc123", "quoteId": "q_xyz789", "depositAddress": "lnbc1000n1pj...", "paymentLinks": { "cashApp": "https://cash.app/launch/lightning/lnbc1000n1pj...", "shortUrl": "https://orchestration.flashnet.xyz/pay/a3xKm2Rq" }, "amountIn": "100000", "estimatedOut": "96543210", "feeAmount": "19308", "feeBps": 20, "totalFeeAmount": "19308", "feeAsset": "USDB", "route": [ "BTC", "USDB" ], "expiresAt": "2026-01-16T12:04:00.000Z" } ``` On mobile, redirect the user to the payment deeplink. On desktop, display the deeplink as a QR code for the user to scan with their phone. Use `shortUrl` when sharing payment links in messages or emails. ```ts theme={null} // Mobile: direct deeplink window.location.href = onramp.paymentLinks.cashApp; // Desktop: render QR code with the deeplink URL // The QR encodes the full cash.app URL, not the BOLT11 invoice renderQR(onramp.paymentLinks.cashApp); // Sharing: use the short URL for texts, emails, etc. const shareableLink = onramp.paymentLinks.shortUrl; // e.g. https://orchestration.flashnet.xyz/pay/a3xKm2Rq ``` The quote and its Lightning invoice expire at `expiresAt`: 24 hours after creation for exact-in onramps, 5 minutes for exact-out and fixed-delivery exact-in onramps (the short TTL bounds BTC price drift against the locked target). If the user doesn't pay in time, create a new onramp order. Use SSE or polling to track progress. USDB orders skip the bridge and delivery steps, so they complete faster. ```ts theme={null} // SSE (preferred) const es = new EventSource( `${BASE_URL}/v1/sse/operations/${onramp.orderId}?token=${API_KEY}` ); es.addEventListener('status', (e) => { const { status } = JSON.parse(e.data); console.log(status); // confirming, swapping, completed, etc. }); // Or poll const status = await fetch( `${BASE_URL}/v1/orchestration/status?id=${onramp.orderId}`, { headers: { Authorization: `Bearer ${API_KEY}` } } ).then((r) => r.json()); ``` ## Frontend integration Onramp shares the browser-side deep link patterns with Pay Links. For the Cash App navigation trick, the SSE order-tracking hook, the API-key proxy pattern, and the pipeline-progress UI, see [Deep link best practices](/products/orchestration/deeplink-best-practices). ## Exact output mode By default, you specify how many sats the user sends (`exact_in`) and Orchestra estimates the output. With `amountMode: "exact_out"`, you specify the destination amount the recipient should receive, and Orchestra calculates the required Lightning payment. This is useful when your UX needs to guarantee a specific dollar amount, for example "onramp exactly \$50 USDC." ```ts theme={null} const onramp = await fetch(`${BASE_URL}/v1/orchestration/onramp`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, 'X-Idempotency-Key': `onramp:${Date.now()}`, }, body: JSON.stringify({ destinationChain: 'solana', destinationAsset: 'USDC', recipientAddress: 'So1anaAddress...', amount: '50000000', // 50 USDC (6 decimals) amountMode: 'exact_out', }), }).then((r) => r.json()); ``` The response includes the calculated `amountIn` (sats required) and a Lightning invoice for that amount. The user pays the invoice, and the recipient receives the specified output amount. Exact-out quotes expire 5 minutes after creation, not the 24 hours of exact-in quotes. For stablecoin destinations, exact-out `amount` must be a whole-cent value in the destination asset's smallest units. For 6-decimal stables, send multiples of `10000`, for example `"50000000"` for \$50.00. If execution produces extra stablecoin units above the exact-out target, Orchestra retains the surplus as `roundingFeeAmount` instead of overdelivering. Affiliate fees (`appFees`, `affiliateId`, `affiliateIds`) are not supported with `exact_out`. Use `exact_in` (or `amountFiatUsd`) if you need affiliate fees on the transaction. Exact-out is not supported for the direct BTC passthrough routes `lightning:BTC -> spark:BTC` and `lightning:BTC -> bitcoin:BTC`. ## USD-pinned mode (`amountFiatUsd`) `amountFiatUsd` lets the partner pin the sender's USD figure. Cash App displays the exact dollar amount, the receiver gets the net after fees, and Orchestra runs the order in `exact_in` mode. The conversion happens server-side at request time: Orchestra fetches the current BTC/USD spot from a Coinbase and Kraken price oracle, computes the equivalent sats, and pins that as the source amount. The spot used is recorded on the order and surfaced on the webhook as `spotUsdPerBtc`. ```ts theme={null} const onramp = await fetch(`${BASE_URL}/v1/orchestration/onramp`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, 'X-Idempotency-Key': `onramp:${Date.now()}`, }, body: JSON.stringify({ destinationChain: 'solana', destinationAsset: 'USDC', recipientAddress: 'So1anaAddress...', amountFiatUsd: '100.00', // sender pays exactly $100 in Cash App }), }).then((r) => r.json()); ``` The order webhook for the resulting completed order includes the fiat snapshot at the top level: ```json theme={null} { "amountIn": "", "amountOut": "99500000", "feeBps": 50, "feeAmount": "500000", "amountFiatUsd": "100.00", "amountFiatCurrency": "USD", "spotUsdPerBtc": "100450.00" } ``` `amountFiatUsd` is mutually exclusive with `amount`. Range $1.00 to $50,000.00. If BTC/USD spot is unavailable at request time, the route fails closed with HTTP 503 (`spot_unavailable`); a stale price would silently misalign the user-displayed Cash App figure. `amountFiatUsd` combined with `amountMode: "exact_out"` is available only for USD-stablecoin destinations and only for invoice-billing partners (other partners get `invoice_required`). It rejects app and affiliate fees with `unsupported_fee_plan`. Plain `amountFiatUsd` runs as `exact_in` and has none of these restrictions. ## API reference Field-level request and response documentation for `POST /v1/orchestration/onramp` lives in [Quotes and orders](/products/orchestration/api/quotes-and-orders#post-v1orchestrationonramp). Two behaviors to keep in mind while integrating: slippage is pinned to 1000 bps on every onramp regardless of input, and exact-in quotes last 24 hours while exact-out and fixed-delivery quotes last 5 minutes. ## Affiliate fees The onramp accepts the same affiliate fee fields as the quote flow: `appFees` (inline), `affiliateId` (one registered affiliate), or `affiliateIds` (several registered affiliates, each optionally overriding the profile's fee bps for that order). Two onramp-specific gates apply: affiliate fees require `amountMode: "exact_in"` (the default), and they require a stablecoin settlement path, so the direct BTC routes `lightning:BTC -> spark:BTC` and `lightning:BTC -> bitcoin:BTC` reject them. See [Fees](/products/orchestration/concepts/fees) for the fee model and [Resources](/products/orchestration/api/resources#affiliates) for registration and claims. ## Webhooks Register an endpoint via `POST /v1/webhooks` to receive order status updates. An onramp order emits `order.processing` on creation, then one event per pipeline stage the route includes (`order.confirming`, `order.swapping`, `order.bridging`, `order.delivering`), and ends with `order.completed`. A failure emits `order.failed`; a refund emits `order.refunding` then `order.refunded`. Orders created with `amountFiatUsd` carry the fiat snapshot (`amountFiatUsd`, `amountFiatCurrency`, `spotUsdPerBtc`) at the top level of the payload. See [Webhooks](/products/orchestration/webhooks) for signature verification and delivery semantics, and [Webhook events](/products/orchestration/api/webhook-events) for the full payload field reference. ## Next steps * [USDB Rewards](/usdb/rewards) for yield details * [API Overview](/products/orchestration/api/overview) for authentication and error handling * [Order Lifecycle](/products/orchestration/order-lifecycle) for status transitions and webhook payloads * [Webhooks](/products/orchestration/webhooks) for real-time order updates # Order Lifecycle Source: https://docs.flashnet.xyz/products/orchestration/order-lifecycle ## How do quotes become orders? A quote is a priced intent with a 2-minute TTL. Call `POST /v1/orchestration/quote` to get deposit instructions and pricing. A quote does not create an order. An order is created when you call `POST /v1/orchestration/submit` with the quote ID and a funded deposit proof. The order tracks execution from deposit through delivery. Orders can also be created automatically via submissionless flows. When a deposit arrives at a quote's deposit address, the system detects the deposit via webhooks (Helius for Solana, Blockdaemon for Bitcoin) or polling (Spark ingress scan) and creates the order without requiring an explicit `/submit` call. Late deposits (arriving after quote expiry) are always accepted and repriced at the live market rate at detection time; execution is still bounded by the quote's `slippageBps`. For submissionless flows, use `GET /v1/orchestration/order?quoteId=...` to poll for order creation. This endpoint returns both the quote state and the order (or `null` if no deposit has been detected yet). See [API: Quotes and Orders](/products/orchestration/api/quotes-and-orders#get-v1orchestrationorder) for the full response shape. `GET /v1/orchestration/status?quoteId=...` returns 404 for quotes that have not been submitted. Use `GET /v1/orchestration/order` instead when you need to poll before the order exists. ## Status state machine Every order moves through a subset of these statuses. The allowed transitions from each status are: | From | Allowed transitions | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `processing` | confirming, bridging, swapping, awaiting\_approval, paused, refunding, delivering, completed, failed, expired, unfulfilled, refunded | | `confirming` | bridging, swapping, refunding, paused, delivering, completed, failed, expired, refunded | | `bridging` | swapping, paused, delivering, completed, failed, refunded | | `swapping` | awaiting\_approval, paused, refunding, bridging, delivering, completed, failed, refunded | | `awaiting_approval` | processing, confirming, swapping, refunding, paused, failed, refunded | | `paused` | processing, failed | | `refunding` | paused, refunded, failed | | `delivering` | confirming, paused, refunding, completed, failed, refunded | | `completed` | (terminal) | | `failed` | (terminal) | | `expired` | (terminal) | | `unfulfilled` | (terminal, resumes on late deposit) confirming, bridging, swapping, delivering, completed | | `refunded` | (terminal) | `awaiting_approval` is used only for [ZeroConf](/products/orchestration/zeroconf) offer acceptance. Market-move refunds happen automatically from `swapping` to `refunding` without partner action. `paused` is an internal operator-review state. Public status responses, SSE, and webhook payloads show paused orders as `processing` until the order resumes or moves to a partner-visible final state. ### Typical happy paths **Stablecoin to BTC:** `processing` -> `bridging` -> `swapping` -> `delivering` -> `completed` **BTC to Stablecoin (Spark source):** `processing` -> `swapping` -> `bridging` -> `delivering` -> `completed` **BTC to Stablecoin (Bitcoin L1 source, no ZeroConf):** `processing` -> `confirming` -> `swapping` -> `bridging` -> `delivering` -> `completed` **BTC to Stablecoin (Bitcoin L1 source, ZeroConf offer):** `processing` -> `awaiting_approval` -> `processing` -> `swapping` -> `bridging` -> `delivering` -> `completed` **Refund on market move:** `processing` -> `swapping` -> `refunding` -> `refunded` **Lightning to direct BTC (Spark or Bitcoin L1):** `processing` -> `confirming` -> `delivering` -> `completed` Terminal statuses are `completed`, `failed`, `expired`, `unfulfilled`, and `refunded`. `unfulfilled` is the one terminal status that can resume: a late deposit moves the order back through `confirming`, `bridging`, `swapping`, and `delivering` to `completed`. Treat it as settled for now and keep listening; webhooks fire when the order resumes. SSE streams close only on `completed`, `failed`, or `refunded`, and stay open through `unfulfilled`. Some `unfulfilled` orders are recovered instead by a **new** order rather than resuming in place — most often when a Bitcoin deposit is replaced (e.g. RBF'd) so the original transaction never confirms and the confirmed replacement is picked up separately. The original stays `unfulfilled` and links to its successor via `supersededByOperationId` (with a dedicated `order.superseded` webhook), while the successor carries `recoveredFromOperationId`. See [Recovered (superseded) orders](/products/orchestration/api/webhook-events#recovered-superseded-orders). Not every status appears in every route. A Spark-source sell may skip `confirming` and `bridging`. Track progress via webhooks or poll `GET /v1/orchestration/status` rather than assuming a fixed sequence. ## Field mapping Fields shift names and semantics between the quote response and the order (status/webhook payload). | Concept | Quote response | Order status / Webhook | Notes | | ------------------------ | ------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | Input amount | `amountIn` | `amountIn` | May differ from quote if actual deposit is over/under | | Output amount | `estimatedOut` | `amountOut` | Quote: estimated. Order: actual. `null` until delivery completes | | Platform fee | `feeAmount` | `feeAmount` | Recalculated if actual deposit differs from quote | | Stable rounding retained | `roundingFeeAmount` | `roundingFeeAmount` | Stablecoin surplus retained instead of delivered. Used when output is capped to a whole-cent or exact-out target | | Total fees | `totalFeeAmount` | -- | Only on quote response when USDC fee asset | | App fees | `appFeeAmount` | -- | Only on quote when appFees/affiliateId requested | | App fee platform cut | `appFeePlatformCutAmount` | -- | Flashnet's 20% cut of app fees. Only on quote when appFees/affiliateId requested | | Deposit target | `depositAddress` | `depositAddress` | Same value carried from quote to order | | Quote reference | `quoteId` | `quoteId` | Order references its source quote | | Order reference | -- | `id` (order) | Only exists after submit | | Destination tx | -- | `destination.txHash` (webhook) | Populated on delivery | When the actual deposit differs from the quoted amount, the engine updates `amountIn` and `feeAmount` on the order before proceeding. An `amount_reconciled` stage is recorded. Webhook payloads and status responses reflect the updated values. ## What happens if the deposit amount differs from the quote? Deposits do not need to match the quoted `amountIn` exactly. Any positive deposit is accepted. The engine adjusts `amountIn` and fees to reflect the actual deposit before execution. Slippage protection is enforced at swap execution time against the quote's `slippageBps`. * **Underpayment**: accepted for exact-in orders. Smaller deposits produce less price impact, so the per-unit rate stays the same or improves. Exact-out orders that receive less than `requiredAmountIn` refund automatically with `exact_out_insufficient_input`. * **Overpayment**: accepted for exact-in orders. Exact-out orders that receive more than `maxAcceptedAmountIn` refund automatically with `exact_out_input_above_max`. * **Market moved past slippage**: if the pool price shifts past `slippageBps` between deposit and execution, the order refunds automatically with `slippage_exceeded`. No partner action is required. Lightning deposits are excluded from flexible amounts since invoices are fixed-amount by protocol design. ## Market moves and refunds Orders that cannot execute cleanly refund without partner involvement. The relevant `errorCode` values on the order record are: * `slippage_exceeded`: pool moved past `slippageBps` between deposit and execution * `exact_out_insufficient_input`: exact-out deposit below `requiredAmountIn` * `exact_out_input_above_max`: exact-out deposit above `maxAcceptedAmountIn` * `exact_out_target_not_met`: pool couldn't produce `targetAmountOut` at execution time All refund paths emit `order.refunding` followed by `order.refunded`. Partners should render these as normal refund outcomes. There is no accept/decline endpoint. Once the deposit lands, execution proceeds automatically or refunds. See [Error codes](/products/orchestration/api/error-codes#order-lifecycle-error-codes) for the full list. ## What are the effective limits? Order size limits combine several constraint types: * Per-direction USD notional bounds, tuned by operators at runtime. These change without notice, so do not hardcode them. * Per-route minimums for specific destinations. * Provider limits applied at quote time on bridged routes. * Static BTC minimums enforced in code: 5,000 sats swap input for Bitcoin L1 deposits, 1,200 sats swap input for Spark deposits, and 10,000 sats for Bitcoin L1 delivery. * An `amountFiatUsd` band of $1.00 to $50,000.00 on onramp and pay links. Call `GET /v1/orchestration/limits` for the current limits on any route. The endpoint is public and authoritative; query it instead of hardcoding numbers. Some USDC-source routes include a sweep fee (`sweepFeeAmount` in the quote response). The sweep fee is deducted from the input before execution, so it raises the effective minimum above the published route minimum. Check the quote response for `sweepFeeAmount` on affected routes. For `lightning:BTC -> bitcoin:BTC`, the final on-chain withdrawal also pays the network withdrawal fee quoted at delivery time. That fee is separate from the platform fee shown on the quote or order. ## Next steps * [Quickstart](/products/orchestration/integration) * [ZeroConf](/products/orchestration/zeroconf) * [API: Quotes and Orders](/products/orchestration/api/quotes-and-orders) * [API: Approval Flows](/products/orchestration/api/approval-flows) (ZeroConf only) * [API: Error codes](/products/orchestration/api/error-codes) # Orchestra Source: https://docs.flashnet.xyz/products/orchestration/overview Flashnet Orchestra is an async orchestration protocol. It gives you deposit instructions, executes the route, and delivers the output asset to your recipient. ## What can I build with Orchestra? ## Start here Build a minimal end-to-end flow. Compare quote/submit, onramp, pay link, and reusable address patterns. State machine, field mapping, and how quotes become orders. ## Routes Convert stablecoins and major assets into BTC on Spark, Bitcoin L1, or Lightning. Convert BTC or USDB into stablecoins and major assets on any supported chain. ## Fiat onramp The cheapest fiat-to-crypto path. Users pay via any Lightning-compatible app; Orchestra delivers the destination asset. Single API call. Shareable payment links that never expire. Each click creates a fresh Lightning invoice at the current rate. ## Reusable deposit addresses Reusable deposit address on any supported source chain that auto-delivers BTC or USDB on Spark. Reusable Bitcoin L1 address that auto-delivers to any supported chain and asset. You specify source asset, destination asset, amount, and recipient. Orchestra returns a deposit instruction, detects the deposit, executes, and delivers. Every route uses the same interfaces, so clients remain simple and flexible. You do not integrate Spark, manage liquidity, run Bitcoin nodes, or operate bridges. Orchestra abstracts three primitives behind that interface: a bridge that moves stablecoin value between chains and Spark, an AMM that swaps USDB and BTC on Spark liquidity, and Bitcoin ingress handling with confirmation-based or [ZeroConf](/products/orchestration/zeroconf) deposit processing. ## Chains and assets Orchestra routes connect the Bitcoin zone (Bitcoin L1, Lightning, and Spark with BTC and USDB) with stablecoins and major assets on EVM chains, Solana, Tron, TON, and other networks. Cross-chain routes between non-BTC assets are also served, exact-in only. The route list changes as chains and assets are added, so discover pairs at runtime with `GET /v1/orchestration/routes` (contract addresses, decimals, and eligibility flags per entry) and check per-route amount bounds with `GET /v1/orchestration/limits`. ## Pricing, fees, and limits Quotes are firm for 2 minutes. Late deposits are repriced at the live market rate and execute within the quote's `slippageBps`, refunding automatically when they cannot. [Pricing](/products/orchestration/concepts/pricing) covers estimates, quotes, and fixed delivery. Fees are embedded in the quoted output: a platform fee set per partner and route, the Spark pool's LP fee on AMM routes, and optionally your own app or affiliate fees, of which Flashnet keeps 20%. [Fees](/products/orchestration/concepts/fees) covers the full model, including invoice billing and affiliate claims. Amount bounds are per route and operator-tuned; query `GET /v1/orchestration/limits` instead of hardcoding numbers. [Amounts](/products/orchestration/concepts/amounts) covers units, amount modes, and `amountFiatUsd`. ## Next steps * [Quickstart](/products/orchestration/integration): Build a minimal end-to-end flow * [Concepts](/products/orchestration/concepts/pricing): Pricing, amounts, fees, and the order lifecycle * [Webhooks](/products/orchestration/webhooks): Receive order status updates * [API Reference](/products/orchestration/api/overview): Complete endpoint documentation # Pay Links Source: https://docs.flashnet.xyz/products/orchestration/pay-links Pay Links are durable URLs that deliver a stablecoin payment via Lightning. Pick the denomination at link creation: * `amountOut` pins the receiver's amount. The sender pays it plus fees on top. Cash App displays the sats required to cover output and fees. Stablecoin `amountOut` values must be whole-cent values. * `amountFiatUsd` pins the sender's USD amount. Cash App displays exactly that figure, the receiver gets the net after fees, and the BTC/USD spot is locked at click time. Each click generates a fresh Lightning invoice and redirects to a Lightning-compatible payment app. The link itself never expires. ```text theme={null} https://orchestration.flashnet.xyz/pay/DNHKySOZ ``` Pay Links share the pipeline with the [fiat onramp](/products/orchestration/onramp). Onramp orders are created per request; Pay Links pre-encode destination, recipient, and denomination once and create an order on every click. Pay Links are not available to residents of New York City. ## Which mode should I use? | | `amountOut` | `amountFiatUsd` | | ---------------- | ------------------------------------------- | ------------------------------------------------------------------------- | | Pin point | Receiver's stablecoin amount | Sender's USD amount | | Cash App display | Sats equivalent to output + fees | Exact USD figure on the link | | Fee position | Added on top of sender's payment | Deducted from receiver's output | | Spot drift | Reprices each click | Reprices each click | | Best for | Invoices, donations of a fixed token amount | "Send me \$X" flows where the displayed dollar amount must match the link | `amountFiatUsd` is the right pick when the user expects Cash App to show the exact figure on the link. `amountOut` is the right pick when the receiver must end up with an exact stablecoin amount regardless of fees. For 6-decimal stablecoins, use multiples of `10000`, for example `"50000000"` for \$50.00. ## How it works 1. Partner calls `POST /v1/pay-links` with a destination chain, asset, recipient address, and exactly one of `amountOut` or `amountFiatUsd`. 2. Orchestra returns a `shortId` and `shortUrl`. 3. Each click creates a fresh order. For `amountOut`, Orchestra reverse-prices the BTC input to deliver the requested output. For `amountFiatUsd`, Orchestra fetches BTC/USD spot, converts the USD figure to sats, and runs in `exact_in` mode. 4. After payment, Orchestra swaps BTC to the destination asset and delivers it. If the link is shared in a group chat and three people click it, three independent orders are created. Spot is fetched per click, so each order reflects the rate at the moment of payment. ## What can I build with pay links? * **Payment requests**: "Pay me \$50 USDC" as a link. Share via text, email, QR code, or embed in a website. * **Invoicing**: Generate a link per invoice line item. The recipient pays the exact amount without manual entry. * **Donations and tips**: A fixed-amount link on a profile or stream overlay. * **Recurring collection**: Reuse the same link for repeated payments to the same address. ## Which destinations are supported? Pay Links support any stablecoin destination that Orchestra serves from a `lightning:BTC` source. For the current list, call [`GET /v1/orchestration/routes`](/products/orchestration/api/quotes-and-orders#get-v1orchestrationroutes) and filter to `lightning:BTC` source pairs with a stablecoin destination. Stablecoin `amountOut` values must be whole-cent amounts. ## Integration Receiver-pinned (`amountOut`): ```ts theme={null} const BASE_URL = 'https://orchestration.flashnet.xyz'; const response = await fetch(`${BASE_URL}/v1/pay-links`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, 'X-Idempotency-Key': `paylink:${Date.now()}`, }, body: JSON.stringify({ destinationChain: 'solana', destinationAsset: 'USDC', recipientAddress: 'So1YourSolanaAddress...', amountOut: '50000000', // $50 USDC (6 decimals) label: 'Invoice #1234', }), }).then((r) => r.json()); ``` Sender-pinned (`amountFiatUsd`): ```ts theme={null} const response = await fetch(`${BASE_URL}/v1/pay-links`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, 'X-Idempotency-Key': `paylink:${Date.now()}`, }, body: JSON.stringify({ destinationChain: 'solana', destinationAsset: 'USDC', recipientAddress: 'So1YourSolanaAddress...', amountFiatUsd: '50.00', // sender pays exactly $50 in Cash App label: 'Tip jar', }), }).then((r) => r.json()); ``` Response: ```json theme={null} { "payLink": { "id": "pl_abc123", "shortId": "DNHKySOZ", "destinationChain": "solana", "destinationAsset": "USDC", "recipientAddress": "So1YourSolanaAddress...", "amountOut": null, "amountFiatUsd": "50.00", "amountFiatCurrency": "USD", "affiliateId": null, "label": "Tip jar", "enabled": true, "createdAt": "2026-01-15T12:00:00.000Z", "updatedAt": "2026-01-15T12:00:00.000Z", "shortUrl": "https://orchestration.flashnet.xyz/pay/DNHKySOZ" } } ``` For receiver-pinned links, `amountOut` carries the value and the fiat fields are `null`. Exactly one of `amountOut` or `amountFiatUsd` is set on every link. Share the `shortUrl` anywhere. On click: * Orchestra creates an exact-out Lightning quote (BTC sats calculated from the desired stablecoin output at current rates). * A fresh BOLT11 invoice is generated. * The browser redirects to `https://cash.app/launch/lightning/{invoice}`. * The user's Lightning-compatible app opens with the payment ready. ``` https://orchestration.flashnet.xyz/pay/DNHKySOZ ``` The default link serves a landing page with OpenGraph metadata (title, image) for rich previews in iMessage, WhatsApp, Twitter, etc., then redirects the user to pay the Lightning invoice. If you want to handle your own messaging previews or embed the link in a custom UI, use the `/go` variant to skip the landing page and go straight to quote creation and payment redirect: ``` https://orchestration.flashnet.xyz/pay/DNHKySOZ/go ``` Each click creates a standard Orchestra order. Track them the same way as any other order: via webhooks or polling. Outbound `order.*` webhooks include `payLinkId` (and `payLinkLabel` when the link was created with a `label`) for orders originated from a pay-link, so you can reconcile events without keeping a separate mapping. Orders created from a pay link are ordinary orders delivered to the link's recipient address. The API has no pay-link filter; list them by recipient address on `/history` (which accepts `address` as required, plus optional `status`, `limit` default 50 max 200, and `offset`), or reconcile individual events via the `payLinkId` webhook field. ```ts theme={null} const orders = await fetch( `${BASE_URL}/v1/orchestration/history?address=So1YourSolanaAddress...`, { headers: { Authorization: `Bearer ${API_KEY}` } } ).then((r) => r.json()); ``` Disabled links return a 404 page. Existing orders created before disabling continue to process. ```ts theme={null} await fetch(`${BASE_URL}/v1/pay-links/pl_abc123`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, 'X-Idempotency-Key': `disable:${Date.now()}`, }, }); ``` ## Frontend integration Pay Links share the browser-side deep link patterns with Onramp. For the Cash App navigation trick, the SSE order-tracking hook, the API-key proxy pattern, and the pipeline-progress UI, see [Deep link best practices](/products/orchestration/deeplink-best-practices). ## API reference Field-level documentation for `POST /v1/pay-links`, `GET /v1/pay-links`, `GET /v1/pay-links/:id`, and `DELETE /v1/pay-links/:id` lives in [Resources](/products/orchestration/api/resources#pay-links). ## What are the fees? Platform fees use custom pricing based on route and volume. Affiliate fees are variable, configured per affiliate. Where fees land depends on which mode the link uses: * `amountOut` mode: fees are added on top of the sender's Lightning invoice. The receiver gets exactly `amountOut`. * `amountFiatUsd` mode: fees are deducted from the receiver's output. The sender pays exactly `amountFiatUsd` in BTC at click-time spot. When execution produces stablecoin surplus above a receiver-pinned amount, Orchestra retains the surplus as `roundingFeeAmount`. This does not change the receiver's `amountOut`. ### Affiliate fees Pay links support affiliate fees through the `affiliateId` field. Register an affiliate via `PUT /v1/affiliates/:affiliateId` first, then reference it when creating the link. Fees follow the same position as the platform fee: on top in `amountOut` mode, deducted from output in `amountFiatUsd` mode. ```json theme={null} { "destinationChain": "solana", "destinationAsset": "USDC", "recipientAddress": "So1...", "amountOut": "50000000", "affiliateId": "my-partner" } ``` ## Pricing Each click computes a fresh quote. The amount the sender pays reflects: 1. The pinned amount: receiver-side (`amountOut`) or sender-side (`amountFiatUsd`). 2. Platform fee (route-dependent). 3. Affiliate fees (if configured). 4. BTC/USDB swap rate at the moment of the click. For `amountFiatUsd`, the BTC/USD spot snapshot is also recorded on the order webhook as `spotUsdPerBtc`. For `amountFiatUsd` orders, the order webhook payload mirrors `amountFiatUsd`, `amountFiatCurrency`, and `spotUsdPerBtc` at the top level so partners can reconcile the displayed Cash App figure with the delivered output without parsing `paymentIntent`. ## Next steps * [Fiat Onramp](/products/orchestration/onramp) for the input-denominated variant * [Order Lifecycle](/products/orchestration/order-lifecycle) for status tracking * [Webhooks](/products/orchestration/webhooks) for real-time order updates # Reusable addresses Source: https://docs.flashnet.xyz/products/orchestration/reusable-addresses Accumulation and liquidation addresses are the same primitive with the input asset swapped. Both are persistent deposit addresses. Both run every inbound deposit through a fee plan that is resolved and frozen at creation time. The two address types differ only in direction. Accumulation takes an inbound supported asset and delivers BTC or USDB on Spark. Liquidation takes inbound BTC on Bitcoin L1 and delivers the output on any supported destination chain. ## What are accumulation addresses? An accumulation address is a persistent deposit address on Solana or any other supported source chain. Each inbound deposit automatically creates an order that converts to USDC if needed, bridges to Spark, and then either delivers `USDB` to a fixed Spark address or swaps into `BTC` and delivers the BTC to a fixed Spark address. Hand out one address per user and skip calling `/quote` and `/submit` per deposit. Live pairs, per-asset contract addresses, decimals, and chain ids come from `GET /v1/orchestration/routes`. Per-route amount bounds come from `GET /v1/orchestration/limits`. Never hardcode contract addresses; read them from the route entry's source and destination detail objects. Creation validates the requested route server-side and rejects unsupported combinations at create time. ### Create an accumulation address ```ts theme={null} const BASE_URL = 'https://orchestration.flashnet.xyz'; const res = await fetch(`${BASE_URL}/v1/accumulation-addresses`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, 'X-Idempotency-Key': `acu:create:${Date.now()}`, }, body: JSON.stringify({ label: 'User 123', sourceChain: 'solana', sourceAsset: 'USDC', destinationAsset: 'BTC', recipientSparkAddress: 'spark1UserRecipient...', feeBps: 5, slippageBps: 50, appFees: [ { recipient: 'So1FeeRecipient...', fee: 50 }, ], }), }).then((r) => r.json()); console.log(res.depositAddress); ``` Response fields: * `depositAddress`: the address to receive deposits (a Solana address for Solana sources, an on-chain deposit address for other chain sources). * `appFees`: array of configured fee recipients, present when a fee plan was configured at creation. * `subscriptions`: status of server-side deposit detection subscriptions (Solana-sourced only). Response example: ```json theme={null} { "accumulationAddressId": "acu_...", "sourceChain": "solana", "sourceAsset": "USDC", "destinationAsset": "BTC", "recipientSparkAddress": "spark1...", "feeBps": 5, "slippageBps": 50, "enabled": true, "depositAddress": "So1...", "createdAt": "2026-02-04T01:00:00.000Z", "appFees": [ { "recipient": "So1FeeRecipient...", "feeBps": 50 } ], "subscriptions": [] } ``` ### What are the deposit requirements? On non-Solana chains, deposits must be plain token transfers to the deposit address. The deposit transaction should contain no calldata beyond a standard ERC-20 `transfer()`. Deposits sent through smart-contract interactions (multisig executions, payment routers, other contract-mediated transfers) are not detected automatically. The funds arrive on-chain, but order creation does not trigger. If a contract-mediated deposit lands by mistake, call the reindex endpoint for that accumulation address. The endpoint asks Relay to rescan the stored deposit address and configured token. If Relay finds a balance, it queues the sweep and the normal fill-detection path creates the order. ```bash theme={null} curl --request POST "$BASE_URL/v1/accumulation-addresses//reindex" \ --header "Authorization: Bearer $FLASHNET_API_KEY" \ --header "X-Idempotency-Key: acu:reindex:" ``` Use the reusable `depositAddress` from the create response when you have it. The `acu_...` id works too. This endpoint is only for Relay-backed accumulation addresses, not Bitcoin liquidation addresses. Solana-sourced deposits do not have this limitation. ### How does a deposit move through the system? 1. Your user sends a supported deposit asset to the returned deposit address. 2. Flashnet detects the deposit and creates an order. 3. The engine processes the order asynchronously. 4. You observe progress via partner webhooks. ### Manage accumulation addresses * `GET /v1/accumulation-addresses` lists accumulation addresses. * `GET /v1/accumulation-addresses/:id` fetches a single address. * `DELETE /v1/accumulation-addresses/:id` disables an address (requires `X-Idempotency-Key`). * `POST /v1/accumulation-addresses/:idOrAddress/reindex` asks Relay to rescan one Relay-backed deposit address. `:idOrAddress` can be the reusable deposit address or the `acu_...` id. Requires `X-Idempotency-Key`. * `POST /v1/accumulation-addresses/sync` re-registers all enabled Solana-sourced addresses with the configured deposit detection webhooks. Chain-sourced addresses do not use webhook subscriptions. Intended for operational recovery and self-hosted deployments. ## What are liquidation addresses? A liquidation address is a persistent Bitcoin L1 deposit address. Each inbound BTC deposit automatically creates an order that swaps BTC and delivers the output asset to a fixed destination on any supported chain. Live destination pairs come from `GET /v1/orchestration/routes`; creation rejects unsupported combinations. Hand out a static `bc1...` address and skip calling `/quote` and `/submit` per deposit. Orders created from liquidation deposits have: * `sourceChain = bitcoin` * `sourceAsset = BTC` * `destinationChain` and `destinationAsset` taken from the liquidation address configuration * `quoteId = null` ### Create a liquidation address ```ts theme={null} const BASE_URL = 'https://orchestration.flashnet.xyz'; const res = await fetch(`${BASE_URL}/v1/liquidation-addresses`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, 'X-Idempotency-Key': `liq:create:${Date.now()}`, }, body: JSON.stringify({ label: 'Merchant settlement', destinationChain: 'ethereum', destinationAsset: 'USDC', destinationAddress: '0xYourEthereumAddress', feeBps: 5, slippageBps: 50, appFees: [ // Inline appFees recipients must live on the fee settlement chain, // not the destination chain. For bitcoin:BTC -> ethereum:USDC, // settlement is Solana USDC, so this must be a Solana address. { recipient: 'So1FeeRecipient...', fee: 50 }, ], }), }).then((r) => r.json()); // res.l1DepositAddress is the reusable Bitcoin address. console.log(res.l1DepositAddress); ``` Response fields: * `liquidationAddressId`: stable id. * `l1DepositAddress`: Bitcoin deposit address (`bc1...`). * `sparkAddress`: Spark address used internally for deposit claiming and sweeps. * `destination.chain`, `destination.asset`, `destination.address`: fixed payout destination. * `appFees`: array of configured fee recipients, present when a fee plan was configured at creation. Response example: ```json theme={null} { "liquidationAddressId": "liq_...", "sparkAddress": "spark1...", "l1DepositAddress": "bc1...", "destination": { "chain": "ethereum", "asset": "USDC", "address": "0x..." }, "feeBps": 5, "slippageBps": 50, "enabled": true, "createdAt": "2026-02-04T01:00:00.000Z", "appFees": [ { "recipient": "So1FeeRecipient...", "feeBps": 50 } ] } ``` ### How are deposits attributed? Each Bitcoin deposit is identified by `(txid, vout)`, where `vout` is the output index that paid the liquidation address. A single Bitcoin transaction can contain multiple outputs to different liquidation addresses, or multiple outputs to the same address. For reliable attribution, use webhooks and store `order.id` from the payload. You can also list an address's orders on demand with `GET /v1/liquidation-addresses/orders` (see [Manage liquidation addresses](#manage-liquidation-addresses)). ### ZeroConf All Bitcoin L1 deposits to liquidation addresses are ZeroConf-eligible. See [ZeroConf](/products/orchestration/zeroconf) for the full offer flow, confirmation behavior, and accept or decline endpoints. ### Manage liquidation addresses * `GET /v1/liquidation-addresses` lists enabled liquidation addresses. * `GET /v1/liquidation-addresses/:id` fetches a single address, enabled or disabled. * `GET /v1/liquidation-addresses/orders` lists the orders for a liquidation address by `id` or `label`. * `DELETE /v1/liquidation-addresses/:id` disables an address (requires `X-Idempotency-Key`). ## How do app fees and affiliates work? Both primitives accept `appFees` or `affiliateIds` at creation. Flashnet resolves and validates the plan at that moment: affiliate profiles are looked up, recipient addresses are validated against the fee settlement chain, and the settlement chain itself is locked in. The resolved plan is then frozen onto the address. Every subsequent deposit replays the frozen plan without re-evaluating profiles. Editing an affiliate profile later does not affect addresses that were created before the edit. The two models are mutually exclusive. Pick one at creation time. | Fee type | Behavior | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Inline `appFees`** | Array of `{ recipient, fee }` objects. Fees pay out immediately to each recipient during order execution. Recipients must be valid addresses on the fee settlement chain, not the destination chain. | | **Registered `affiliateIds`** | Array where each entry is a plain affiliate id string or a `{ affiliateId, feeBps }` object that overrides the profile's default fee bps for this address only, without mutating the stored profile. Fees are held back, accumulated for dashboard visibility, and delivered to the affiliate's configured payout destination on claim. | ```ts theme={null} body: JSON.stringify({ // accumulation example; liquidation uses destinationChain/destinationAsset/destinationAddress sourceChain: 'solana', sourceAsset: 'USDC', destinationAsset: 'BTC', recipientSparkAddress: 'spark1UserRecipient...', feeBps: 5, slippageBps: 50, affiliateIds: [ 'referrer-alice', { affiliateId: 'platform-fee', feeBps: 25 }, ], }), ``` Fee math follows the same rules as orchestration quotes. The platform fee is deducted first, then each app or affiliate fee is applied to the remainder. A 20% platform cut is retained on every app fee. The fee settlement chain depends on the route: * Destinations on a supported chain (Solana, any EVM chain, and similar) settle fees on Solana USDC. * `spark:USDB` destinations settle fees on Spark USDB. * `spark:BTC`, `bitcoin:BTC`, and `lightning:BTC` destinations do not support fee plans, because the route has no stablecoin leg to settle against. For a `bitcoin:BTC -> ethereum:USDC` liquidation address, the settlement chain is Solana USDC, so inline `appFees` recipients must be Solana addresses. The `appFees` field in create, list, and get responses reflects the frozen plan. Each entry always has `recipient` and `feeBps`. Entries backed by a registered affiliate also carry `affiliateId`, and their `recipient` is the profile's configured `payoutAddress`. Inline entries omit `affiliateId`. The array is omitted entirely from responses for addresses created without a fee plan. You cannot combine `appFees` and `affiliateIds` on the same address. Pick one model at creation time. For fee math, affiliate registration, and claim payouts, see [Quotes and Orders](/products/orchestration/api/quotes-and-orders#post-v1orchestrationquote) and [Resource Management](/products/orchestration/api/resources#affiliates). ## Webhooks Deposits on both primitives produce normal `order.*` webhook events. Register an endpoint with `POST /v1/webhooks`, verify `X-Flashnet-Signature` on inbound requests, and treat webhook delivery as at-least-once. See [Webhooks](/products/orchestration/webhooks). ## Next steps * [API Reference](/products/orchestration/api/overview) * [Stablecoin to BTC](/products/orchestration/stablecoin-to-btc) * [BTC to Stablecoin](/products/orchestration/btc-to-stablecoin) * [Webhooks](/products/orchestration/webhooks) # Risk & Compliance Source: https://docs.flashnet.xyz/products/orchestration/risk-and-compliance Flashnet screens orders for sanctions and illicit-finance risk before they settle. The checks run automatically through [Elliptic](https://www.elliptic.co/) and other third-party providers used across the industry for anti-money-laundering and sanctions compliance. ## What we screen Every order carries a source address and a destination address. Flashnet checks both against third-party risk data from Elliptic and other providers, which covers Bitcoin and the other chains Orchestra routes across. The check looks for sanctioned entities, known illicit actors, and exposure to high-risk activity such as theft, fraud, and darknet markets. Screening is part of order processing. It needs no extra step from your integration and does not change the shape of the API response. ## What happens on a result An order that clears screening settles normally. An order that comes back high-risk is flagged for compliance review before it can settle, and an order tied to a sanctioned or prohibited address is rejected. A flagged order surfaces through the normal order status, so your integration reacts the same way it does to any other state change. Flashnet settles without taking custody of user funds. Screening applies to the addresses on each order rather than to balances we hold, because we hold none. ## Law enforcement and regulatory requests Law enforcement and regulatory authorities can submit formal requests to Flashnet's legal team at [legal@flashnet.xyz](mailto:legal@flashnet.xyz). ## Our commitment Screening is built into how Orchestra processes orders, not a manual pass added after settlement. Flashnet runs these checks continuously, keeps a durable record of every screening decision, and aligns its controls with global standards for anti-money-laundering and sanctions enforcement. # Stablecoin to BTC Source: https://docs.flashnet.xyz/products/orchestration/stablecoin-to-btc This flow starts with a funded source asset on any supported chain and ends with BTC delivered to Spark, Bitcoin L1, or Lightning, or USDB delivered to Spark. If you want a reusable deposit address that runs this flow automatically on each deposit, use [reusable addresses](/products/orchestration/reusable-addresses#what-are-accumulation-addresses). Common uses: * Wallet buy flow: users pay USDC and receive BTC to a Spark address, Bitcoin address, or Lightning invoice. * Payout rails: convert USDC into BTC without running Bitcoin infrastructure. Live pairs, per-asset contract addresses, decimals, and chain ids come from `GET /v1/orchestration/routes`. Per-route amount bounds come from `GET /v1/orchestration/limits`. Never hardcode contract addresses; read them from the route entry's source and destination detail objects. ## Flow 1. Create a quote: `POST /v1/orchestration/quote`. 2. Send the source asset to `depositAddress` for `amountIn`. 3. Submit the deposit transaction: `POST /v1/orchestration/submit`. 4. Track status via webhooks or `GET /v1/orchestration/status?id=...`. Quotes expire 2 minutes after creation. Late deposits are always repriced at live market rates at detection time and execute against the quote's `slippageBps`. ## Affiliate fees App fees are supported on these routes when `amountMode=exact_in`. Fees are computed in USDC and settled on Solana. Flashnet retains a 20% platform cut of all app fees; the remaining 80% goes to the fee recipient. Affiliate fees use a holdback model where the recipient's 80% share accumulates and is claimed by the affiliate. For `SOL`/`ETH` routes, the engine converts to USDC first, then applies fees. See [Quotes and Orders](/products/orchestration/api/quotes-and-orders#post-v1orchestrationquote) for the full affiliate fee model. ## Example: USDC (Base) to BTC (Spark) ```ts theme={null} const BASE_URL = 'https://orchestration.flashnet.xyz'; const quote = await fetch(`${BASE_URL}/v1/orchestration/quote`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, // Required for authenticated quotes. 'X-Idempotency-Key': `quote:${Date.now()}`, }, body: JSON.stringify({ sourceChain: 'base', sourceAsset: 'USDC', destinationChain: 'spark', destinationAsset: 'BTC', amount: '100000000', recipientAddress: 'spark1...', slippageBps: 50, }), }).then((r) => r.json()); // Send USDC to quote.depositAddress for quote.amountIn. // Capture the deposit transaction hash. const txHash = await sendUsdcOnBase({ to: quote.depositAddress, amountUsdcSmallest: quote.amountIn, }); const submit = await fetch(`${BASE_URL}/v1/orchestration/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FLASHNET_API_KEY}`, 'X-Idempotency-Key': `submit:${quote.quoteId}:${txHash}`, }, body: JSON.stringify({ quoteId: quote.quoteId, txHash, // Optional but recommended. When present, the deposit verifier checks the sender address. sourceAddress: '0xYourSenderAddress', }), }).then((r) => r.json()); const status = await fetch( `${BASE_URL}/v1/orchestration/status?id=${encodeURIComponent(submit.orderId)}`, ).then((r) => r.json()); console.log(status.order.status); ``` ## Example: SOL (Solana) to BTC (Spark) ```json theme={null} { "sourceChain": "solana", "sourceAsset": "SOL", "destinationChain": "spark", "destinationAsset": "BTC", "amount": "25000000", "recipientAddress": "spark1...", "slippageBps": 50 } ``` `amount` is lamports (`1 SOL = 1_000_000_000 lamports`). ## Example: SOL (Solana) to BTC (Spark) with affiliate fee ```json theme={null} { "sourceChain": "solana", "sourceAsset": "SOL", "destinationChain": "spark", "destinationAsset": "BTC", "amount": "100000000", "recipientAddress": "spark1...", "appFees": [ { "recipient": "Dtkxt55zEUDj6NTXGRtH8uQsACsTjoSVkXfYBEF8xkkT", "fee": 100 } ], "slippageBps": 50 } ``` ## Example: SOL (Solana) to BTC (Spark) with affiliate registry ```json theme={null} { "sourceChain": "solana", "sourceAsset": "SOL", "destinationChain": "spark", "destinationAsset": "BTC", "amount": "100000000", "recipientAddress": "spark1...", "affiliateId": "flashpartner", "slippageBps": 50 } ``` ## Example: USDC to BTC (Bitcoin L1) Set `destinationChain = bitcoin` and provide a Bitcoin address in `recipientAddress`. ```json theme={null} { "sourceChain": "base", "sourceAsset": "USDC", "destinationChain": "bitcoin", "destinationAsset": "BTC", "amount": "100000000", "recipientAddress": "bc1...", "slippageBps": 50 } ``` The submit step is the same as any USDC source quote. ## Example: USDC to BTC (Lightning) Lightning-destination routes use custom pricing. USDB routes get the lowest rates. For Lightning payouts, `recipientAddress` must be a BOLT11 invoice. Use an amountless (0-amount) invoice for exact-in mode, or an invoice with an amount matching the target sats for exact-out mode. Amountless invoices are accepted in both modes. ```json theme={null} { "sourceChain": "solana", "sourceAsset": "USDC", "destinationChain": "lightning", "destinationAsset": "BTC", "amount": "25000000", "recipientAddress": "lnbc1...", "slippageBps": 50 } ``` The submit step is the same as any USDC source quote. ## Example: USDC to BTC (Lightning, exact out) Use exact-out when the recipient must receive a precise number of sats on Lightning. The system determines the required USDC input and includes a routing fee buffer. ```json theme={null} { "sourceChain": "base", "sourceAsset": "USDC", "destinationChain": "lightning", "destinationAsset": "BTC", "amount": "100000", "amountMode": "exact_out", "recipientAddress": "lnbc1u1...", "refundAddress": "0xYourBaseUsdcRefundAddress", "slippageBps": 0 } ``` `amount` is the target BTC delivery in sats. The `recipientAddress` can be an amountless invoice or an invoice encoding the same amount (100,000 sats in this case). The quote response includes the same exact-out fields as BTC-to-USDC exact-out: * `targetAmountOut`: the requested sats (matches `amount`) * `requiredAmountIn`: USDC to deposit * `maxAcceptedAmountIn`: upper bound on accepted deposit * `inputBufferBps`: buffer applied to the required input Affiliate fees (`appFees` / `affiliateId` / `affiliateIds`) are not supported with `amountMode=exact_out`. ## Example: USDT (Ethereum) to BTC (Spark) For chain sources other than Solana, the quote returns a deposit address on the source chain. The deposit is detected automatically. ```json theme={null} { "sourceChain": "ethereum", "sourceAsset": "USDT", "destinationChain": "spark", "destinationAsset": "BTC", "amount": "10000000", "recipientAddress": "spark1...", "slippageBps": 50 } ``` `amount` is in USDT smallest units (6 decimals). The `depositAddress` in the response is an Ethereum address. Send USDT there and the order is created automatically. ## Deposit verification The engine verifies that the deposit transaction sent the correct asset to the quoted `depositAddress`. When `sourceAddress` is provided on submit, the verifier also confirms the sender matches. Deposits do not need to match the quoted `amountIn` exactly. See [Deposit Amount Flexibility](/products/orchestration/order-lifecycle#what-happens-if-the-deposit-amount-differs-from-the-quote) for details on how over/underpayments are handled. ## Next steps * [API Reference](/products/orchestration/api/overview) * [BTC to Stablecoin](/products/orchestration/btc-to-stablecoin) * [Webhooks](/products/orchestration/webhooks) # Attestations Source: https://docs.flashnet.xyz/products/orchestration/tee-attestation Orchestra's worker runs inside a Trusted Execution Environment. The TEE isolates signing keys so they cannot be extracted, even with root access to the host. You can verify this yourself by requesting a signed attestation document from the API. ## How do I request an attestation? ```bash theme={null} curl "https://orchestration.flashnet.xyz/v1/attestation?nonce=my-random-value" ``` ```json theme={null} { "enclave": true, "nonce": "my-random-value", "document": "" } ``` The `document` field is a COSE\_Sign1 structure signed by the Nitro Secure Module (NSM). It chains to AWS's Nitro Attestation PKI root certificate. The nonce you provide is embedded in the document, proving it was generated fresh for your request. If the worker is not running in an enclave, the endpoint returns `enclave: false` with an error. ## What is in the attestation document? The COSE\_Sign1 document contains a CBOR-encoded payload with: | Field | Description | | ------------- | ----------------------------------------------------------------------------------------------- | | `module_id` | Enclave instance identifier | | `timestamp` | When the document was generated (milliseconds since epoch) | | `pcrs` | Platform Configuration Registers (PCR0 = enclave image hash, PCR1 = kernel, PCR2 = application) | | `nonce` | Your nonce, echoed back to prove freshness | | `certificate` | The NSM signing certificate (X.509 DER) | | `cabundle` | Certificate chain from the NSM cert to the AWS Nitro root CA | ## How do I verify the attestation? ### 1. Decode the document The `document` field is base64-encoded. Decode it, then parse as CBOR. The outer structure is COSE\_Sign1: `[protected_headers, unprotected_headers, payload, signature]`. The `payload` is another CBOR-encoded map containing the attestation fields above. ### 2. Check the nonce Extract the `nonce` field from the payload and confirm it matches what you sent. This proves the document was generated for your specific request, not replayed from an earlier one. ### 3. Verify PCR values PCR0 is the hash of the enclave image. Each build produces a deterministic PCR0 value. You can compare it against the expected value to confirm the exact code running inside the enclave. Current PCR values are published by CI after each build and served at the same endpoint. ### 4. Verify the signature The COSE\_Sign1 signature covers `["Signature1", protected_headers, external_aad, payload]`. Verify it using the public key from the `certificate` field. Then validate the certificate chains through `cabundle` to the [AWS Nitro Attestation PKI root](https://aws-nitro-enclaves.amazonaws.com/AWS_NitroEnclaves_Root-G1.zip). Successful signature and chain validation confirms the document was produced by a real Nitro Secure Module, not fabricated. ## Example: JavaScript verification ```javascript theme={null} import { decode } from "cborg"; const nonce = crypto.randomUUID(); const resp = await fetch( `https://orchestration.flashnet.xyz/v1/attestation?nonce=${nonce}` ); const { document } = await resp.json(); // Decode base64 -> COSE_Sign1 -> payload const raw = Uint8Array.from(atob(document), (c) => c.charCodeAt(0)); const cose = decode(raw, { useMaps: true }); const payload = decode(cose[2], { useMaps: true }); // Convert to object const doc = Object.fromEntries( [...payload.entries()].map(([k, v]) => [String(k), v]) ); // Verify nonce matches const returnedNonce = new TextDecoder().decode(doc.nonce); console.assert(returnedNonce === nonce, "Nonce mismatch"); // Read PCR0 (enclave image hash) const pcr0 = Array.from(doc.pcrs.get(0)) .map((b) => b.toString(16).padStart(2, "0")) .join(""); console.log("PCR0:", pcr0); // Certificate chain depth console.log("Cert chain:", doc.cabundle.length, "certificates"); ``` ## What does the attestation prove? 1. **Worker runs in a Nitro Enclave.** The NSM signature is unforgeable outside real hardware. 2. **Code matches the published build.** PCR0 matches the build hash from CI. 3. **Proof is fresh.** Your nonce is embedded in the signed document. No server outside a Nitro Enclave can produce a valid attestation document. The NSM hardware key is sealed inside AWS's custom silicon and cannot be exported. # Webhooks Source: https://docs.flashnet.xyz/products/orchestration/webhooks Partner webhooks deliver order status changes for your partner account. Partner webhooks are the primary integration for: * orchestration orders * accumulation address deposits (orders have `quoteId = null`) * liquidation address deposits (orders have `quoteId = null`) ## How do I register an endpoint? Create an endpoint: * `POST /v1/webhooks` with `{ "url": "https://..." }` The response includes: * `webhookId` * `secret` (returned once) ## What events are emitted? Webhook events are derived from public `order.status` transitions: * `order.processing` * `order.confirming` * `order.bridging` * `order.swapping` * `order.awaiting_approval` * `order.refunding` * `order.delivering` * `order.completed` * `order.failed` * `order.unfulfilled` * `order.refunded` Internal `paused` transitions do not emit `order.paused`. Public webhook payloads continue to show `data.status` as `processing` until the order resumes or reaches a partner-visible final state. See [Order Lifecycle](/products/orchestration/order-lifecycle) for the full state machine, transition rules, and what each status means. ## What does the payload look like? Webhooks are delivered as an HTTP `POST` with `Content-Type: application/json`. Payload envelope: ```json theme={null} { "event": "order.refunding", "timestamp": "2026-02-04T01:30:47.000Z", "data": { "id": "ord_...", "type": "order", "status": "refunding", "quoteId": "q_...", "amountIn": "250000", "amountOut": null, "feeBps": 5, "feeAmount": "100000", "slippageBps": 50, "source": { "chain": "bitcoin", "asset": "BTC", "address": null, "txHash": "", "sweepTxHash": null }, "destination": { "chain": "base", "asset": "USDC", "address": "0x...", "txHash": null }, "depositAddress": "bc1q...", "recipientAddress": "0x...", "payLinkId": "pl_abc123", "payLinkLabel": "October invoice #42", "flashnetRequestId": null, "sparkTxHash": null, "refund": { "asset": null, "amount": null, "txHash": null }, "error": { "code": "slippage_exceeded", "message": "Pool moved past slippage tolerance between deposit and execution" }, "paymentIntent": { "version": 1, "amountMode": "exact_out", "targetAmountOut": "100000000", "requiredAmountIn": "250000", "maxAcceptedAmountIn": "250050", "inputBufferBps": 2, "actualAmountIn": "249900", "refundAddress": "bc1q...", "exactOutExecution": "strict" }, "feePlan": { "version": 1, "settlementChain": "solana", "settlementAsset": "USDC", "appFees": [ { "affiliateId": "flashpartner", "recipient": "So1AffiliateOne...", "feeBps": 50 }, { "recipient": "So1AffiliateTwo...", "feeBps": 50 } ] }, "feePayouts": { "version": 1, "entries": [ { "idempotencyKey": "order:ord_...:full:appfee:0", "leg": "full", "chain": "solana", "role": "app_fee", "affiliateId": "flashpartner", "recipient": "So1AffiliateOne...", "feeBps": 50, "amount": "24750", "platformCutAmount": "4950", "recipientAmount": "19800", "txHash": "5kW...", "recordedAt": "2026-02-04T01:35:00.000Z" }, { "idempotencyKey": "order:ord_...:full:payout", "leg": "full", "chain": "solana", "role": "recipient_payout", "recipient": "So1RecipientAddress...", "feeBps": null, "amount": "2475000", "platformCutAmount": null, "recipientAmount": null, "txHash": "3hN...", "recordedAt": "2026-02-04T01:35:02.000Z" } ] }, "createdAt": "2026-02-04T01:30:00.000Z", "updatedAt": "2026-02-04T01:30:47.000Z", "completedAt": null } } ``` Notes: * `data` is an order snapshot at the moment the event was emitted. * `paymentIntent` is included on exact-out orders and on orders created with `amountFiatUsd`. For fiat-denominated orders, `data` also mirrors `amountFiatUsd`, `amountFiatCurrency`, and `spotUsdPerBtc` at the top level. * `payLinkId` is included when the order originated from a pay-link. `payLinkLabel` is included when that pay-link was created with a `label`. Use either to reconcile webhook events back to the originating pay-link without maintaining an external mapping. * `zeroconfOffer` is included when a ZeroConf offer has been generated for a Bitcoin L1 deposit. See [ZeroConf](/products/orchestration/zeroconf) for the offer flow and [Offer Fields](/products/orchestration/api/approval-flows#zeroconf-offer-fields) for the field reference. * `feePlan` is included when quote `appFees` or `affiliateId` was requested. * `feePayouts` is included once payout legs are recorded. * `feePayouts.entries[*].role` is `app_fee`, `recipient_payout`, `platform_fee`, or `fee_custody`. * `feePayouts.entries[*].affiliateId` is present for registry-based app-fee entries. * `feePayouts.entries[*].leg` describes fee disposition, not ZeroConf execution: `full` is a completed recipient or fee-custody transfer; `holdback` is an affiliate fee retained for a later claim; `instant` appears only on historical multi-leg records. * `feePayouts.entries[*].amount` is the gross fee. `platformCutAmount` is Flashnet's 20% cut. `recipientAmount` is the 80% paid to the fee recipient. These fields are `null` for `recipient_payout` entries. * When a Flashnet swap has executed, `data.swap` is included with simulation and execution metadata. * Some fields are `null` until the engine reaches that step. ## What stages appear in the status endpoint? `GET /v1/orchestration/status` includes a `stages` array. Stages are monotonic markers recorded when a step completes. Common stage names: * `deposit_confirmed` * `amount_reconciled` (actual deposit differed from quoted amount; `amountIn` and `feeAmount` updated) * `swept` (accumulation/liquidation and other Bitcoin source flows) * `bridged` (bridge completed) * `swapped` (swap completed) * `delivered` * `refund_requested` * `refunded` Bitcoin L1 deposits can also record: * `zeroconf_offer_pending` * `zeroconf_offer_accepted` * `zeroconf_offer_declined` * `zeroconf_accepted` * `deposit_claimed` Treat stages as informational. The primary state machine is `data.status`. ## How do I verify webhook signatures? Each delivery includes two headers: * `X-Flashnet-Signature`: hex-encoded HMAC-SHA256 * `X-Flashnet-Timestamp`: millisecond epoch timestamp of the delivery attempt The signature is computed as: * `hex(HMAC_SHA256(secret, timestamp + "." + raw_body_json))` where `timestamp` is the value of `X-Flashnet-Timestamp`. Verify the signature against the raw request body bytes, not a re-serialized JSON object. Use the timestamp from the header, not from the payload. The signature covers `timestamp.body`, not just `body`. If you are upgrading from a previous integration that verified against the raw body alone, you must update your verification code to prepend the timestamp. Example (Node): ```ts theme={null} import crypto from 'node:crypto'; function timingSafeEqualHex(aHex: string, bHex: string): boolean { if (!/^[0-9a-f]+$/i.test(aHex) || !/^[0-9a-f]+$/i.test(bHex)) return false; if (aHex.length !== bHex.length) return false; const a = Buffer.from(aHex, 'hex'); const b = Buffer.from(bHex, 'hex'); if (a.length !== b.length) return false; return crypto.timingSafeEqual(a, b); } export function verifyFlashnetWebhook(params: { rawBody: string; signatureHeader: string | undefined; timestampHeader: string | undefined; secret: string; }): boolean { if (!params.signatureHeader || !params.timestampHeader) return false; const expected = crypto .createHmac('sha256', params.secret) .update(`${params.timestampHeader}.${params.rawBody}`) .digest('hex'); return timingSafeEqualHex(expected, params.signatureHeader); } ``` ## How are webhooks delivered? * At least once: the same event may be delivered multiple times. * Consider deliveries successful only when your endpoint returns a `2xx` status. Retries use a fixed backoff schedule: * 10 seconds * 30 seconds * 2 minutes * 10 minutes * 30 minutes * 2 hours * 6 hours * 24 hours After the final retry attempt, the delivery is marked failed. ## What should my handler do? * Verify the signature before parsing JSON. * Make processing idempotent. * A safe key is `(data.id, event, timestamp)`. * Return `2xx` only after you have persisted the event. # ZeroConf Source: https://docs.flashnet.xyz/products/orchestration/zeroconf ZeroConf lets eligible Bitcoin L1 deposits continue before the transaction reaches its first block confirmation. ## What does ZeroConf do? When a Bitcoin L1 deposit is detected, the engine asks Spark for an instant static-deposit quote. If Spark returns a 0-conf fulfillment plan, Orchestra creates a ZeroConf offer and moves the order to `awaiting_approval`. Partners accept the offer to continue immediately, or decline it to wait for the normal confirmed path. ## How are confirmations handled? | Scenario | Confirmations required | | -------------------------------------------------- | ---------------------- | | ZeroConf offer accepted | 0 (instant) | | ZeroConf offer declined | 1 | | ZeroConf offer expired (no response) | 1 | | Spark requires confirmation or no offer is created | 1 | ## What is the offer flow? Create an exact-in quote with `sourceChain=bitcoin`. The quote response returns a Bitcoin L1 `depositAddress`. It does not include a ZeroConf offer. After the Bitcoin transaction is broadcast and detected, the engine evaluates the specific UTXO for ZeroConf eligibility. If Spark returns a 0-conf plan, the engine stores a pending `zeroconfOffer` and moves the order to `awaiting_approval`. If Spark requires confirmation, the order waits for 1 block. Call `POST /v1/orchestration/zeroconf/accept` for instant credit, or `POST /v1/orchestration/zeroconf/decline` to wait for 1 confirmation. If you do not respond before `expiresAt`, the offer expires and the engine waits for 1 confirmation. After acceptance (instant credit) or confirmation (1 block), the order proceeds through swapping, bridging, and delivery. ## What fields are in a ZeroConf offer? Three fields drive the decision: * `expiresAt`: the deadline. Respond before it or the offer expires and the order waits for 1 confirmation. * `instantSats`: what the order is credited on acceptance. * `feeSats`: the difference between the deposited amount and `instantSats`, from Spark's static-deposit quote. This is not the Flashnet orchestration platform fee; platform pricing stays in the normal quote and order fields such as `feeBps` and `feeAmount`. Read `feeSats` from each offer; do not hard-code a fixed percentage or sat amount. ZeroConf is single-leg: accepting the offer credits the full net `instantSats`. Confirmation does not release an additional amount. The full object schema, including `status`, `depositSats`, and resolution timestamps, is in [ZeroConf offer fields](/products/orchestration/api/approval-flows#zeroconf-offer-fields). ## How do I accept or decline an offer? Accept (instant credit): ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/orchestration/zeroconf/accept" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: zeroconf:accept:YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"orderId": "ord_..."}' ``` Decline (wait for 1 confirmation): ```bash theme={null} curl -sS -X POST "https://orchestration.flashnet.xyz/v1/orchestration/zeroconf/decline" \ -H "Authorization: Bearer fn_..." \ -H "X-Idempotency-Key: zeroconf:decline:YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"orderId": "ord_...", "reason": "Optional reason"}' ``` ## When does ZeroConf apply? * ZeroConf applies to eligible `sourceChain=bitcoin` exact-in orders after the Bitcoin transaction is detected. * Exact-out (`amountMode=exact_out`) uses confirmation-based processing and does not use ZeroConf. * Quote responses do not predict whether an offer will be created. The decision depends on the detected UTXO and Spark's fulfillment plan. * If Spark does not return a 0-conf plan, the engine falls back to the confirmed path without a partner action. ## How does ZeroConf work with liquidation addresses? Liquidation addresses use Bitcoin L1 deposits and are ZeroConf-eligible. When Spark returns a 0-conf plan, each deposit generates a ZeroConf offer requiring partner resolution via webhook and API call. If no offer is created, the deposit continues on the confirmed path. Some deployments require ZeroConf to be configured for liquidation address provisioning. If address creation returns a configuration error, contact your Flashnet operator. See [reusable addresses](/products/orchestration/reusable-addresses#what-are-liquidation-addresses) for setup. ## Next steps * [Order Lifecycle](/products/orchestration/order-lifecycle) * [API: Approval Flows](/products/orchestration/api/approval-flows) * [Reusable Addresses](/products/orchestration/reusable-addresses) # Rewards Delegation Source: https://docs.flashnet.xyz/usdb/delegation Use endorsements to direct a share of USDB daily rewards to other pubkeys Rewards delegation is implemented as **endorsements** in the Rewards API. An endorser signs a request that assigns part of future daily reward payouts to one or more recipient pubkeys. ## High-Level YAML Example **Create delegation (upsert):** ```yaml theme={null} kind: rewards_endorsement version: 1 action: upsert endorser: "02abc123...compressed_pubkey_hex" to: "03def456...compressed_pubkey_hex" ratioBps: 2500 timestampNonce: "01958d67-2d1a-7a8a-bf8a-5b0a947f8f11" ``` **Revoke delegation (delete):** ```yaml theme={null} kind: rewards_endorsement version: 1 action: delete endorser: "02abc123...compressed_pubkey_hex" to: "03def456...compressed_pubkey_hex" timestampNonce: "01958d67-41c1-7dd4-8aa3-ef3575a84ab5" ``` ## What Endorsements Do * Split only the endorser's daily reward payout. * Allocate payout shares in basis points (`ratioBps`). * Keep any unallocated share with the endorser. * Stay active until updated or deleted. * Credit recipients in the same daily payout cycle. If a recipient also earns their own rewards, payout history combines both values: * `rewardsPayoutSats` for their own rewards. * `endorsementsPayoutSats` for delegated rewards received. * `payoutSats` as the total. ## Example If an endorser configures: * Recipient A: `3000` bps (30%) * Recipient B: `2000` bps (20%) And the endorser's payout for the day is `10000` sats: * Recipient A receives `3000` sats as endorsements payout. * Recipient B receives `2000` sats as endorsements payout. * Endorser keeps `5000` sats as rewards payout. ## Security Model Every create/update/delete request requires: * A `timestampNonce` in UUIDv7 format. * A secp256k1 signature by the endorser pubkey. * The signature is over the SHA-256 hash of a canonical YAML payload. Additional protections and constraints: * Nonce timestamp must be within 5 minutes. * Nonces are single-use (replay protection). * You cannot endorse to yourself. * Sum of all active ratios for one endorser cannot exceed `10000` bps. ## Endpoints * `GET /v1/rewards/:pubkey/endorsements` * `POST /v1/rewards/:pubkey/endorsements` * `DELETE /v1/rewards/:pubkey/endorsements/:toPubkey` For full request/response examples, see [Rewards API](/rewards/integrators/api#endorsements). # FAQ Source: https://docs.flashnet.xyz/usdb/faq Common questions about USDB backing, reserves, rewards, and Brale USDB is fully backed 1:1 by US Treasury bills and cash equivalents, held by regulated custodians. It's issued by [Brale](https://brale.xyz), a licensed stablecoin issuer. Brale maintains reserves in: * Short-duration US Treasury Bills (less than 3 months maturity) * Cash at regulated U.S. financial institutions Every USDB is redeemable for \$1 USD worth of these assets. Brale is a regulated stablecoin-as-a-service provider based in the United States. **Licensing:** Registered Money Services Business (MSB) with FinCEN (#31000257808337), holds Money Transmitter licenses in 44 U.S. states (NMLS ID #2376957), and is subject to quarterly state reporting and federal compliance requirements. You can verify Brale's licenses yourself at [nmlsconsumeraccess.org](https://nmlsconsumeraccess.org). Brale undergoes multiple layers of independent verification: * **Monthly audits**: Third-party reserve attestations by Abdo accounting firm * **Daily reporting**: Public self-attestations of reserves published daily * **Annual audit**: Comprehensive financial review by a licensed CPA firm * **Daily reconciliation**: Automated matching of on-chain token supply vs. fiat balances Reserve attestations are publicly available on [brale.xyz](https://brale.xyz). Flashnet funds rewards from protocol fees and other revenue. As volume grows, so does the revenue base that sustains rewards. This system is baked into Flashnet's long-term strategy, and is not a promotional campaign. Rewards are paid daily in BTC and scale with your protocol usage. **No.** You can buy, sell, transfer, or redeem your USDB at any time. Rewards are calculated based on a rolling average and trading volume, but there's no minimum holding period or withdrawal restrictions. See the [Redeem USDB](/usdb/redeem) page for complete details on all exit paths, including what's available now and what's coming soon. 10 USDB. Balances below 10 USDB don't earn rewards. Daily. Rewards are calculated at midnight UTC and distributed to eligible wallets. USDB maintains its \$1 peg through full 1:1 backing by US Treasury bills and cash equivalents. The value is secured by real assets held by regulated custodians. Yes. Create an account at [Brale](https://brale.xyz), complete their verification process, and redeem USDB for USD (bank transfer) or USDC at a 1:1 ratio. See the [Redeem USDB](/usdb/redeem) page for step-by-step instructions. Brale maintains multiple layers of protection: **Legal structure:** Reserves are held in segregated accounts at regulated U.S. financial institutions. The structure is bankruptcy-remote, with reserves legally separate from Brale's operational funds. Reserves are used exclusively to back outstanding USDB. **Security:** * SOC 2 Type II certified (highest standards for security, availability, and confidentiality) * Smart contracts independently audited by Certik * Multi-party computation (MPC) for private key protection Yes. USDB is a Spark-native token that can be held in any Spark wallet, transferred to any Spark address, and used in any Spark-compatible application. Flashnet is just one place to trade and earn rewards on USDB. Spark is a Bitcoin Layer 2 network that enables instant, low-cost transfers of BTC and tokens like USDB. It uses a FROST-based protocol to achieve sub-second settlement while maintaining self-custody. Learn more at [spark.money](https://spark.money) or read the [Spark documentation](https://docs.spark.money). # Get USDB Source: https://docs.flashnet.xyz/usdb/get-usdb Bridge USDC or swap BTC to acquire USDB on Spark There are two ways to get USDB on Spark: Convert USDC from Solana, Ethereum, Base, Arbitrum, Optimism, or Polygon to USDB on Spark Swap your BTC for USDB instantly on any supported venue More integrations are planned, including native on- and off-ramps. [Contact us](mailto:partnerships@flashnet.xyz) if you want to integrate USDB. *** ## Bridge from USDC Bridge USDC from another supported chain to get USDB. Navigate to [usdb.flashnet.xyz/bridge](https://usdb.flashnet.xyz/bridge). Choose your preferred network as your source chain. Connect the wallet holding your USDC (Phantom, Coinbase Wallet, etc.). Enter the amount of USDC you want to bridge. Paste your Spark wallet address (starts with `spark1...`). Approve the transaction in your wallet. USDB arrives in \~1-2 minutes. ### Supported Source Chains Bridge fees vary by route and network conditions. The bridge UI shows exact fees before confirmation. *** ## Swap BTC for USDB Swap BTC for USDB on any of these venues. Swaps settle instantly.
Luminex Luminex
Utxo.Fun Utxo.Fun
SatsTerminal SatsTerminal
Xverse Xverse
Pick any of the trading venues above. Connect or create a Spark wallet. Select the swap direction and enter your amount. Check the price, fees, and slippage, then confirm. Sub-second finality Low barrier to entry Deep liquidity pools Configurable limits ### Fees Trading volume counts toward your [reward tier](/usdb/rewards). Higher volume = higher rewards. *** ## What You Need Before getting USDB, make sure you have: You need a Spark wallet to receive USDB. Use any Spark-compatible wallet like [Xverse](https://xverse.io), [Guap](https://useguap.com/), [Blitz Wallet](https://blitz-wallet.com/), [LayerZ](https://layerzwallet.com/), or [BitBit](https://bitbit.bot/). Developers can also receive USDB directly via the Spark CLI or the [native Spark SDK](https://docs.spark.money/wallets/overview), without using a consumer wallet. Either USDC on a supported chain (for bridging) or BTC on Spark (for swapping). Your 24-hour trading volume determines your [reward tier](/usdb/rewards). A daily \$500 volume moves you to the 4% tier from the 3.5% base. *** ## After You Get USDB Once you have USDB in your Spark wallet, you can: * **Earn rewards**: Hold USDB to earn BTC rewards (3.5 - 6%) * **Earn Flashpoints**: Accumulate points that boost your rewards * **Trade**: Swap back to BTC anytime * **Transfer**: Send USDB to any Spark address, free and instant * **Redeem**: [Exit to BTC, USD, or USDC](/usdb/redeem) whenever you want # Integrate USDB Source: https://docs.flashnet.xyz/usdb/integrate Add USDB support to wallets and apps with revenue sharing Give your users BTC rewards. Keep a cut for yourself. ## Revenue Potential ## Integration Effort ## Integration Models Display USDB balances, rewards, and Flashpoints in your wallet UI Let users swap BTC ↔ USDB with your app taking a fee Offer USDC → USDB bridging with your branding Full-featured USDB experience under your brand ## Who Should Integrate ## What You Get REST API for balances, rewards, Flashpoints, and leaderboards. Full Swagger documentation. Track your fees earned and performance metrics. Direct access to the Flashnet team for integration questions. Featured placement in Flashnet materials for launch partners. ## Contact [partnerships@flashnet.xyz](mailto:partnerships@flashnet.xyz) # What is USDB? Source: https://docs.flashnet.xyz/usdb/overview Dollar-backed stablecoin on Spark with 3.5-6% BTC rewards paid daily USDB USDB is a dollar-backed stablecoin on [Spark](https://docs.spark.money). Hold USDB and earn 3.5 - 6% rewards paid in BTC daily. ## Why USDB Hold 10+ USDB and earn rewards paid in BTC daily. No exposure to BTC volatility Sub-second transfers on Spark Spark-to-Spark transfers are free ## How USDB is Backed USDB is issued by [Brale](https://brale.xyz), a regulated US-based stablecoin issuer. Every USDB is fully backed 1:1 by: USDB follows the same fully-reserved backing model as USDC and USDT. Users who onboard through Brale can redeem USDB for USD at a 1:1 ratio at any time. ## USDB vs Other Stablecoins ## Token Details **Token Address:** [`btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87`](https://sparkscan.io/token/btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87?network=mainnet) # Redeem USDB Source: https://docs.flashnet.xyz/usdb/redeem Exit paths for converting USDB back to BTC or USDC USDB is redeemable at any time. There is no lock-up period. This page documents all available exit paths and their current status. ## Available Now ### Swap to BTC Convert USDB to BTC instantly on any supported trading venue.
Luminex Luminex
Utxo.Fun Utxo.Fun
SatsTerminal SatsTerminal
Xverse Xverse
**How it works:** 1. Go to any venue above 2. Connect your Spark wallet 3. Swap USDB for BTC 4. From there, you can send BTC to Lightning, on-chain Bitcoin, or any other destination This is the fastest exit path. Swaps settle in under a second. ### Redeem through Brale [Brale](https://brale.xyz) is the regulated issuer behind USDB. Users with a Brale account can redeem USDB directly for USD or USDC at a 1:1 ratio. Sign up at [brale.xyz](https://brale.xyz). Brale requires identity verification (KYC) to comply with US money transmission laws. Connect your US bank account or set up USDC withdrawal to an external wallet. Send USDB from your Spark wallet to your Brale deposit address. Redeem USDB for USD (bank transfer) or USDC (to your linked wallet). Brale redemption is available to all users who complete their verification process. This is the only way to convert USDB directly to fiat USD. *** ## Coming Soon ### Bridge to USDC **Not available yet.** Direct USDB to USDC bridging is being integrated by partners and will be available shortly after launch. Once available, you will be able to bridge USDB directly to USDC on Solana, Ethereum, Base, Arbitrum, Optimism, and Polygon through [SatsTerminal](https://spark.satsterminal.com/). We will update this page when the bridge is live. *** ## Exit Path Comparison | Method | Speed | Fees | Requirements | Status | | ---------------- | ----------- | -------- | ------------------- | ----------- | | Swap to BTC | Instant | \~0.3% | Spark wallet | **Live** | | Brale redemption | Depends | None | Brale account (KYC) | **Live** | | Bridge to USDC | \~2 minutes | Variable | Destination wallet | Coming soon | *** ## Large Positions For positions over \$100,000, we recommend: 1. **Brale direct redemption** for the lowest fees and most straightforward process 2. **Multiple smaller swaps** to BTC if you need liquidity faster than Brale's settlement time 3. **Contact us** at [partnerships@flashnet.xyz](mailto:partnerships@flashnet.xyz) for OTC arrangements *** ## Questions Cross-chain bridging requires maintaining liquidity reserves on multiple chains. Our partners are finalizing this infrastructure. We expect it to be available within the first week after launch. Yes. BTC/USDB liquidity pools are live on multiple venues. You can swap to BTC at any time. No minimum for swapping to BTC. Brale may have minimum redemption amounts based on their policies. For large trades, consider splitting into smaller amounts or using Brale redemption. You can also contact us for OTC arrangements. # Rewards Source: https://docs.flashnet.xyz/usdb/rewards BTC reward tiers and daily payout mechanics for USDB holders Hold USDB and earn BTC rewards paid daily to your wallet. ## How Rewards Work Every USDB holder earns rewards in BTC. Your reward rate depends on your 24-hour trading volume. 3.5% - 6% in BTC Higher trading volume = higher reward rate BTC deposited to your wallet at 01:00 UTC ## Reward Tiers Your 24-hour trading volume determines your reward rate: Volume is measured in 24-hour rolling windows. Your rate updates continuously as you trade. ## Rewards Calculator Estimate your daily and monthly BTC rewards: ## Requirements * **Minimum balance**: 10 USDB to earn rewards * **Maximum balance**: 500,000 USDB (rewards capped at this amount) * **Payout timing**: Daily at 01:00 UTC * **Payout currency**: BTC (deposited to your Spark wallet) ## How Rewards Are Calculated Your daily reward is based on your **Time-Weighted Average Balance (TWAB)**, not a snapshot. For example, if you hold 1,000 USDB for 12 hours then 2,000 USDB for 12 hours, your TWAB is 1,500 USDB. This prevents gaming by depositing right before the daily cutoff. **Daily reward formula:** ``` daily_reward = (TWAB * reward_rate) / 365 ``` The reward is converted to BTC at the current exchange rate and deposited to your wallet. ## Quick Start [Bridge USDC or swap BTC](/usdb/get-usdb) for USDB. Your USDB automatically earns rewards. No staking required. Trade to reach higher reward tiers and earn more. BTC rewards deposited to your wallet daily. Flashnet reserves the right to remove users suspected of sybil attacks or other abuse from the rewards program. # Transparency Source: https://docs.flashnet.xyz/usdb/transparency Where USDB rewards come from and why they're sustainable Where does the yield come from? WHERE Flashnet pays BTC rewards from its own revenue. No rehypothecation, no lending, no fractional reserves. Flashnet doesn't even issue USDB. Brale does. Brale is a US-regulated stablecoin issuer. See [Who is Brale?](/usdb/faq#who-is-brale) for details. ## Two Separate Companies **Brale** issues USDB and maintains the reserves. You hold their stablecoin. **Flashnet** operates the protocol and shares revenue with USDB holders. You use their protocol. Independent companies, working together. ## Where Rewards Come From Flashnet generates revenue from protocol activity and converts a portion to BTC for daily distribution to USDB holders. Your reward rate (3.5 - 6%) scales with protocol usage. More usage, higher rate. ## Principal Safety Your USDB is backed 1:1 by T-bills and cash equivalents held at regulated custodians. Flashnet never touches those reserves. If Flashnet disappeared tomorrow, your USDB would still be redeemable through Brale. ## Reward Sustainability Rewards scale with protocol activity. High usage means more revenue and higher rewards. Low usage means lower rewards. The system balances itself. ## Growth Strategy USDB rewards are a growth investment: 1. USDB holders bring liquidity 2. More liquidity improves execution 3. Better execution attracts users 4. More users generate more revenue 5. More revenue funds more rewards Rewards cost Flashnet money today to generate more revenue tomorrow. Uber subsidized rides to build network density. Banks offer sign-up bonuses to acquire customers. Flashnet pays BTC rewards to grow protocol usage. The difference is that these rewards are built into the protocol indefinitely. ## Brale's Role Brale runs a standard stablecoin business: 1. You give Brale \$1 2. Brale gives you 1 USDB 3. Brale holds your \$1 in T-bills at regulated custodians 4. When you redeem, Brale returns \$1 This is how USDC and USDT work. Flashnet's rewards are separate from this backing mechanism. Check out the full FAQ