AlcorDocs

Bridge

The Alcor bridge moves tokens between Ethereum, BNB Smart Chain, Polygon, Telos, WAX, XPR Network and Vaulta. For partners the interesting route is Ethereum → WAX. The user signs one transaction on Ethereum and the bridge delivers the tokens to their WAX account by itself: there is nothing to claim and no WAX wallet involved. Small deposits usually arrive in 13–20 minutes. The live estimate is in Chains.

There are four ways to integrate it:

  • Deposit link: send users to the Alcor bridge page with the transfer prefilled.
  • Deposit widget: open the bridge over your own page in a modal, popup or inline block.
  • Bridge API: read routes, limits, timing and transfer status yourself.
  • Bridge contracts: call the contracts from your own code or bot, with no Alcor page involved.

Supported assets are USDC, USDT, ETH, BNB, POL, WAX, XPR and A (Vaulta). The authoritative list, with minimums and maximums, is always Routes.

https://alcor.exchange/v/wax/bridge?from=ethereum&to=wax&recipient=alice.wam&token=USDC&amount=50&partner=mygame&return_url=https://mygame.io/play

A plain URL to the bridge page with the transfer already filled in. No code is needed on your side.

Query Parameters

ParameterExampleDescription
fromethereumChain to start on. The user can still change it.
towaxChain the tokens land on.
recipientalice.wamReceiving account, prefilled. The user can edit it on this page.
tokenUSDCAsset, by the symbol the bridge lists (USDC, USDT, ETH).
amount50Prefilled amount, in token units.
partnermygameYour id, so the deposits you bring are attributed to you.
return_urlhttps://mygame.io/playShows a "← Back to mygame.io" link. https only (http is allowed on localhost).

All parameters are optional. A malformed value is ignored rather than guessed at.

Deposit widget

// <script src="https://alcor.exchange/sdk/bridge/v1.js"></script>

document.getElementById('deposit').onclick = function () {
  AlcorBridge.open({
    to: 'wax',
    recipient: 'alice.wam',   // fixed: the user cannot change it
    token: 'USDC',
    amount: '50',
    partner: 'mygame',
    onSent: function (e) { console.log('on its way', e.transactions) },
    onFinished: function (e) {
      if (e.outcome === 'delivered') refreshBalance()
    },
    onClose: function () {},
  })
}
<!-- Without the script: embed the widget yourself -->
<iframe src="https://alcor.exchange/embed/bridge?to=wax&recipient=alice.wam&token=USDC&partner=mygame&mode=inline"
        style="width:100%;max-width:480px;height:640px;border:0"></iframe>

Load https://alcor.exchange/sdk/bridge/v1.js and call one of its functions. The script is tiny and has no dependencies. The path is versioned: v1.js keeps its options and events for as long as anyone uses it.

Functions

FunctionShowsReturns
AlcorBridge.open(options)Modal over your page{ close() }
AlcorBridge.popup(options)Separate window. Call it from a click handler.{ close() }, or null if the popup was blocked
AlcorBridge.mount(element, options)Inline iframe that grows with its content{ destroy() }
AlcorBridge.url(options)Nothing. Returns the widget address for your own iframe or link.string

Browser extension wallets (MetaMask, Rabby) and WalletConnect work in all modes. In the popup the user's existing Alcor session and wallet connection carry over.

Options

OptionRequiredDescription
totrueChain the tokens land on: wax, telos or ethereum. Into WAX the bridge pays out by itself. A deposit to Telos ends with a Collect step signed on Telos, and a withdrawal to Ethereum with a Release step signed on Ethereum. The widget shows that button when its turn comes.
recipientfalseReceiving account. When given, it is fixed in the widget. For WAX, the widget checks that the account exists before anything is signed.
fromfalseChain to start on. Defaults to ethereum, or telos when to is Ethereum.
tokenfalseUSDC, USDT, ETH.
amountfalsePrefilled amount, in token units.
partnerfalseYour id, for attribution.
originfalseWhere the widget is served from. Defaults to the origin of the script itself.
onReady, onSent, onFailed, onFinished, onClosefalseEvent callbacks, see below.

Events

onSent receives:

{
  "direction": "hop",
  "from_chain": "ethereum",
  "to_chain": "wax",
  "token": "USDC",
  "amount": "50",
  "sender": "0x285202c8db763db06ab42e0705104cb7919ef95e",
  "recipient": "alice.wam",
  "partner": "mygame",
  "embed": "iframe",
  "transactions": [
    { "chain": "Ethereum", "label": "Send", "ref": "0x0af8…2917", "url": "https://etherscan.io/tx/0x0af8…2917" }
  ]
}

onFinished receives:

{
  "key": "d:1:12",
  "outcome": "delivered",
  "token": "USDC",
  "amount": "50",
  "transactions": [ … ]
}
CallbackWhenData
onReadyThe widget has loaded and is prefilled{ to, recipient }
onSentThe user signed and the transfer is on its wayTransfer details and the first transaction
onFailedThe transfer could not be started, e.g. rejected in the walletTransfer details plus errorCode, error
onFinishedThe transfer reached its end{ key, outcome, token, amount, transactions }
onCloseThe user closed the widget{}

outcome is delivered, or returned if the destination refused the tokens and they went back to the sender. key identifies the transfer in Transfer status.

Credit nothing in your app on onSent. Wait for outcome: "delivered", or better, check the balance on chain. onFinished only fires while the widget is open. A transfer the user walked away from still completes, and you can follow it with Transfer status.

Messages

If you embed the iframe yourself, listen for postMessage events from https://alcor.exchange:

{ source: "alcor-bridge", version: 1, type, data }

TypeData
ready, sent, failed, finished, closeSame as the callbacks above
resize{ height }, the content height in pixels, so you can size the iframe

Add mode=inline to the widget URL to hide its close button. Everything in the messages is public on-chain data.

Bridge API

Read-only HTTP API of the bridge indexer, open to any origin (CORS *). The full reference, every endpoint and field, is served by the indexer itself: telos.alcor.exchange/api/bridge/docs.

https://telos.alcor.exchange/api/bridge/v1/

Chains are named by id: mainnet (Ethereum), bsc (BNB Smart Chain), polygon (Polygon), telos (Telos), wax (WAX), xpr (XPR Network), vaulta (Vaulta). Amounts are integers in the asset's smallest unit. Divide by 10^precision.

Run your own indexer

The indexer is open source: github.com/alcorexchange/alcor-bridge-indexer (MIT). It holds no keys and sends no transactions, and every proof it serves is verified on chain, so your own instance gives you independence and your own rate limits. It needs a Telos node with state-history; the README covers the rest: cp .env.example .env, docker compose up -d.

Routes

curl https://telos.alcor.exchange/api/bridge/v1/routes
{
  "routes": [
    {
      "symbol": "USDC",
      "precision": 6,
      "from": "mainnet",
      "to": "wax",
      "via": "telos",
      "open": true,
      "min": "5000000",
      "max": "98735000000",
      "how": {
        "deposit": {
          "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
          "telosTo": "hop.alcor",
          "memo": "1181148696416462999:<account>|1:<your address>"
        }
      }
    }
  ]
}

Every way each asset can travel right now, with its limits.

HTTP Request

GET https://telos.alcor.exchange/api/bridge/v1/routes

Response

NameTypeDescription
symbolstringAsset symbol
precisionnumberDecimals of min and max
from, tostringChain ids of both ends
viastringChain the transfer passes through, or null for a direct route
openbooleanWhether the route accepts transfers now
min, maxstringAmount limits, in the smallest unit
howobjectHow the transfer is started. Used by the widget, and by your own code if you send the transaction yourself.

Chains

curl https://telos.alcor.exchange/api/bridge/v1/chains

Relevant part of the response:

{
  "sources": [
    {
      "id": "mainnet",
      "name": "Ethereum",
      "chainId": 1,
      "timing": {
        "deposit": {
          "seconds": 990,
          "range": [768, 1212],
          "measured": true,
          "human": "about 17 min (13 min–20 min)"
        }
      }
    }
  ]
}

State of each chain the bridge reaches, including how long a transfer from it currently takes. Timing is measured on recent transfers, not configured.

