# Relayers

A relayer pays the recipient on the destination chain within seconds, out of its own stock of the token, and is paid back, plus its fee, when the bridged tokens arrive over the standard path a few seconds later. Nobody has to trust it: the recipient, the amount and the fee are fixed by the order, and a relayer that pays anything else is simply never repaid. If no relayer fills an order, the transfer arrives the standard way, and the fee no relayer earned goes to the recipient.

There is one fast bridge per chain, shared by every token launched on Pangea. A new token can use it the moment its launch lands: nothing is deployed for it. An order names its launch, never a token address, and the bridge reads the token itself from Pangea’s own factories, so a relayer is only ever repaid in, and a recipient only ever paid in, that launch’s own token on that chain.

> **Note:** The fast bridges on Ethereum, Base, Robinhood Chain and BSC are deployed, at the addresses listed at the end of this section. Orders out of Solana are posted by Pangea’s fast-bridge program, in the same format. Into Solana the fast lane needs a token that allows a 232-byte message, which every launch from the current factories does.

## What a transfer costs, and who is paid

- **The token’s fee.** The token keeps 0.2% of every transfer between chains, on either lane. It goes to the platform.
- **The fast lane’s fee.** On the fast lane the fast bridge keeps 0.2% first, also for the platform, and the token’s fee is then taken from what is left. The bridge’s admin can change it, per launch or for all, but never above 1%.
- **The relayer’s fee.** This is a price, not a platform fee. Relayers publish what they charge and the app takes the lowest. The bridge refuses any order that would pay a relayer more than 10% of what arrives, or the lower cap its admin sets.
- **Your limit.** You set a slippage, 0.5% unless you change it and 10% at most. If the best relayer price is above it, no fast transfer is offered, and the bridge refuses an order that would pay the recipient less.

One transfer of 1,000 tokens, three ways, with a relayer charging 0.1% as the example price:

| Who gets | Standard | Fast, filled | Fast, unfilled |
| --- | --- | --- | --- |
| Platform | 2 | 3.996 | 3.996 |
| Relayer | 0 | 0.996004 | 0 |
| Recipient | 998 | 995.007996 | 996.004 |
| Arrives in | 4 to 16 seconds | seconds | 4 to 16 seconds |

On the fast lane the fast bridge keeps 2 tokens and the token keeps 1.996, so 996.004 are on their way. The relayer pays the recipient 995.007996 at once and receives the 996.004 when they arrive, keeping 0.996004. Both platform fees are taken on the chain the transfer leaves, when it is sent, so a fast transfer that nobody fills has still paid them.

## Becoming a relayer

Anyone can be a relayer. There is nothing to apply for and nothing to stake: you publish a price, watch for orders and fill the ones you want. Filling is never an obligation, and leaving an order unfilled takes nothing from anyone. It does count against you if the order was posted at your published price, as described under benching below.

What follows is the whole protocol, two HTTP calls and a handful of contract calls, and any program that follows it is a relayer. The HTTP calls are for the price board only: watching and filling work entirely on chain.

## 1. What you need

- **The token, on every chain you deliver to.** Fills are paid from your own wallet. The fast bridge holds no stock of its own.
- **An approval.** On each of those chains, approve that chain’s fast bridge to move your tokens: `approve(fastBridge, amount)` on the token. Without it your prices are not taken and a fill reverts.
- **Gas.** A fill is a transaction on the destination chain, paid in its native asset: ETH on Ethereum, Base and Robinhood Chain, BNB on BSC.
- **An RPC endpoint for each chain.** You read `OrderPosted` logs on the chains orders leave and send fills on the chains they go to, so the endpoint must serve `eth_getLogs`. A WebSocket endpoint makes you faster: subscribed to the fast bridge’s logs it tells you about an order the moment it is posted.
- **One key.** The address that signs your prices is the address the app checks for tokens and approval, so sign with the wallet you fill from.

