Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SDK Documentation Spark
Request API Key

Send and receive USDC/USDT

The SDK can send and receive USDC and USDT between a Spark wallet and several supported external chains: Ethereum-family chains (Arbitrum, Base, and similar EVM networks), Solana, and Tron. The Spark side is either BTC sats or USDB, the 6-decimal USD-pegged token on Spark. Each payment runs as two legs (a Spark-side transfer and the provider-driven external delivery) reconciled onto a single PaymentPaymentPaymentPaymentPaymentPaymentPaymentPaymentPayment row.

The send flow is documented on the Sending payments page; the receive flow on the Receiving payments page. This page covers shared concepts: providers, lifecycle, retry safety, and limitations.

Supported address formats

parseparseparseparseparseparseparseParseParse recognizes cross-chain destinations in the following forms, returning InputType::CrossChainAddressInputType.CROSS_CHAIN_ADDRESSInputType.crossChainAddressInputType.CrossChainAddressInputType.CrossChainAddressInputType.CrossChainAddressInputType.CrossChainAddressInputTypeCrossChainAddressInputType.CrossChainAddress with the parsed CrossChainAddressDetailsCrossChainAddressDetailsCrossChainAddressDetailsCrossChainAddressDetailsCrossChainAddressDetailsCrossChainAddressDetailsCrossChainAddressDetailsCrossChainAddressDetailsCrossChainAddressDetails — address family, bare address, and optional token contract address, chain id, and amount.

Parsing is for send only. Receives don't take a counterparty address: the receiver chooses a route and the SDK returns a provider-controlled deposit address for the sender to pay.

Bare addresses

The SDK detects three address families from format alone. A bare address parses with no contract_address, chain_id, or amount — the caller selects the destination chain and asset via get_cross_chain_routesget_cross_chain_routesgetCrossChainRoutesgetCrossChainRoutesgetCrossChainRoutesgetCrossChainRoutesgetCrossChainRoutesGetCrossChainRoutesGetCrossChainRoutes.

  • EVM — 0x + 40 hex characters (lowercase or checksummed):
    0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
    
  • Solana — base58 encoding of a 32-byte public key:
    EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
    
  • Tron — base58check with a T prefix (34 characters total):
    TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
    

Canonical URIs

URIs let the recipient encode chain, token contract, and amount alongside the address. Unknown query parameters are ignored.

  • EVM — EIP-681. Native send or ERC-20 transfer; the optional @<chain_id> suffix is the EIP-681 chain identifier (e.g. 8453 for Base):
    ethereum:<addr>[@<chain_id>]?value=<wei>
    ethereum:<contract>[@<chain_id>]/transfer?address=<to>&uint256=<amount>
    
  • Solana — Solana Pay-style. spl-token= carries the SPL mint when the destination is an SPL token rather than native SOL:
    solana:<addr>?amount=<amount>&spl-token=<mint>
    
  • Tron — TRC-20 destinations carry the contract on token=:
    tron:<addr>?amount=<amount>&token=<contract>
    

URIs whose recipient address doesn't match the scheme's address family (e.g. a solana: URI carrying an EVM address) are not recognized as cross-chain. Unknown schemes are not recognized as cross-chain either — they may still be classified by another input type if the format matches.

Providers

get_cross_chain_routesget_cross_chain_routesgetCrossChainRoutesgetCrossChainRoutesgetCrossChainRoutesgetCrossChainRoutesgetCrossChainRoutesGetCrossChainRoutesGetCrossChainRoutes returns the routes offered by each provider, tagged with CrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.ProviderCrossChainRoutePair.Provider.

ProviderDirectionSpark sideExternal sideMechanism
Orchestra (Flashnet)Send + ReceiveBTC sats + USDBUSDC / USDT on Ethereum chains (Arbitrum, Base), Solana, TronSpark transfer to a deposit address, then provider bridges to the destination chain

The provider tag on each CrossChainRoutePairCrossChainRoutePairCrossChainRoutePairCrossChainRoutePairCrossChainRoutePairCrossChainRoutePairCrossChainRoutePairCrossChainRoutePairCrossChainRoutePair is the source of truth. When the same destination is offered by multiple providers, every route is returned; the caller picks one based on supported source/destination assets, fees, or other preferences.

Amount limits

Each entry in CrossChainRoutePair.accepted_assetsCrossChainRoutePair.accepted_assetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.AcceptedAssetsCrossChainRoutePair.AcceptedAssets carries an optional limitslimitslimitslimitslimitslimitslimitsLimitsLimits block: the amount bounds the provider publishes for moving that route with that Spark-side asset.

