Skip to main content

Bridges

A bridge moves a treasury balance from one chain to another. Sweeps consolidate within a chain; bridging is what gets those balances onto the one chain you want to hold them on.

ChainOS facilitates a bridge. It does not operate one. It selects the provider for a route, builds the unsigned source transaction, polls the attestation service, builds the unsigned destination transaction, tracks the state and records both legs in your ledger. It never holds your funds in transit, provides no liquidity and takes no custody at any point. Your Edge signs both legs, from keys we do not have.

That is also why provider selection is narrow: ChainOS only uses issuer-operated burn-and-mint bridges, where the issuer of the token controls the contract on both sides. A third-party wrapper would hand you an asset nobody is obliged to honour.

The in-flight window

Almost every burn-and-mint bridge is two transactions on two chains with an attestation between them:

A burn on the source chain, an attestation signed by the token issuer's service, and a mint on the destination chain — with the funds in neither chain in between.

Between the burn and the mint your funds exist in neither place. Everything else on this page follows from that.

The transfer status that matters is SOURCE_CONFIRMED. Before it, nothing is at risk and a failure costs you a gas fee. After it, the funds exist only as a claim against an off-chain service, and inFlightSince is stamped — every threshold, alert and exposure figure counts from that moment rather than from when you started the transfer. Time spent building and signing carries no risk, and counting it would make every alert wrong by however long your Edge took to wake up.

Three refusals, all of them cheap

POST /v1/treasury/bridges/transfers checks three things before it builds anything, in this order. All three leave your funds in your source treasury wallet. After the burn there are no cheap refusals left.

1. Can the destination receive? A live on-chain check, not a configuration lookup. On Solana the associated token account must exist; on the XRP Ledger and Stellar a trust line must be established. Burning when the destination cannot receive leaves the funds attested and unredeemable — so this is checked at quote time and again immediately before the burn.

POST /v1/treasury/bridges/quote returns destinationReady and, when it is false, a preparation array carrying the unsigned transaction that would fix it. Sign it through the ordinary path and quote again.

RLUSD on the XRP Ledger

RLUSD there is an issued currency, not a token contract. Your address needs a trust line to the issuer before it can hold any, and only the holder can sign one — the sender cannot create it for you. The trust line also locks a further 2 XRP of owner reserve, so an RLUSD-capable address needs 12 XRP rather than 10. An address funded to exactly the base reserve cannot open one.

2. Are you within your in-flight limits? Three caps on your bridge policy bound how much of your money can be burned and not yet minted at any moment:

{ "maxInFlightValue": "500000000000",
"maxInFlightCount": 20,
"maxSingleTransfer": "100000000000" }

Above any of them the transfer is refused with 409 IN_FLIGHT_LIMIT_EXCEEDED, naming what is already in flight. This is the guard working rather than a failure: refusing a bridge is always recoverable — the funds are still on the source chain and you can transfer in an hour. Accumulating unbounded exposure to an off-chain attestation service is not.

3. Is the cost within maxFeeRatioBps? It defaults to 100 bps, a third of a sweep policy's 300, and the difference is deliberate: a sweep's alternative is leaving funds stranded in a deposit address, while a bridge's alternative is waiting until more has accumulated, which costs nothing. Bridging 50 USDC out of Ethereum can cost several dollars across both legs.

The ratio is the protocol fee's share of the amount. Gas is reported separately in each chain's own currency and deliberately not folded in — comparing a fee in ETH against an amount in USDC needs a price, and ChainOS has no price feed and will not acquire one to answer this.

When something goes wrong

The failure states are not interchangeable, and treating them as one is the most expensive mistake you can make here.

StatusWhat it meansWhat to do
SOURCE_FAILEDThe burn never landed. No funds were at risk.Retry immediately. Nothing left your wallet.
STALLED_ATTESTATIONBurned, not attested. Funds in flight.Do not retry. See below.
STALLED_RELAYAttested, not minted. Funds in flight, recoverable.POST /relay — submit the mint yourself.
EXPIREDThe attestation window lapsed.POST /reattest. Do not start a new transfer.
DEST_FAILEDThe mint reverted. The attestation is still valid.Fix the cause, then POST /relay.
Never retry a stalled transfer

A stalled bridge is alarming, and the instinct is to start another one. That would burn a second amount against a message that is still perfectly valid. Every transfer carries a recommendedAction field saying what to actually do; the bridge_stalled webhook carries the same sentence. Show it to whoever is on call.

Stall thresholds come from the provider rather than from a global setting, because fifteen minutes is a stall for LayerZero and an ordinary finalized CCTP transfer out of Ethereum. Each provider declares its own expected timings; we warn at three times and alert at ten.

Relay modes

ModeWho submits the mint
self_relayYou do. We build it, your Edge signs, we broadcast.
auto_relayThe protocol's relayer network. We watch for completion.
hybridAuto-relay, then self-relay after a timeout.

hybrid is the default. Relayer networks stall, and without a fallback a stalled relay leaves funds burned and unminted indefinitely. You can also POST /relay at any time once an attestation is in hand — you should not have to wait for our timer to agree with you.

Watching your exposure

GET /v1/treasury/bridges/in-flight
{
"byToken": [
{ "token": "USDC", "count": 3, "value": "72100000000",
"oldestInFlightSeconds": 340 }
],
"stalled": [],
"maxInFlightValue": "500000000000",
"utilisationBps": 1684
}

oldestInFlightSeconds is the number to alert on. A total says how much is at risk; that one says whether something is wrong.

The routes

TokenProtocolNotes
USDC, EURCCircle CCTPOne integration, both assets.
USDTLayerZero OFTVerified chains only — see below.
RLUSDWormhole NTTThe XRPL leg needs a trust line.
cNGNIssuer APIPermissioned; needs a relationship with the issuer.

Native coins are never bridged. Moving ETH to Polygon needs either a wrapping bridge or a swap — a wrapped derivative or third-party liquidity — and both are excluded by design. Native assets are swept, not bridged.

Some gaps are deliberate and worth knowing about:

  • USDC on BNB Smart Chain. Circle has a CCTP domain for BSC (domain 17), but does not issue USDC there — the domain carries USYC. A burn addressed to it could never be minted, so the route does not exist.
  • USDT on Base. Native USDT does not exist on Base at all.
  • USDT on TRON. TRON carries an enormous amount of legacy USDT and is not an EVM LayerZero endpoint, so the OFT standard does not reach it.

Ask for a route that does not exist and you get 422 NO_BRIDGE_ROUTE with the reason and any alternatives that would serve the same purpose. You should never discover a gap by receiving an error after building on top of it — which is why pipelines check routes at configuration time.

Rehearse it first

Everything on this page works with a sandbox key, and the sandbox is a simulator rather than a stub: your Edge really signs. What is simulated is the outside world — the attestation service, the gas price, the provider's availability.

GET /v1/sandbox/treasury/scenarios

Eight scenarios, each corresponding to a production incident that will eventually happen. Two matter more than the rest: stalled attestation and expired attestation. Both put your funds in the state where they exist in neither place, and the cheapest possible moment to find out whether your alerting fires and your team knows not to retry is now.

The sandbox route matrix is identical to production, so you cannot build something in sandbox that is impossible live.