| name | jb-relayr |
| description | Relayr API reference for multi-chain transaction bundling. Pay gas on one chain, execute on many. Used for omnichain deployments, cross-chain operations, and meta-transactions. |
Relayr: Multi-Chain Transaction Bundling
Relayr is a meta-transaction relay service by 0xBASED that bundles transactions across chains. Users sign transactions for multiple chains, pay gas on one chain, and Relayr relayers execute on all others.
Overview
1. User signs ERC2771 forward requests for each target chain
2. POST to Relayr API to get quote with payment options
3. User selects which chain to pay on
4. User sends single payment transaction
5. Relayr relayers execute on all other chains
6. Poll for bundle completion status
API Base URLs
Production API: https://api.relayr.ba5ed.com
Dashboard: https://relayr.ba5ed.com
Authentication
No API key required. Relayr is permissionless. Anyone can submit bundles.
API Endpoints
1. Create Bundle Quote
POST /v1/bundle/prepaid
Creates a bundle of transactions and returns payment options.
Request Body:
{
"transactions": [
{
"chain": 1,
"target": "0x...",
"data": "0x...",
"value": "0"
}
],
"virtual_nonce_mode": "Disabled"
}
Fields:
| Field | Type | Description |
|---|
chain | number | Target chain ID |
target | string | Contract to call (usually ERC2771Forwarder) |
data | string | Encoded calldata |
value | string | ETH value in wei (usually "0" for meta-txs) |
virtual_nonce_mode | string | "Disabled" or "Enabled" for sequential ordering |
Response:
{
"bundle_uuid": "550e8400-e29b-41d4-a716-446655440000",
"payment_info": [
{
"chain": 1,
"target": "0x...",
"amount": "1234567890",
"calldata": "0x..."
},
{
"chain": 10,
"target": "0x...",
"amount": "987654321",
"calldata": "0x..."
}
],
"per_txn": [
{
"txn_uuid": "...",
"chain": 1,
"gas_cost"
Response Fields:
| Field | Description |
|---|
bundle_uuid | Unique identifier for tracking |
payment_info | Array of payment options (one per supported chain) |
payment_info[].chain | Chain ID to pay on |
payment_info[].target | Address to send payment to |
payment_info[].amount | Wei amount to pay |
payment_info[].calldata | Transaction data for payment |
per_txn | Per-transaction details |
txn_uuids | Array of transaction UUIDs |
2. Get Bundle Status
GET /v1/bundle/{bundle_uuid}
Poll this endpoint to check execution status.
Response:
{
"bundle_uuid": "550e8400-e29b-41d4-a716-446655440000",
"payment_received": true,
"payment_chain": 1,
"transactions": [
{
"txn_uuid": "...",
"chain": 1,
"status": "Success",
"tx_hash": "0x...",
"block_number": 12345678
},
{
"txn_uuid": "...",
"chain": 10,
"status": "Pending",
"tx_hash": null,
"block_number"
3. Get Transaction Status
GET /v1/transaction/{txn_uuid}
Get status of individual transaction within a bundle.
Transaction Status Values
| Status | Meaning |
|---|
Quoted | Bundle created, awaiting payment |
PaymentReceived | Payment confirmed, queued for execution |
Pending | Transaction submitted, awaiting confirmation |
Success | Transaction confirmed on-chain |
Completed | Alias for Success |
Failed | Transaction reverted |
Expired | Payment not received within deadline (48h) |
ERC2771 Forward Request Format
Relayr uses ERC2771 meta-transactions. The forwarder contract validates signatures and executes calls with the original sender preserved via _msgSender().
TypedData Domain
const domain = {
name: 'Juicebox',
version: '1',
chainId: 1,
verifyingContract: '0x...'
};
TypedData Types
const types = {
ForwardRequest: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'gas', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint48' },
{ name: 'data', type: 'bytes' }
]
};
Message Fields
| Field | Type | Description |
|---|
from | address | Original signer address |
to | address | Target contract to call |
value | uint256 | ETH to forward (usually 0) |
gas | uint256 | Gas limit for execution |
nonce | uint256 | User's forwarder nonce (query from contract) |
deadline | uint48 | Unix timestamp expiry (max 48 hours from now) |
data | bytes | Encoded function calldata |
Getting the Nonce
Query the forwarder contract for the user's current nonce:
const forwarder = new ethers.Contract(forwarderAddress, [
'function nonces(address) view returns (uint256)'
], provider);
const nonce = await forwarder.nonces(userAddress);
Complete JavaScript Example
import { ethers } from 'ethers';
const RELAYR_API = 'https://api.relayr.ba5ed.com';
const FORWARDER = {
1: '0x...',
10: '0x...',
8453: '0x...',
42161: '0x...'
};
async function executeOmnichain(signer, targetChains, targetContract, calldata) {
const address = await signer.getAddress();
const signedRequests = [];
for (const chainId of targetChains) {
const nonce = await getNonce(chainId, address);
const domain = {
name: 'Juicebox',
version: '1',
chainId: chainId,
verifyingContract: FORWARDER[chainId]
};
const types = {
: [
{ : , : },
{ : , : },
{ : , : },
{ : , : },
{ : , : },
{ : , : },
{ : , : }
]
};
deadline = .(.() / ) + * * ;
message = {
: address,
: targetContract,
: ,
: ,
: nonce,
: deadline,
: calldata
};
signature = signer.(domain, types, message);
forwarderAbi = [
];
iface = ethers.(forwarderAbi);
encodedData = iface.(, [
[message., message., message., message., message., message., message.],
signature
]);
signedRequests.({
: chainId,
: [chainId],
: encodedData,
:
});
}
quoteResponse = (, {
: ,
: { : },
: .({
: signedRequests,
:
})
});
(!quoteResponse.) {
();
}
quote = quoteResponse.();
.(, quote.);
.(, quote..(
));
quote;
}
() {
tx = signer.({
: paymentInfo.,
: paymentInfo.,
: paymentInfo.
});
.(, tx.);
tx.();
.();
tx;
}
() {
() {
response = ();
status = response.();
(onUpdate) (status);
allDone = status..(
[, , ].(tx.)
);
(allDone) {
anyFailed = status..( tx. === );
(anyFailed) {
failed = status..( tx. === );
();
}
status;
}
.(, status..( ).());
( (r, ));
}
}
() {
rpcUrls = {
: ,
: ,
: ,
:
};
provider = ethers.(rpcUrls[chainId]);
forwarder = ethers.(
[chainId],
[],
provider
);
forwarder.(address);
}
() {
provider = ethers.(.);
signer = provider.();
targetChains = [, , ];
targetContract = ;
calldata = ;
quote = (signer, targetChains, targetContract, calldata);
cheapest = quote..(
(p.) < (min.) ? p : min
);
.();
(signer, cheapest);
result = (quote., {
.(, status..( t.));
});
.();
.(, result..( ));
}
RelayrClient Class
class RelayrClient {
constructor(apiUrl = 'https://api.relayr.ba5ed.com') {
this.api = apiUrl;
}
async createBundle(transactions) {
const response = await fetch(`${this.api}/v1/bundle/prepaid`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
transactions,
virtual_nonce_mode: 'Disabled'
})
});
if (!response.ok) {
throw new Error(`Relayr quote failed: ${response.statusText}`);
}
return await response.json();
}
async getBundleStatus(bundleUuid) {
const response = await fetch(`${this.api}/v1/bundle/${bundleUuid}`);
return await response.json();
}
async () {
response = ();
response.();
}
() {
{ pollInterval = , timeout = , onUpdate } = options;
startTime = .();
(.() - startTime < timeout) {
status = .(bundleUuid);
(onUpdate) (status);
allDone = status..(
[, , ].(tx.)
);
(allDone) {
status;
}
( (r, pollInterval));
}
();
}
}
Error Handling
| Error | Cause | Solution |
|---|
Quote failed | Invalid transaction data | Check calldata encoding |
Nonce too low | Nonce already used | Query fresh nonce from forwarder |
Deadline expired | Signature expired | Re-sign with new deadline |
Insufficient payment | Gas price changed | Request new quote |
Transaction reverted | Contract execution failed | Debug on target chain explorer |
Best Practices
- Always quote fresh - Gas prices fluctuate, get a new quote immediately before paying
- Set reasonable gas limits - Too low causes reverts, too high is expensive
- Use 48-hour deadlines - Maximum allowed, gives time for execution
- Handle chain switching - Users may need to switch wallets to pay on different chain
- Poll with backoff - Start at 2-3s, increase to 10s after 30 seconds
- Verify nonces - Always query fresh nonce from forwarder contract
- Handle partial failures - Some chains may succeed while others fail
Use Cases
- Omnichain project deployment - Deploy Juicebox project to multiple chains at once
- Cross-chain configuration - Update settings across all chains simultaneously
- Multi-chain token operations - Coordinate token actions across networks
- Batch transactions - Execute multiple transactions with single payment
Related Skills
/jb-bendystraw - Query cross-chain data after deployment
/jb-omnichain-ui - Build UIs using Relayr
/jb-project - Project deployment configurations