Check a bridge before you approve it. `tokenOf(launchId)` returns the token it moves for a launch, read from Pangea’s factories (`isFactory(factory)` says which it accepts), and `remoteBridge(eid)` its counterpart on another chain.

## 2. Publish your price

You send a signed price sheet to the price board. The board is public: read it to see the prices you are competing with, the ceiling, and who is benched. A route is a pair of chains on one shared bridge, so one price covers every token on that route; the app only takes it for a transfer when you hold that token on the destination.

- Publish, POST: `https://www.pangea.trade/api/bridge/relayer-quotes`
- Read, GET: `https://www.pangea.trade/api/bridge/relayer-quotes`

The body you POST:

```json
{
  "sheet": {
    "v": 1,
    "relayer": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "issuedAt": 1760000000,
    "expiresAt": 1760000120,
    "routes": [
      {
        "fromEid": 30184,
        "toEid": 30102,
        "bridge": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
        "feeBps": 10,
        "minFee": "1000000",
        "maxAmount": "50000000000000"
      },
      {
        "fromEid": 30101,
        "toEid": 30102,
        "bridge": "0xcccccccccccccccccccccccccccccccccccccccc",
        "feeBps": 10,
        "minFee": "1000000",
        "maxAmount": "50000000000000"
      }
    ]
  },
  "signature": "0x<65-byte signature, 130 hex characters>"
}
```

| Field | What it is |
| --- | --- |
| `v` | Always 1. |
| `relayer` | Your address. The signature must be this address’s. |
| `issuedAt`, `expiresAt` | Unix seconds. A sheet may live at most 300 seconds, and its issue time may be at most 120 seconds ahead of the server’s clock. |
| `routes` | One to 64 routes. The same route may not appear twice. |
| `fromEid`, `toEid` | The LayerZero endpoint ids of the chain an order leaves and the chain you deliver on. |
| `bridge` | The fast bridge on the chain the order leaves. Not the one you fill on. |
| `feeBps` | Your price in basis points of what arrives, a whole number from 0 to the board’s ceiling. The ceiling is 1000, which is 10%, unless the board reports another. |
| `minFee` | The least you charge whatever the size, in base units, as a decimal string. Gas does not shrink with the amount. |
| `maxAmount` | The most you will front on that route, in base units, as a decimal string above zero. |
| `solana` | Optional, and needed for any route that delivers to Solana: your Solana address. The body then also carries `solanaSignature`, your Solana key’s Ed25519 signature over the same text, in base58 or base64. On a route from Solana, `bridge` is 32 bytes, written as 0x and 64 hex digits. |

The signature is over the text below, signed as an Ethereum personal message: EIP-191, which wallets offer as `personal_sign`. The lines are joined by a single newline with none at the end, addresses are in lower case, and the routes are sorted by `fromEid`, then `toEid`, then `bridge`, whatever order the JSON lists them in. This is the text for the body above:

The text you sign:

```text
Pangea relayer quotes v1
relayer: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
issued: 1760000000
expires: 1760000120
30101>30102 0xcccccccccccccccccccccccccccccccccccccccc 10bps min 1000000 max 50000000000000
30184>30102 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 10bps min 1000000 max 50000000000000
```

Your fee for a transfer is `feeBps` of what arrives, rounded up, and never less than `minFee`. If that comes to more than the ceiling allows on that transfer, or the transfer is larger than `maxAmount`, you have no price for it.

A sheet stops counting at its expiry, so publish again before then. Every 30 seconds is plenty, and the board accepts 120 publishes per caller in ten minutes. Each new sheet replaces your last one and must carry a later `issuedAt`. Stop publishing and you are off the board when your last sheet expires.

| Answer | What it means |
| --- | --- |
| `200` | Kept. The body is `{ ok, relayer, expiresAt }`. |
| `400` | The sheet is malformed, expired or over the ceiling. The reasons are in `error`. |
| `401` | The signature is not this relayer’s over this sheet. |
| `403` | This address may not publish on this deployment. The board is open to any address unless the deployment lists who may publish or has turned an address away. |
| `409` | A sheet issued at the same time or later is already held for this relayer. |
| `413` | The body is larger than 32,768 bytes. |
| `429` | Too many requests. The message says how many seconds to wait. |
| `424` | The board cannot be read or written right now. |

