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
Tprefix (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.8453for 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.
| Provider | Direction | Spark side | External side | Mechanism |
|---|---|---|---|---|
| Orchestra (Flashnet) | Send + Receive | BTC sats + USDB | USDC / USDT on Ethereum chains (Arbitrum, Base), Solana, Tron | Spark 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.
| Field | Meaning |
|---|---|
min_amountmin_amountminAmountminAmountminAmountminAmountminAmountMinAmountMinAmount / max_amountmax_amountmaxAmountmaxAmountmaxAmountmaxAmountmaxAmountMaxAmountMaxAmount | Bounds 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_centsmaxUsdCentsmaxUsdCentsmaxUsdCentsmaxUsdCentsmaxUsdCentsMaxUsdCentsMaxUsdCents | Bounds 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:
- The per-request
max_slippage_bpsmax_slippage_bpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsMaxSlippageBpsMaxSlippageBpson the request if set. - Otherwise, the SDK falls back to
default_slippage_bpsdefault_slippage_bpsdefaultSlippageBpsdefaultSlippageBpsdefaultSlippageBpsdefaultSlippageBpsdefaultSlippageBpsDefaultSlippageBpsDefaultSlippageBpsonCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigfrom the SDK configuration. - 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:
- The per-request
target_overpay_bpstarget_overpay_bpstargetOverpayBpstargetOverpayBpstargetOverpayBpstargetOverpayBpstargetOverpayBpsTargetOverpayBpsTargetOverpayBpson the request if set. - Otherwise, the SDK falls back to
default_target_overpay_bpsdefault_target_overpay_bpsdefaultTargetOverpayBpsdefaultTargetOverpayBpsdefaultTargetOverpayBpsdefaultTargetOverpayBpsdefaultTargetOverpayBpsDefaultTargetOverpayBpsDefaultTargetOverpayBpsonCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfigCrossChainConfig. - 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.
| Field | Reflects |
|---|---|
statusstatusstatusstatusstatusstatusstatusStatusStatus | The Spark-side transfer (outbound on send; inbound claim on receive) |
conversion_info.statusconversion_info.statusconversionInfo.statusconversionInfo.statusconversionInfo.statusconversionInfo.statusconversionInfo.statusConversionInfo.StatusConversionInfo.Status | The provider-driven cross-chain leg |
conversion_info.delivered_amountconversion_info.delivered_amountconversionInfo.deliveredAmountconversionInfo.deliveredAmountconversionInfo.deliveredAmountconversionInfo.deliveredAmountconversionInfo.deliveredAmountConversionInfo.DeliveredAmountConversionInfo.DeliveredAmount | Final amount delivered to the recipient, set when terminal |
conversion_info.external_tx_hashconversion_info.external_tx_hashconversionInfo.externalTxHashconversionInfo.externalTxHashconversionInfo.externalTxHashconversionInfo.externalTxHashconversionInfo.externalTxHashConversionInfo.ExternalTxHashConversionInfo.ExternalTxHash | Transaction 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_amountdeliveredAmountdeliveredAmountdeliveredAmountdeliveredAmountdeliveredAmountDeliveredAmountDeliveredAmountis set, andexternal_tx_hashexternal_tx_hashexternalTxHashexternalTxHashexternalTxHashexternalTxHashexternalTxHashExternalTxHashExternalTxHashidentifies 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 exceededmax_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:
- Pass a caller-supplied
idempotency_keyidempotency_keyidempotencyKeyidempotencyKeyidempotencyKeyidempotencyKeyidempotencyKeyIdempotencyKeyIdempotencyKeyonSendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequestSendPaymentRequest. 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. - 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_configcrossChainConfigcrossChainConfigcrossChainConfigcrossChainConfigcrossChainConfigCrossChainConfigCrossChainConfigis incompatible withbackground_tasks_enabledbackground_tasks_enabledbackgroundTasksEnabledbackgroundTasksEnabledbackgroundTasksEnabledbackgroundTasksEnabledbackgroundTasksEnabledBackgroundTasksEnabledBackgroundTasksEnableddisabled. - 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 family | Asset | Chains |
|---|---|---|
| EVM | USDC | Arbitrum One, Avalanche, Base, BSC, Codex, Ethereum, HyperEVM, Ink, Linea, Monad, Optimism, Plume, Polygon PoS, Sei, Sonic, Tempo, Unichain, World Chain, XDC |
| EVM | USDT | Arbitrum 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 |
| Solana | USDC | |
| Solana | USDT | |
| Tron | USDT |