HTTP Request

GET https://telos.alcor.exchange/api/bridge/v1/chains

Response

NameTypeDescription
sources[].idstringChain id
sources[].namestringDisplay name
sources[].timing.deposit.secondsnumberTypical time for a deposit from this chain
sources[].timing.deposit.rangenumber[]Fastest and slowest recent times, in seconds
sources[].timing.deposit.measuredbooleantrue when measured on recent transfers
sources[].timing.deposit.humanstringThe same, phrased for people

Transfer status

curl https://telos.alcor.exchange/api/bridge/v1/hops/d:1:12
curl "https://telos.alcor.exchange/api/bridge/v1/hops?party=alice.wam"
{
  "key": "d:1:12",
  "status": "delivered",
  "progress": {
    "stage": "delivered on WAX",
    "percent": 100,
    "etaSeconds": null
  },
  "from": { "chain": "mainnet", "address": "0x285202c8db763db06ab42e0705104cb7919ef95e" },
  "to": { "chain": "wax", "recipient": "alice.wam" },
  "back": { "chain": "mainnet", "recipient": "0x285202c8db763db06ab42e0705104cb7919ef95e" },
  "deposit": {
    "symbol": "USDC",
    "amount": "50000000",
    "precision": 6,
    "src_tx": "0x0af8049909488c9ebb129dbc30db39ca0c7dc7009978756a8562e38970f92917"
  }
}

