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
- Open. The user locks the input in the source escrow. You build this transaction via the API and the user signs it.
- Deliver. The solver sends the output to the recipient on the destination chain, from its own inventory.
- Attest. Attestors independently verify the delivery on-chain and sign. A 2-of-3 quorum is required.
- Claim. The solver presents the quorum attestation to the source escrow and claims the locked input.
- 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"
}'| Field | Required | Description |
|---|---|---|
user | Yes | The Solana address funding the swap. |
inSym | Yes | Input token symbol on Solana. |
outSym | Yes | Output token symbol on Base. |
uiAmount | Yes | Human decimal amount. Subject to the cross-chain cap. |
recipientEvm | Yes | Base 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"| Query | Required | Description |
|---|---|---|
rail | No | sol-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
}status | Meaning |
|---|---|
pending | Locked on the source chain, awaiting delivery. |
claimed | Delivered and claimed. The swap succeeded; deliverRef is the destination transaction. |
refunded | Expired unfilled; the input returned to the user. Terminal. |
attention | Flagged for operator review. reason carries the detail. |
unknown | No 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
| Contract | Chain | Address |
|---|---|---|
SourceEscrow | Base | 0x24E047cce017ed2623C1455D21B6E8574AA6C3D0 |
AttestationVerifierMofN | Base | 0xD79E70860Db6171C2288f0ae78756F3EF24357E3 |
OutputSettler | Base | 0xc748F7996eea4e0Ae1C56F41E346F63CA1D92ACb |
origin_settler | Solana | 5MwMD5AbUDd2s5E4TSa3GvMKRUKQWxpKgH8LHWFXgqoY |
dest_settler | Solana | 5SsnpHY5AGAR4XsLze8s5SdAqW6ECy1HiaNSTyWLU5AG |
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
- Same-chain swaps: atomic single-transaction swaps
- Quotes and token info: read endpoints and redemption pricing