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.

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' \
  -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 ".../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 a bearer token. Calling them without one returns a 401 directing you back to status polling.


Testnet contracts

ContractChainAddress
SourceEscrowBase0x24E047cce017ed2623C1455D21B6E8574AA6C3D0
AttestationVerifierMofNBase0xD79E70860Db6171C2288f0ae78756F3EF24357E3
OutputSettlerBase0xc748F7996eea4e0Ae1C56F41E346F63CA1D92ACb
origin_settlerSolana5MwMD5AbUDd2s5E4TSa3GvMKRUKQWxpKgH8LHWFXgqoY
dest_settlerSolana5SsnpHY5AGAR4XsLze8s5SdAqW6ECy1HiaNSTyWLU5AG

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.

Mainnet addresses are shared with allowlisted accounts.


Next steps