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
0xaddress 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:
| Status | Meaning |
|---|---|
PENDING | Accepted. Poll getTransaction with the hash until it is SUCCESS or FAILED. |
DUPLICATE | Already submitted. Poll the same hash. |
TRY_AGAIN_LATER | The node did not queue it. Nothing moved. Submit again in a few seconds. |
ERROR | Rejected. 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.