FieldMeaning
min_amountmin_amountminAmountminAmountminAmountminAmountminAmountMinAmountMinAmount / max_amountmax_amountmaxAmountmaxAmountmaxAmountmaxAmountmaxAmountMaxAmountMaxAmountBounds in the base units of the asset paid in: the Spark-side asset on a send, the external asset on a receive
min_usd_centsmin_usd_centsminUsdCentsminUsdCentsminUsdCentsminUsdCentsminUsdCentsMinUsdCentsMinUsdCents / max_usd_centsmax_usd_centsmaxUsdCentsmaxUsdCentsmaxUsdCentsmaxUsdCentsmaxUsdCentsMaxUsdCentsMaxUsdCentsBounds on the order's value, in USD cents

Bounds are per asset rather than per route, because the same external endpoint can carry a dust floor when moved as sats and none when moved as a token. Either denomination can be absent, and a provider may publish none at all, so treat a missing bound as "no published limit" rather than as zero.

Validate the amount against the bounds before preparing the payment. A route can enforce a tighter bound than it publishes, so an amount inside the published band can still be rejected at prepare.

A rejected amount surfaces as SdkError::CrossChainAmountOutOfRangeSdkError.CROSS_CHAIN_AMOUNT_OUT_OF_RANGESdkError.crossChainAmountOutOfRangeSdkError.CrossChainAmountOutOfRangeSdkError.CrossChainAmountOutOfRangeSdkError.CrossChainAmountOutOfRangeSdkError.CrossChainAmountOutOfRangeSdkErrorCrossChainAmountOutOfRangeSdkError.CrossChainAmountOutOfRange, carrying too_smalltoo_smalltooSmalltooSmalltooSmalltooSmalltooSmallTooSmallTooSmall for the direction and the published bound in whichever denominations the provider publishes.

A route the provider won't serve surfaces as SdkError::CrossChainRouteUnavailableSdkError.CROSS_CHAIN_ROUTE_UNAVAILABLESdkError.crossChainRouteUnavailableSdkError.CrossChainRouteUnavailableSdkError.CrossChainRouteUnavailableSdkError.CrossChainRouteUnavailableSdkError.CrossChainRouteUnavailableSdkErrorCrossChainRouteUnavailableSdkError.CrossChainRouteUnavailable. temporarytemporarytemporarytemporarytemporarytemporarytemporaryTemporaryTemporary indicates that a route is currently unavailable and the same request may succeed later, rather than requiring another route.

Slippage

Cross-chain slippage protects against price movement between quote and delivery. Values are expressed in basis points (1 bps = 0.01%).

Resolution at prepare/receive time:

  1. The per-request max_slippage_bpsmax_slippage_bpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsMaxSlippageBpsMaxSlippageBps on the request if set.
  2. Otherwise, the SDK falls back to default_slippage_bpsdefault_slippage_bpsdefaultSlippageBpsdefaultSlippageBpsdefaultSlippageBpsdefaultSlippageBpsdefaultSlippageBpsDefaultSlippageBpsDefaultSlippageBps on CrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfig from the SDK configuration.
  3. Otherwise, the built-in default of 100 bps (1%) is used.

Values outside 10 to 500 are rejected at both config validation and per-request validation.

Target overpay

On CrossChainFeeMode::FeesExcludedCrossChainFeeMode.FEES_EXCLUDEDCrossChainFeeMode.feesExcludedCrossChainFeeMode.FeesExcludedCrossChainFeeMode.FeesExcludedCrossChainFeeMode.FeesExcludedCrossChainFeeMode.FeesExcludedCrossChainFeeModeFeesExcludedCrossChainFeeMode.FeesExcluded sends and receives the SDK pads the user's target amount by target_overpay_bpstarget_overpay_bpstargetOverpayBpstargetOverpayBpstargetOverpayBpstargetOverpayBpstargetOverpayBpsTargetOverpayBpsTargetOverpayBps so the realized delivery lands at or above target despite provider slippage.

Resolution at prepare/receive time:

  1. The per-request target_overpay_bpstarget_overpay_bpstargetOverpayBpstargetOverpayBpstargetOverpayBpstargetOverpayBpstargetOverpayBpsTargetOverpayBpsTargetOverpayBps on the request if set.
  2. Otherwise, the SDK falls back to default_target_overpay_bpsdefault_target_overpay_bpsdefaultTargetOverpayBpsdefaultTargetOverpayBpsdefaultTargetOverpayBpsdefaultTargetOverpayBpsdefaultTargetOverpayBpsDefaultTargetOverpayBpsDefaultTargetOverpayBps on CrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfig.
  3. Otherwise, the built-in default of 15 bps is used.

