Skip to content

Cross-chain swaps

Cross-chain swaps move value between Base and Solana at 1:1 par. They cannot settle in one transaction, so they run as an intent: the user locks the input in a source-chain escrow, the solver delivers the output on the destination chain, and only then, after an independent attestor quorum confirms the delivery, can the solver claim the locked input.

The user's funds are protected by the escrow. If the order expires unfilled, the lock is refundable permissionlessly.

The settlement flow

  1. Open. The user locks the input in the source escrow. You build this transaction via the API and the user signs it.
  2. Deliver. The solver sends the output to the recipient on the destination chain, from its own inventory.
  3. Attest. Attestors independently verify the delivery on-chain and sign. A 2-of-3 quorum is required.
  4. Claim. The solver presents the quorum attestation to the source escrow and claims the locked input.
  5. Refund. If the fill deadline passes without a delivery, the lock is refundable.

All three integrator endpoints on this page — both open/build routes and the status poll — require a partner x-api-key header. See Authentication.

Steps 2 through 5 are driven by the solver worker. Your integration does not settle anything: it opens the order and then polls status. The settle and refund endpoints exist for manual operator use and are token-gated precisely so an open caller can't trigger a delivery that races the solver and double-pays.


POST /api/xchain/sol-origin/open/build

Builds the Solana lock transaction for a Solana → Base swap. Returns an unsigned transaction for the user to sign and submit.

curl -X POST .../api/xchain/sol-origin/open/build \
  -H 'content-type: application/json' \
  -H "x-api-key: $SBC_API_KEY" \
  -d '{
    "user": "<solana-pubkey>",
    "inSym": "CFUSD",
    "outSym": "SBC",
    "uiAmount": "25",
    "recipientEvm": "0xRecipientOnBase"
  }'
FieldRequiredDescription
userYesThe Solana address funding the swap.
inSymYesInput token symbol on Solana.
outSymYesOutput token symbol on Base.
uiAmountYesHuman decimal amount. Subject to the cross-chain cap.
recipientEvmYesBase address that receives the output. Validated as a checksummed EVM address.
{
  "txB64": "<base64 unsigned transaction>",
  "orderId": "a3f1...ee",
  "inAmount": 25,
  "outAmount": 25,
  "outToken": "SBC",
  "recipient": "0xRecipientOnBase",
  "fillDeadline": 1750000600,
  "expires": 1750001200,
  "blockhash": "...",
  "lastValidBlockHeight": 123456
}

inAmount and outAmount are equal, because this is a par route. Keep the orderId; it's the only handle you have for tracking the order afterward.

Sign txB64 with the user's wallet and submit it to Solana. The order is open once that transaction confirms. Respect lastValidBlockHeight; past it, the blockhash is stale and you must rebuild.

The API refuses to build a lock it can't fill. If the solver's Base inventory is short and no fallback sourcing path is enabled for the route, the request fails with a 400 rather than letting the user lock funds into a route that would only end in a refund.


POST /api/xchain/evm-origin/open/build

The mirror direction: Base → Solana. Identical shape, with recipientSol in place of recipientEvm:

{ "user": "0xYourAddress", "inSym": "SBC", "outSym": "CFUSD", "uiAmount": "25", "recipientSol": "<solana-pubkey>" }

The user locks into the Base SourceEscrow and the solver delivers an SPL transfer on Solana.


GET /api/xchain/status/:orderId

The endpoint you poll. It reads live source-chain state and reconciles it against the solver's settlement records.

curl -H "x-api-key: $SBC_API_KEY" \
  ".../api/xchain/status/a3f1...ee?rail=sol-origin"
QueryRequiredDescription
railNosol-origin (default) or evm-origin. Must match the direction you opened.
{
  "orderId": "0xa3f1...ee",
  "rail": "sol-origin",
  "status": "claimed",
  "delivered": "25",
  "deliverRef": "<destination tx signature>",
  "claimRef": "<source claim tx>",
  "recipient": "0xRecipientOnBase",
  "reason": null
}
statusMeaning
pendingLocked on the source chain, awaiting delivery.
claimedDelivered and claimed. The swap succeeded; deliverRef is the destination transaction.
refundedExpired unfilled; the input returned to the user. Terminal.
attentionFlagged for operator review. reason carries the detail.
unknownNo matching order found, usually a bad orderId, or the lock transaction hasn't confirmed yet.

deliverRef is the destination-chain delivery and claimRef the source-chain claim. Show deliverRef to your user; that's the transaction where they received funds.

orderId may be passed with or without the 0x prefix; it must be 32 bytes (64 hex characters). The response always returns it prefixed.


Refunds

If a delivery never lands, the escrow lock becomes refundable after expiry. This is automatic: the solver worker sweeps expired orders and refunds them, and status moves to refunded on its own. You do not need to call anything.

The /settle and /refund endpoints on both rails are operator paths and require an operator bearer token — a partner API key does not open them. Calling them without the token returns a 401 directing you back to status polling.


Contracts

ContractChainAddress
SourceEscrowBase0x3592efdA1b10C94Ee508b827c9dA2AA07E8c7B49
AttestationVerifierMofNBase0xF8e058AC46facfA7f3E02FBDff52d7d9464d606A
OutputSettlerBase0x14c712ba17fE1ef7C0121C2FAB656b2bA2739Bcc
origin_settlerSolana5MwMD5AbUDd2s5E4TSa3GvMKRUKQWxpKgH8LHWFXgqoY
dest_settlerSolana5SsnpHY5AGAR4XsLze8s5SdAqW6ECy1HiaNSTyWLU5AG

The Base escrow and verifier are also reported live by GET /api/config as escrow and verifier — read them from there rather than hardcoding.

The verifier enforces a 2-of-3 attestor threshold. Attestor identities are managed independently of the solver, so the address that delivers cannot also authorize its own claim.


Next steps