Skip to Content
XebraDocs
Guides

Send from Stellar

Move USDC from a Stellar account to another chain with one wallet signature.

A transfer out of Stellar is one transaction. The user's wallet signs it, it calls Xebra's wrapper contract on Stellar, the wrapper burns the USDC through Circle's CCTP, and Circle's Forwarding Service mints it on the destination. Nobody has to come back and finish it.

Stellar transfers use Standard speed only. Which chains Stellar sends to right now comes from the wrapper contract itself: see Routes or call GET /v1/routes.

What you need

  • The sender's Stellar account (G...). It must exist, hold enough USDC, and hold a little XLM for the network fee.
  • The recipient's address on the destination: an 0x address on an EVM chain, or a wallet address on Solana.
  • A Stellar wallet that can sign a transaction envelope. See Wallet integration.

The flow

Quote

Ask what the transfer costs and what arrives. Show recipientGets and fees.total to the user.

For Solana, pass the recipient too. A wallet that has no USDC account yet costs more to deliver to, and the quote includes that only when it knows the recipient.

Build

Call createTransfer with the sender and recipient. Pass the total you showed as maxTotalFee: if fees have risen since, the API refuses with fee_above_limit instead of building a more expensive transfer.

Before it builds, the API checks what can be checked without a signature: the sender's account and USDC balance (insufficient_balance), the recipient's address (invalid_recipient), and a simulation of the transaction against the contract (transaction_rejected).

Sign and submit

The answer has exactly one step, id: "burn", with tx.kind: "stellar". tx.xdr is a prepared Soroban transaction (simulated, with its authorization and resources attached) whose source is the sender. Have the wallet sign it with tx.networkPassphrase, then submit the signed envelope to a Soroban RPC node.

Track

The burn's identifier is the Stellar transaction hash: 64 hex characters, without 0x. Pass it to getTransfer or waitForDelivery with chain: "stellar". See Track a transfer.

In code

import {  } from "@xebra/sdk";

// Your wallet: sign the envelope, return the signed XDR.
declare function (: string, : string): <string>;
// Your Soroban RPC: submit, wait for the ledger, return the hash (64 hex characters).
declare function (: string, : string): <string>;

const  = ();

const  = await .({ : "stellar", : "base", : "25" });

const {  } = await .({
  : "stellar",
  : "base",
  : "25",
  : "GABC...", // the Stellar account that signs and pays
  : "0x1234...", // who receives on Base
  : .., // refuse if fees rose since the quote
});

const  = [0];
if (! || .. !== "stellar") throw new ("expected one Stellar step");

const  = await (.., ..);
const  = await (, ..);

const  = await .("stellar", );
.(., .);

Submitting to Soroban RPC

sendTransaction on a Soroban RPC node returns a hash whatever happens, so read its status:

StatusMeaning
PENDINGAccepted. Poll getTransaction with the hash until it is SUCCESS or FAILED.
DUPLICATEAlready submitted. Poll the same hash.
TRY_AGAIN_LATERThe node did not queue it. Nothing moved. Submit again in a few seconds.
ERRORRejected. Nothing moved. errorResult says why (for example txTooLate, txInsufficientBalance).

Expiry and the price

The step's expiresAt is the end of the transaction's time bounds. After it, Stellar rejects the transaction (txTooLate) and nothing moves: build the transfer again.

The fee is pinned when the transfer is built. The transaction carries the most the wrapper may take, and the contract refuses rather than charge more than the quote showed. A quote itself is good until its own expiresAt; after that, quote again.

Delivery is Circle's

Once the burn lands, Circle attests it and its Forwarding Service mints on the destination. If that delivery does not complete, the transfer shows needs_claim or failed, and anyone can finish it. See Claim a stuck transfer and Forwarding.

On this page