Values outside 0 to 500 are rejected at both config validation and per-request validation. CrossChainFeeMode::FeesIncludedCrossChainFeeMode.FEES_INCLUDEDCrossChainFeeMode.feesIncludedCrossChainFeeMode.FeesIncludedCrossChainFeeMode.FeesIncludedCrossChainFeeMode.FeesIncludedCrossChainFeeMode.FeesIncludedCrossChainFeeModeFeesIncludedCrossChainFeeMode.FeesIncluded requests ignore this field.

Status lifecycle

The Spark-side transfer and the external cross-chain leg have distinct status fields. They are tracked separately on the persisted PaymentPaymentPaymentPaymentPaymentPaymentPaymentPaymentPayment row so each can settle independently.

FieldReflects
statusstatusstatusstatusstatusstatusstatusStatusStatusThe Spark-side transfer (outbound on send; inbound claim on receive)
conversion_info.statusconversion_info.statusconversionInfo.statusconversionInfo.statusconversionInfo.statusconversionInfo.statusconversionInfo.statusConversionInfo.StatusConversionInfo.StatusThe provider-driven cross-chain leg
conversion_info.delivered_amountconversion_info.delivered_amountconversionInfo.deliveredAmountconversionInfo.deliveredAmountconversionInfo.deliveredAmountconversionInfo.deliveredAmountconversionInfo.deliveredAmountConversionInfo.DeliveredAmountConversionInfo.DeliveredAmountFinal amount delivered to the recipient, set when terminal
conversion_info.external_tx_hashconversion_info.external_tx_hashconversionInfo.externalTxHashconversionInfo.externalTxHashconversionInfo.externalTxHashconversionInfo.externalTxHashconversionInfo.externalTxHashConversionInfo.ExternalTxHashConversionInfo.ExternalTxHashTransaction on the non-Spark chain: the delivery, or the funding deposit

The cross-chain status walks one of:

  • ConversionStatus::PendingConversionStatus.PENDINGConversionStatus.pendingConversionStatus.PendingConversionStatus.PendingConversionStatus.PendingConversionStatus.PendingConversionStatusPendingConversionStatus.Pending: deposit/transfer submitted, provider working on the cross-chain leg.
  • ConversionStatus::CompletedConversionStatus.COMPLETEDConversionStatus.completedConversionStatus.CompletedConversionStatus.CompletedConversionStatus.CompletedConversionStatus.CompletedConversionStatusCompletedConversionStatus.Completed: provider reports the order terminal-successful; delivered_amountdelivered_amountdeliveredAmountdeliveredAmountdeliveredAmountdeliveredAmountdeliveredAmountDeliveredAmountDeliveredAmount is set, and external_tx_hashexternal_tx_hashexternalTxHashexternalTxHashexternalTxHashexternalTxHashexternalTxHashExternalTxHashExternalTxHash identifies the transaction on the non-Spark chain: the delivery on a send, the funding deposit on a receive. Boltz-provider conversions leave the hash unset.
  • ConversionStatus::RefundNeededConversionStatus.REFUND_NEEDEDConversionStatus.refundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatusRefundNeededConversionStatus.RefundNeeded: the cross-chain leg was rejected after deposit (typically because the realized rate exceeded max_slippage_bpsmax_slippage_bpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsMaxSlippageBpsMaxSlippageBps); the deposit is awaiting refund. (send only)
  • ConversionStatus::RefundedConversionStatus.REFUNDEDConversionStatus.refundedConversionStatus.RefundedConversionStatus.RefundedConversionStatus.RefundedConversionStatus.RefundedConversionStatusRefundedConversionStatus.Refunded: the funds have been refunded back to the wallet.
  • ConversionStatus::FailedConversionStatus.FAILEDConversionStatus.failedConversionStatus.FailedConversionStatus.FailedConversionStatus.FailedConversionStatus.FailedConversionStatusFailedConversionStatus.Failed: terminal failure with no refund pending.

