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

Send USDC/USDT

The SDK can send USDC or USDT from a Spark wallet to a recipient on any of several supported chains: Ethereum-family chains (Arbitrum, Base, and similar EVM networks), Solana, and Tron. The source on the Spark side is either BTC sats or USDB. The SDK orchestrates two legs — a Spark-side transfer to a provider-controlled deposit and the provider-driven delivery of the destination asset — and reconciles both onto a single PaymentPaymentPaymentPaymentPaymentPaymentPaymentPaymentPayment row.

The send flow itself lives in the Sending payments page. This page covers how it works under the hood: the providers, the lifecycle, retry semantics, 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.

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.

  • EVM0x + 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.

  • EVMEIP-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

The SDK ships with two cross-chain providers. get_cross_chain_routesget_cross_chain_routesgetCrossChainRoutesgetCrossChainRoutesgetCrossChainRoutesgetCrossChainRoutesgetCrossChainRoutesGetCrossChainRoutesGetCrossChainRoutes returns the union of routes offered by each, tagged with CrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.providerCrossChainRoutePair.ProviderCrossChainRoutePair.Provider.

ProviderSource assetsDestinationsMechanism
Orchestra (Flashnet)BTC sats + USDBUSDC / USDT on Ethereum chains (Arbitrum, Base), Solana, TronSpark transfer to a deposit address, then provider bridges to the destination chain
BoltzBTC sats onlyUSDC / USDT on Ethereum chains (Arbitrum, Base), Solana, TronLightning reverse swap: SDK pays a hold invoice, provider claims the on-chain leg

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

Slippage

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

Resolution at prepare time:

  1. The per-request max_slippage_bpsmax_slippage_bpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsmaxSlippageBpsMaxSlippageBpsMaxSlippageBps on PaymentRequest::CrossChainPaymentRequest.CROSS_CHAINPaymentRequest.crossChainPaymentRequest.CrossChainPaymentRequest.CrossChainPaymentRequest.CrossChainPaymentRequest.CrossChainPaymentRequestCrossChainPaymentRequest.CrossChain wins 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.

Quote expiry

Each cross-chain 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.

Status lifecycle

The Spark/USDB token transfer and the cross-chain delivery have distinct status fields. They are tracked separately on the persisted PaymentPaymentPaymentPaymentPaymentPaymentPaymentPaymentPayment row so each can settle independently.

FieldReflects
statusstatusstatusstatusstatusstatusstatusStatusStatusThe Spark or USDB token transfer (sender-side settlement)
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

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.
  • ConversionStatus::RefundNeededConversionStatus.REFUND_NEEDEDConversionStatus.refundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatusRefundNeededConversionStatus.RefundNeeded — provider rejected the submit or order failed before delivery; the local Spark transfer is settled and the deposit is sitting at the provider awaiting refund.
  • 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 ConversionStatus::RefundNeededConversionStatus.REFUND_NEEDEDConversionStatus.refundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatusRefundNeededConversionStatus.RefundNeeded and ConversionStatus::PendingConversionStatus.PENDINGConversionStatus.pendingConversionStatus.PendingConversionStatus.PendingConversionStatus.PendingConversionStatus.PendingConversionStatusPendingConversionStatus.Pending rows onto their terminal state by polling the provider.

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 BTC source, or Boltz funded directly from the sats balance), 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 through a sats-only provider (e.g. Boltz). 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.

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