What a GET returns:

```json
{
  "now": 1760000030,
  "maxFeeBps": 1000,
  "sheets": [
    {
      "sheet": "<a sheet, as published>",
      "signature": "0x<its signature>"
    }
  ],
  "benched": [
    {
      "relayer": "0x<address>",
      "until": 1760003600
    }
  ]
}
```

Reading is limited to 300 requests per caller in ten minutes. `maxFeeBps` is the ceiling in force, `sheets` every live sheet with its signature, and `benched` the relayers whose prices are not being taken, with the unix second each bench ends.

**How the app chooses.** For each transfer it works out every live sheet’s price and looks at the 4 lowest, lowest first. Between equal prices the sheet with the earlier `issuedAt` comes first. It takes the first whose relayer holds what the fill pays, in that launch’s token on the destination chain, and has approved the fast bridge there for it. Benched relayers are left out. If that price would leave the recipient less than the sender’s slippage allows, no fast transfer is offered. When no relayer quotes a transfer at all, the fast lane is simply not shown.

## 3. Watch for orders

Every fast transfer is an order, posted on the fast bridge of the chain it leaves. That bridge emits one event for it:

On the source chain’s fast bridge:

```solidity
event OrderPosted(
    bytes32 indexed orderId,
    bytes32 indexed launchId,
    uint32 dstEid,
    bytes32 indexed sender,
    uint64 senderNonce,
    bytes32 to,
    uint256 amount,
    uint256 relayerFee,
    uint64 lzNonce
);
```

- topic0: `0x7e20a23f868a6866f04de222fcf771b4747be0d748f6c3af50083f019a56c231`

| Field | What it is |
| --- | --- |
| `orderId` | `keccak256(abi.encode(uint32 srcEid, bytes32 srcBridge, bytes32 launchId, bytes32 sender, uint64 senderNonce, uint32 dstEid, bytes32 to, uint256 amount, uint256 relayerFee))`. |
| `launchId` | The launch the order is for. The token on either chain is `tokenOf(launchId)` on that chain’s bridge. |
| `dstEid` | The endpoint id of the chain to deliver on. |
| `sender`, `senderNonce` | Who posted the order, as 32 bytes, and that sender’s own counter on that bridge. |
| `to` | The recipient, as 32 bytes: an EVM address left-padded with zeros, or a Solana owner’s public key. An EVM destination refuses a recipient whose top 12 bytes are not zero. |
| `amount` | What arrives on the destination, in base units. Not what the sender parted with: both platform fees are already out of it. |
| `relayerFee` | What the filler keeps, out of `amount`, in base units. |
| `lzNonce` | The LayerZero nonce of the message carrying the order. A fill has to name it. |

## Orders from Solana

An order from Solana is posted by Pangea’s fast-bridge program (`post_order`) and carried by the sender’s own OFT send, in the same Solana transaction. On an EVM destination, for source endpoint `30168`, the order’s `srcBridge` is the sender’s own key (the OFT’s `composeFrom`), and the sender in the order is always taken to be that key: nobody can post in another’s name.

**Verify before you fill.** Skipping `post_order` only skips the fast lane’s fee, so an EVM-side check cannot tell you the order was paid for. Before filling an order from Solana, read the Solana transaction yourself and fill only when it holds both the `post_order` instruction and the OFT send it checks, with the same launch, recipient, amount and fee, and the transaction is finalized. A fill on anything less is a bet nobody else is making for you.

## 4. Fill

A fill is one call, made on the chain the order is going to:

On the destination chain’s fast bridge:

```solidity
function fillOrder(
    uint32 srcEid,
    bytes32 srcBridge,
    bytes32 launchId,
    bytes32 sender,
    uint64 senderNonce_,
    address to,
    uint256 amount,
    uint256 relayerFee,
    uint64 lzNonce
) external returns (bytes32 id);
```

`srcEid` is the endpoint id of the chain you saw the event on, and `srcBridge` the bridge that emitted it, as 32 bytes (the address left-padded with zeros): the destination accepts only a bridge it has named for that chain, else `NotARemoteBridge`. From Solana, `srcBridge` is the sender’s own key instead (see below). The other arguments are the event’s own fields, with `to` as the 20-byte address it pads: `senderNonce_` is its `senderNonce`. In the same transaction the bridge moves `amount - relayerFee` of `tokenOf(launchId)` from your wallet to the recipient and records you as owed `amount`.

Every one of those fields is hashed into the order’s id. Change the source bridge, the launch, the amount, the recipient or the fee and you have filled an order nobody posted: you have paid someone, and nothing will ever repay you.

Check before you send, on the destination bridge. Each check is a read and saves the gas of a fill that would revert:

- `filledBy(orderId)` is the zero address: nobody has filled it yet.
- `notYetArrived(token, srcEid, lzNonce)`, with `token` the launch’s token on this chain, is true: the order’s own message has not reached this chain. Once it has, there is nothing left to deliver early and a fill is refused.
- You hold `amount - relayerFee` of the token there and your approval covers it.
- `relayerFee` is at least your own price for that amount. You are never obliged to fill.

| Revert | What it means |
| --- | --- |
| `NoRemote(srcEid)` | This bridge has no counterpart on that source chain. |
| `NotARemoteBridge()` | The `srcBridge` you named is not a bridge this chain has ever named for that source chain. |
| `UnknownLaunch(launchId)` | None of Pangea’s factories this bridge accepts knows that launch. |
| `AlreadyFilled()` | Somebody filled it first. One fill per order. |
| `AlreadySettled()` | The order has already been paid out. |
| `FeeExceedsAmount()` | The fee is not less than the amount. No real order looks like this. |
| `TooLateToFill()` | The message you named has already reached this chain. |
| A token transfer error | Your balance or your approval is too small. |

A fill that goes through emits `OrderFilled(orderId, relayer, to, paid)`.

## 5. Get repaid

You are repaid on the chain you filled on, in the token, when the order’s own message arrives and the bridge settles it: seconds after the order was posted. The bridge checks that the message came from the launch’s own token and the source chain’s own fast bridge before it pays anyone. `settlement(orderId)` on the destination bridge says how the order settled:

| Value | What happened |
| --- | --- |
| `0` | Not settled yet. |
| `1` | Nobody filled it. The recipient was paid everything that arrived, the relayer’s fee included. Event: `SettledSlow`. |
| `2` | A fill earned the fee. The filler was sent the whole `amount`. Event: `RelayerPaid`. |
| `3` | A fill did not earn the fee. The filler was sent back `amount - relayerFee`, what it paid out, and the fee went to the recipient. Events: `RelayerPaid` and `FeeReturned`. |
| `4` | Held. The recipient could not be paid on this chain (it is not an address here), so nobody could fill it and the bridge keeps it for the sender. Event: `Refundable(orderId, guid, srcEid, sender, amount)`. It is never a strike against a relayer. |

The same order can only arrive twice from Solana, when a sender reuses a nonce. The second arrival is paid to the recipient (`ArrivedAgain` and `SettledSlow`) and the first settlement stands: each arrival brought its own tokens, so none is refused.

## Sending a held order back

Anyone may send a held amount back to its sender on the chain it came from: `quoteRefund(guid)` gives LayerZero’s fee for the message back, and `refund(guid)`, paying that fee, sends it (`Refunded`). `refunds(guid)` shows what is held. The bridge screen offers this button itself when a transfer it sent is held.

**What earns the fee.** The fee pays for delivering early with your own tokens, so a fill earns it only when both of these are true:

- The `lzNonce` your fill named is the nonce of the message that settles the order.
- The settlement lands in a later block than your fill. A fill settled in its own block is repaid what it paid out and earns nothing.

`filledBy(orderId)` and `filledLzNonce(orderId)` return the filler and the nonce it named. So fill as soon as you are willing to.

## Ceilings

| Limit | What it bounds |
| --- | --- |
| `MAX_FAST_FEE_BPS` | The fast lane’s own fee can never be set above 100 basis points, 1%. Read the fees in force for a launch with `feesOf(launchId)`. |
| `maxRelayerFeeBps` | No order may pay a relayer more than this share of what arrives: 1000 basis points, 10%, unless the bridge’s admin sets a lower one, for all launches or one. An order above it is refused with `RelayerFeeTooHigh`. |
| `maxFeeBps` | The price board’s own ceiling on a published price, reported by every read of the board. |
| `minReceived` | The sender’s floor, written into the order. An order that would pay the recipient less is refused with `ReceivesTooLittle` before any token moves. |

## What gets you benched

When the app sends an order at your published price, it remembers that the price was yours. It looks at the order again from 2 minutes after it was posted and judges it once it has settled, or after 30 minutes if it still has not. An order nobody filled, or one you filled without earning the fee, is a strike against you.

After 3 strikes within 60 minutes your prices are not taken for 60 minutes, however low they are. Those are the defaults, and a deployment can set its own. Your sheets stay on the board, and `benched` in the board’s answer says until when.

- It is not a strike if any relayer’s fill earned the fee: the sender was served.
- It is not a strike if another address filled the order without earning the fee, because that fill shut you out.
- It is not a strike if you no longer held enough on the destination to pay it when it was judged.
- A fill does not clear earlier strikes. Each one stops counting 60 minutes after it was given.

## The risks you take

Relaying is a business you run with your own money. Nothing here promises that it pays.

- **A reorg on the source chain.** Between your fill and your repayment, the fill is unsecured. If the source chain reorganises and the order’s transaction is gone, the message never arrives and nothing repays you. How many blocks you wait before filling is your own trade between speed and that risk. If a reorg keeps the order but moves its message to a different LayerZero nonce, you are repaid what you paid out and earn nothing.
- **Filling before the order is in a block.** Acting on pending transactions is the fastest way to fill and the riskiest: a transaction that has not been mined is likelier to vanish than a confirmed one.
- **Inventory.** You hold the token on every chain you serve, and its price moves while you hold it. Your fee is paid in the token too.
- **Gas.** A fill that reverts still costs gas, and gas is paid in the chain’s native asset while your fee is paid in the token. A price that does not cover gas loses money slowly.
- **Time.** Your tokens are out until the order settles. Settlement is run by LayerZero’s executor; if it stalls, anyone may run it, and until someone does you wait.
- **Being shut out.** Only one fill per order is accepted. Another address can fill ahead of you, even with a nonce that earns it nothing, and that order is then closed to you.

## Contract addresses

One fast bridge per chain, shared by every launch: Ethereum, Base, Robinhood Chain and BSC (deployed 16 October 2026), then Solana. The full interface is the bridge’s ABI and `INTERFACE.md`. These are the addresses, and the endpoint ids a price sheet and a fill use:

| Chain | Fast bridge | Endpoint id | Chain id |
| --- | --- | --- | --- |
| Ethereum | 0xE6feCfb8520Ce9417beB569ef95d6b8304802897 | 30101 | 1 |
| Base | 0x51D658a71AEa38f123d4c73e42703Fb9C949900A | 30184 | 8453 |
| Robinhood Chain | 0xcC87BeDb95E4ac0cf20039442A964ADB390c02c4 | 30416 | 4663 |
| BSC | 0xd7C10226327451ac768512aC4A5035a9477f862a | 30102 | 56 |

> **Note:** [How a standard transfer works →](https://www.pangea.trade/app/docs#bridging)

Part of the Pangea documentation: https://www.pangea.trade/app/docs.md