A transfer from Ethereum to WAX, by its key (the key from the widget's onFinished), or every transfer an account or address took part in. The list form returns { "hops": [ … ] }, newest first.

HTTP Request

GET https://telos.alcor.exchange/api/bridge/v1/hops/<key>

GET https://telos.alcor.exchange/api/bridge/v1/hops?party=<account or address>

Status

StatusMeaning
arrivingLocked on Ethereum, waiting for Ethereum to finalize
sendingPassed through Telos, on its way to the destination
deliveredPaid out to the recipient
bouncedRefused by the destination, sent back
returningComing back to the sender
returnedBack with the sender

Bridge contracts

Everything the widget does is a public contract call. Bots, wallets and exchanges can make the same calls themselves: no API key, no allowlist, no Alcor page involved.

The full guide for bots, with every chain's transaction, how to follow a transfer and what can go wrong: telos.alcor.exchange/api/bridge/docs/bot.

How the bridge is built:

  • Telos holds the ledger, bridge.alcor. Every route runs between Telos and one other chain. Ethereum ↔ WAX is two legs, joined by the hop.alcor contract on Telos.
  • A deposit is locked in the vault on its chain, proved on Telos and credited there. The bridge's relayer pushes the proof, so you send one transaction.
  • A withdrawal is burned on Telos and proved on the destination chain. Into WAX the relayer pays it out. Into Ethereum or BSC you send the proof yourself, in one transaction (see Release on Ethereum or BSC).
  • Keys name each leg: d:<domain>:<nonce> for a deposit, w:<id> for a withdrawal. Every endpoint of the Bridge API takes them.

Do not hardcode addresses in a bot. Read them from Chains (sources[].contracts) and Routes (how). They are listed below for orientation, as deployed on 2026-10-03.

Addresses

ChainContractAddressWhat it is
EthereumAlcorVault0x3e447d533321ad6a8412f97034ac295a9ff8d858Deposits in, withdrawals paid out
EthereumAlcorWithdrawals0x04fb700b93eb68cadd88d8e8d4dfe4857d5cb3c6Verifies a withdrawal proof and tells the vault to pay
EthereumUSDC, USDT0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48, 0xdac17f958d2ee523a2206206994597c13d831ec7ETH is native: token 0x0000000000000000000000000000000000000000
BSCAlcorVault0x53f18eaa8bf8099b5ba21bb7e11ed311b677690eSame contract as on Ethereum
BSCAlcorWithdrawals0x6132b4d02e02f1b378f0fc67c3d4f553eb76acb5Same contract as on Ethereum. BNB is native
Telosbridge.alcorThe ledger: credits deposits, opens withdrawals
Teloswrap.alcorBridged tokens on Telos: USDC and USDT (6 decimals), ETH, BNB and WAX (8)
Teloshop.alcorCarries a transfer between two chains through Telos
WAXbridge.alcorThe vault on WAX: holds WAX, mints and burns the mirrors
WAXwrap.alcorMirror tokens on WAX: USDC, USDT, ETH, BNB. WAX itself is eosio.token

ABIs of the EVM contracts: AlcorVault.json, AlcorWithdrawals.json. The Antelope contracts publish theirs on chain (get_abi).

Domains and units

ChainDomain
Ethereum1
BSC56
WAX1181148696416462999 (first 8 bytes of the WAX chain id)

The domain names the far chain in every memo and key. It is a decimal string in the API: the WAX one is larger than a JSON number holds exactly.

Amounts. The ledger counts in Telos units (the token's precision on Telos). On an EVM chain, amount = canonical × scale: scale is 1 for USDC and USDT and 10^10 for ETH and BNB (18 decimals there, 8 on Telos). A remainder below scale is not taken: an ERC-20 deposit pulls only canonical × scale, and the excess of a native deposit is sent back in the same transaction.

Telos accounts on EVM. telosTo is an Antelope account name packed into a uint64, the standard Antelope encoding. With wharfkit: BigInt(Name.from('hop.alcor').value.toString()).

Deposit from Ethereum or BSC

import { Contract, toUtf8Bytes, hexlify } from 'ethers'   // ethers v6
import { Name } from '@wharfkit/antelope'

const API = 'https://telos.alcor.exchange/api/bridge/v1'
const vaultAbi = await (await fetch('https://api.alcor.exchange/abi/AlcorVault.json')).json()

const { sources } = await (await fetch(`${API}/chains`)).json()
const vault = new Contract(sources.find(s => s.id === 'mainnet').contracts.vault, vaultAbi, signer)

const telosName = n => BigInt(Name.from(n).value.toString())
const me = await signer.getAddress()

// ETH to the WAX account alice.wam, back to me if WAX never pays
const memo = hexlify(toUtf8Bytes(`1181148696416462999:alice.wam|1:${me}`))
await vault.depositNative(telosName('hop.alcor'), me, 0, 0, memo, { value: 10n ** 16n })

// 50 USDC to the Telos account alice (approve the vault first)
const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'
await vault.deposit(USDC, 50_000000n, telosName('alice'), me, 0, 0, '0x')

deposit(token, amount, telosTo, refundTo, fillFee, filler, memo) for an ERC-20, after approve(vault, amount). depositNative(telosTo, refundTo, fillFee, filler, memo) payable, for ETH or BNB.

ParameterTypeDescription
tokenaddressERC-20 address from Routes (how.deposit.token)
amountuint256Raw token units
telosTouint64Who receives on Telos: an account, or hop.alcor to go on to another chain
refundToaddressWhere the deposit goes back if it cannot be delivered. Must not be zero
fillFee, filleruint64Reserved. Pass 0
memobytesUp to 256 bytes, see below

Where it lands depends on telosTo and memo:

  • A Telos account, empty memo: credited to that account on the ledger. The owner collects it with bridge.alcor::claim (see Telos actions).
  • A Telos account or contract, with a memo: delivered to it as a wrap.alcor transfer carrying that memo. This is how a deposit pays a contract.
  • To WAX: telosTo = hop.alcor, memo <WAX domain>:<WAX account>|<this chain's domain>:<your address>. The first half is where it goes, the second where it comes back if WAX never pays it. The tokens arrive on WAX by themselves.

Check that the WAX account exists before you deposit. The bridge cannot see WAX accounts: tokens sent to a missing one come back only after the withdrawal deadline, about a week.

USDT on Ethereum refuses to change a non-zero allowance: set it to 0 first.

A deposit the vault refuses reverts with a custom error from the ABI: DepositsPaused, AssetDisabled, BelowMinimum(minDeposit), CapExceeded, AmountTooSmall, MemoTooLong, ZeroAccount, ZeroRefundAddress, UseDepositNative (the native coin through deposit).

The Deposit event gives the key: d:<chainId>:<nonce>.

Deposit from WAX

# 100 USDC from WAX to the Telos account alice
cleos -u https://wax.greymass.com push action wrap.alcor transfer \
  '["bob.wam", "bridge.alcor", "100.000000 USDC", "alice"]' -p bob.wam

# 100 USDC from WAX to an Ethereum address
cleos -u https://wax.greymass.com push action wrap.alcor transfer \
  '["bob.wam", "bridge.alcor", "100.000000 USDC", "hop.alcor:1:0x285202c8db763db06ab42e0705104cb7919ef95e|1181148696416462999:bob.wam"]' -p bob.wam
// wharfkit session on WAX
await session.transact({
  action: {
    account: 'wrap.alcor', name: 'transfer',
    authorization: [session.permissionLevel],
    data: { from: session.actor, to: 'bridge.alcor', quantity: '100.000000 USDC', memo: 'alice' },
  },
})

A plain transfer to bridge.alcor on WAX. The token is the route's antelopeToken in Chains: eosio.token for WAX, wrap.alcor for the mirrors. The memo says where it goes:

  • <telos account>: credited on Telos, collected with claim.
  • <telos account>:<note>: delivered to that account as a transfer with <note> as its memo.
  • hop.alcor:<domain>:0x<address>|1181148696416462999:<your WAX account>: lands on Ethereum (1) or BSC (56). The last leg is a withdrawal you release yourself, see Release on Ethereum or BSC. The second half is where it comes back if that leg is never released.

The deposit's key is d:1181148696416462999:<nonce>, the nonce from the vault's deplog action in your transaction.

Withdraw from Telos

# 50 USDT from Telos to an Ethereum address
cleos -u https://mainnet.telos.net push action wrap.alcor transfer \
  '["alice", "bridge.alcor", "50.000000 USDT", "1:0x285202c8db763db06ab42e0705104cb7919ef95e"]' -p alice

# 10 WAX from Telos to the WAX account bob.wam
cleos -u https://mainnet.telos.net push action wrap.alcor transfer \
  '["alice", "bridge.alcor", "10.00000000 WAX", "1181148696416462999:bob.wam"]' -p alice

A transfer of a wrap.alcor token to bridge.alcor on Telos, with the destination in the memo: <domain>:<recipient>[:<fee>].

  • Into WAX the relayer pays it out, in about two minutes. Nothing else to do.
  • Into Ethereum or BSC the withdrawal waits for you to release it on that chain, see the next section.
  • fee is optional, a whole number of Telos units out of the amount. It goes to whoever sends the release, so a recipient with no gas on the destination chain can still be paid by someone else. Without it, the fee is zero.

The amount must not exceed withdrawableNow of the route in Chains: each vault lets only so much out per day.

To attach data for the receiver, use wrap.alcor::withdraw(owner, quantity, domain, recipient, fee, memo) instead of a transfer: recipient is the 20 address bytes, fee an asset in the same token, and memo up to 1024 bytes. The memo is proved with the amount and comes out in the Released event of AlcorWithdrawals.

The withdrawal id is in the inline bridge.alcor::wdlog action of your transaction: wdlog(id, domain, vault, token, recipient, amount, deadline, fee, memo). The key is w:<id>.

Release on Ethereum or BSC

const wdAbi = await (await fetch('https://api.alcor.exchange/abi/AlcorWithdrawals.json')).json()
const p = await (await fetch(`${API}/proofs/w:${id}`)).json()   // 404 until the proof exists

if (p.track === 'light') {
  // a root covering this burn is already trusted: ~145-180k gas
  await new Contract(p.light.releaseTo, wdAbi, signer).release(p.light.release)
} else {
  // the root is submitted in the same transaction: ~2.6M gas
  const s = p.hard.submitRoot
  await new Contract(p.hard.releaseTo, wdAbi, signer)
    .submitAndRelease(s.finality, s.bits, s.signature, s.policyKeys, s.pending, p.hard.release)
}

A withdrawal to an EVM chain is paid out when someone sends its proof to AlcorWithdrawals. Anyone may send it: every field is checked against a root signed by Telos finalizers, and the tokens go only to the recipient the burn named.

  1. Wait until GET /v1/transfers/w:<id> shows hasProof: true, about a minute after the burn.
  2. Fetch GET /v1/proofs/w:<id>. Its objects are in Solidity struct order and carry both contract addresses. Pass them through unchanged.
  3. Send release on the light track or submitAndRelease on the hard track. One transaction either way.

Deadline. The proof carries a deadline, about a week after the burn. Past it release reverts with PastDeadline. Send the same body to expire (or submitAndExpire) instead, and the tokens come back to their owner on Telos.

Daily limit. A release over the vault's remaining daily allowance reverts with OutflowLimited and consumes nothing. The same proof works once the allowance refills, any time before the deadline.

Fees. When the withdrawal set a fee, msg.sender receives it in the same transaction. GET /v1/pending lists every withdrawal ready to settle right now, with action saying release or expire.

Telos actions

# Collect a credited deposit
cleos -u https://mainnet.telos.net push action bridge.alcor claim '["alice", "USDC"]' -p alice
ActionWho may callWhat it does
bridge.alcor::claim(owner, sym_code)The ownerTransfers a credited deposit to its owner. Table claims, scope = owner
bridge.alcor::forward(domain, nonce)AnyoneDelivers a deposit that carried a memo. The relayer does it; call it if you do not want to wait
bridge.alcor::bounce(domain, nonce)Anyone, an hour after arrivalSends an undeliverable deposit back to its refundTo
hop.alcor::recover(id)AnyoneSends a hop that came back from its destination on to its way-back address
<gateway>::creditAnyone, with a proofCredits a deposit. The relayer does it. To do it yourself, send the actions of GET /v1/proofs/d:<domain>:<nonce> in one transaction

Tables worth reading on bridge.alcor: routes (limits and backing per route), claims (credited, not yet collected), wdraws (withdrawals and their status).

Contract events

ContractEventMeaning
AlcorVaultDeposit(token, from, amount, canonical, telosTo, nonce, refundTo, fillFee, filler, memo)Deposit locked. Key d:<chainId>:<nonce>
AlcorVaultReleased(id, token, to, amount, feeTo, fee)Withdrawal w:<id> paid out, raw units
AlcorVaultVoided(id)Withdrawal w:<id> expired, refunded on Telos
AlcorWithdrawalsReleased(id, token, to, amount, memo)The same payout with the Telos memo, in Telos units
AlcorWithdrawalsExpired(id)expire accepted
WAX bridge.alcoraction deplog(nonce, bridge, token_contract, token_symbol, from, raw, canonical, beneficiary, memo)Deposit from WAX. Key d:1181148696416462999:<nonce>
Telos bridge.alcoraction wdlog(id, domain, vault, token, recipient, amount, deadline, fee, memo)Withdrawal opened. Key w:<id>

Transfers and timing

curl https://telos.alcor.exchange/api/bridge/v1/transfers/d:56:1
curl "https://telos.alcor.exchange/api/bridge/v1/transfers?address=0x285202c8db763db06ab42e0705104cb7919ef95e&limit=20"

GET /v1/transfers/<key> is one leg; GET /v1/transfers?account=&address=&direction=&status=&limit=&before= lists legs, newest first (before is the src_time of the last row, for paging). Transfer status follows a whole Ethereum ↔ WAX journey instead. A leg moves through these statuses:

deposit      seen ──▶ provable ──▶ credited
                               └─▶ parked ──▶ forwarded
                                          └─▶ bounced
withdrawal   burned ──▶ provable ──▶ released
                                └─▶ expired ──▶ refunded

Typical times, measured on recent transfers. The live figures are in Chains under timing:

From → toTimeYour transactions
Ethereum → Telosabout 17 min, Ethereum finalityDeposit
Ethereum → WAXabout 20 minDeposit
BSC → Telosabout 30 sDeposit
BSC → WAXabout 3 minDeposit
WAX → Telosabout 50 sTransfer
Telos → WAXabout 2 minTransfer
Telos → Ethereum or BSCabout 1 min to the proofTransfer, then release
WAX → Ethereum or BSCabout 2–3 min to the proofTransfer, then release

Poll every 10–15 seconds: a transfer changes state on the scale of minutes.

On this page