A background monitor runs while the SDK is active and reconciles non-terminal payments by polling the provider. When it changes the conversion info of a payment that was already reported, the SDK emits SdkEvent::PaymentMetadataUpdatedSdkEvent.PAYMENT_METADATA_UPDATEDSdkEvent.paymentMetadataUpdatedSdkEvent.PaymentMetadataUpdatedSdkEvent.PaymentMetadataUpdatedSdkEvent.PaymentMetadataUpdatedSdkEvent.PaymentMetadataUpdatedSdkEventPaymentMetadataUpdatedSdkEvent.PaymentMetadataUpdated with the updated payment. On a receive, the inbound payment carries its conversion_infoconversion_infoconversionInfoconversionInfoconversionInfoconversionInfoconversionInfoConversionInfoConversionInfo from its first event. The delivered amount and the deposit transaction follow in that event once the provider confirms the order.

Send

Quote expiry

Each cross-chain send prepare response carries an expires_atexpires_atexpiresAtexpiresAtexpiresAtexpiresAtexpiresAtExpiresAtExpiresAt quote-expiry timestamp on SendPaymentMethod::CrossChainAddressSendPaymentMethod.CROSS_CHAIN_ADDRESSSendPaymentMethod.crossChainAddressSendPaymentMethod.CrossChainAddressSendPaymentMethod.CrossChainAddressSendPaymentMethod.CrossChainAddressSendPaymentMethod.CrossChainAddressSendPaymentMethodCrossChainAddressSendPaymentMethod.CrossChainAddress. If the quote has expired by the time you call send_paymentsend_paymentsendPaymentsendPaymentsendPaymentsendPaymentsendPaymentSendPaymentSendPayment, you must re-prepare to obtain a fresh quote (with a new expires_atexpires_atexpiresAtexpiresAtexpiresAtexpiresAtexpiresAtExpiresAtExpiresAt) and try again.

Retry safety

Calling send_paymentsend_paymentsendPaymentsendPaymentsendPaymentsendPaymentsendPaymentSendPaymentSendPayment is safe to retry on transient errors only when the send has no token-transfer leg. Whether the source asset displayed on the route is BTC or USDB is not the determinant — what matters is the actual first leg the SDK executes.

Sends with no token leg

When the first leg is a Spark sats transfer (Orchestra with a BTC source), the SDK threads a deterministic transfer id through to the underlying Spark transfer. Retrying with the same PrepareSendPaymentResponsePrepareSendPaymentResponsePrepareSendPaymentResponsePrepareSendPaymentResponsePrepareSendPaymentResponsePrepareSendPaymentResponsePrepareSendPaymentResponsePrepareSendPaymentResponsePrepareSendPaymentResponse produces the same transfer id, and the Spark protocol returns the original transfer instead of firing a new one — no double-deposit.

Two ways to drive idempotency:

  1. Pass a caller-supplied idempotency_keyidempotency_keyidempotencyKeyidempotencyKeyidempotencyKeyidempotencyKeyidempotencyKeyIdempotencyKeyIdempotencyKey on SendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequest. The top-level dispatcher first looks for an existing payment with that id and short-circuits the retry if found; otherwise the key is used as the Spark transfer id.
  2. Omit idempotency_keyidempotency_keyidempotencyKeyidempotencyKeyidempotencyKeyidempotencyKeyidempotencyKeyIdempotencyKeyIdempotencyKey — the SDK derives a deterministic UUIDv5 from the provider's quote/swap id. Re-sending the same prepared shape produces the same id and dedupes at the Spark protocol layer even if the first attempt's persistence step never completed.

Sends with a token leg

When the first leg is a token transfer at the Spark protocol layer, there is no upstream idempotency hook. The dispatcher rejects a caller-supplied idempotency_keyidempotency_keyidempotencyKeyidempotencyKeyidempotencyKeyidempotencyKeyidempotencyKeyIdempotencyKeyIdempotencyKey with SdkError::InvalidInputSdkError.INVALID_INPUTSdkError.invalidInputSdkError.InvalidInputSdkError.InvalidInputSdkError.InvalidInputSdkError.InvalidInputSdkErrorInvalidInputSdkError.InvalidInput, and a retry can fire a second token transfer and overpay.

This arises in two ways for a cross-chain send:

  • Direct token send — USDB source on Orchestra. The first leg is a USDB transfer to the provider deposit address.
  • Token conversion — USDB balance routed over a route whose Spark side takes only sats. The SDK auto-converts USDB → BTC via the stable-balance flow before the provider leg; that conversion is itself a token transfer.

This matches the existing contract for direct token sends.

If you need at-most-once semantics in either of these cases, debounce retries at the application layer until the SDK either returns a payment or a terminal error.

Receive

The receiver picks a route, sets an amount in the source asset's base units (route.decimals; USD-stable parity, so 1_000_000 is one USD on any 6-decimal route), and gets back a provider-controlled deposit address to share with the sender. See Receive payment for the full request shape and fee-mode semantics.

Destination selection

The destinationdestinationdestinationdestinationdestinationdestinationdestinationDestinationDestination on ReceivePaymentMethod::CrossChainReceivePaymentMethod.CROSS_CHAINReceivePaymentMethod.crossChainReceivePaymentMethod.CrossChainReceivePaymentMethod.CrossChainReceivePaymentMethod.CrossChainReceivePaymentMethod.CrossChainReceivePaymentMethodCrossChainReceivePaymentMethod.CrossChain is a SparkAssetSparkAssetSparkAssetSparkAssetSparkAssetSparkAssetSparkAssetSparkAssetSparkAsset indicating which Spark-side asset the receiver wants to land. When unset, the SDK auto-picks: the wallet's active stable-balance token if the route supports landing it, otherwise BTC. An explicit choice must appear in the selected CrossChainRoutePair.accepted_assetsCrossChainRoutePair.accepted_assetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.AcceptedAssetsCrossChainRoutePair.AcceptedAssets.

To force a specific destination, build a SparkAssetSparkAssetSparkAssetSparkAssetSparkAssetSparkAssetSparkAssetSparkAssetSparkAsset variant: SparkAsset::BitcoinSparkAsset.BITCOINSparkAsset.bitcoinSparkAsset.BitcoinSparkAsset.BitcoinSparkAsset.BitcoinSparkAsset.BitcoinSparkAssetBitcoinSparkAsset.Bitcoin for BTC, or SparkAsset::TokenSparkAsset.TOKENSparkAsset.tokenSparkAsset.TokenSparkAsset.TokenSparkAsset.TokenSparkAsset.TokenSparkAssetTokenSparkAsset.Token carrying the Spark token_identifiertoken_identifiertokenIdentifiertokenIdentifiertokenIdentifiertokenIdentifiertokenIdentifierTokenIdentifierTokenIdentifier (the btkn1... bech32m id from CrossChainRoutePair.accepted_assetsCrossChainRoutePair.accepted_assetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.acceptedAssetsCrossChainRoutePair.AcceptedAssetsCrossChainRoutePair.AcceptedAssets) for USDB or any other supported token.

Quote expiry

The receive prepare response carries an expires_atexpires_atexpiresAtexpiresAtexpiresAtexpiresAtexpiresAtExpiresAtExpiresAt timestamp. The SDK does not gate on it: Orchestra reprices late deposits at the live rate. The receive monitor keeps probing for the deposit for 24 hours past expires_atexpires_atexpiresAtexpiresAtexpiresAtexpiresAtexpiresAtExpiresAtExpiresAt before locally closing the row as unfunded; late deposits inside that window still link correctly.

Limitations

  • Mainnet only. Cross-chain providers operate against live external networks; there is no testnet equivalent in the SDK today.
  • Background tasks required. Both providers depend on background monitors to reconcile delivery status. cross_chain_configcross_chain_configcrossChainConfigcrossChainConfigcrossChainConfigcrossChainConfigcrossChainConfigCrossChainConfigCrossChainConfig is incompatible with background_tasks_enabledbackground_tasks_enabledbackgroundTasksEnabledbackgroundTasksEnabledbackgroundTasksEnabledbackgroundTasksEnabledbackgroundTasksEnabledBackgroundTasksEnabledBackgroundTasksEnabled disabled.
  • Token-leg sends have no idempotency guarantee. Applies to a direct USDB send and to any USDB-funded send that auto-converts through bitcoin. See Retry safety above.

Supported chains

Chain familyAssetChains
EVMUSDCArbitrum One, Avalanche, Base, BSC, Codex, Ethereum, HyperEVM, Ink, Linea, Monad, Optimism, Plume, Polygon PoS, Sei, Sonic, Tempo, Unichain, World Chain, XDC
EVMUSDTArbitrum One, Berachain, BSC, Conflux eSpace, Corn, Ethereum, Flare, Hedera, HyperEVM, Ink, Mantle, MegaETH, Monad, Morph, Optimism, Plasma, Polygon PoS, Rootstock, Sei, Stable, Tempo, Unichain, XLayer
SolanaUSDC
SolanaUSDT
TronUSDT