skills$openclaw/jb-relayr
mejango6.9k

by mejango

jb-relayr – OpenClaw Skill

jb-relayr is an OpenClaw Skills integration for coding workflows. 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.

6.9k stars9.4k forksSecurity L1
Updated Feb 7, 2026Created Feb 7, 2026coding

Skill Snapshot

namejb-relayr
descriptionRelayr 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. OpenClaw Skills integration.
ownermejango
repositorymejango/juicypath: jb-relayr
languageMarkdown
licenseMIT
topics
securityL1
installopenclaw add @mejango/juicy:jb-relayr
last updatedFeb 7, 2026

Maintainer

mejango

mejango

Maintains jb-relayr in the OpenClaw Skills directory.

View GitHub profile
File Explorer
1 files
jb-relayr
SKILL.md
13.9 KB
SKILL.md

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:

FieldTypeDescription
chainnumberTarget chain ID
targetstringContract to call (usually ERC2771Forwarder)
datastringEncoded calldata
valuestringETH value in wei (usually "0" for meta-txs)
virtual_nonce_modestring"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": "500000",
      "status": "Quoted"
    }
  ],
  "txn_uuids": ["uuid1", "uuid2"]
}

Response Fields:

FieldDescription
bundle_uuidUnique identifier for tracking
payment_infoArray of payment options (one per supported chain)
payment_info[].chainChain ID to pay on
payment_info[].targetAddress to send payment to
payment_info[].amountWei amount to pay
payment_info[].calldataTransaction data for payment
per_txnPer-transaction details
txn_uuidsArray 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": null
    }
  ]
}

3. Get Transaction Status

GET /v1/transaction/{txn_uuid}

Get status of individual transaction within a bundle.


Transaction Status Values

StatusMeaning
QuotedBundle created, awaiting payment
PaymentReceivedPayment confirmed, queued for execution
PendingTransaction submitted, awaiting confirmation
SuccessTransaction confirmed on-chain
CompletedAlias for Success
FailedTransaction reverted
ExpiredPayment 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',           // Or actual contract name
  version: '1',
  chainId: 1,                 // Target chain ID
  verifyingContract: '0x...'  // ERC2771Forwarder address
};

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

FieldTypeDescription
fromaddressOriginal signer address
toaddressTarget contract to call
valueuint256ETH to forward (usually 0)
gasuint256Gas limit for execution
nonceuint256User's forwarder nonce (query from contract)
deadlineuint48Unix timestamp expiry (max 48 hours from now)
databytesEncoded 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';

// ERC2771Forwarder addresses (same on all chains - deterministic deployment)
const FORWARDER = {
  1: '0x...', // Ethereum mainnet
  10: '0x...', // Optimism
  8453: '0x...', // Base
  42161: '0x...' // Arbitrum
};

/**
 * Deploy or execute across multiple chains with single payment
 */
async function executeOmnichain(signer, targetChains, targetContract, calldata) {
  const address = await signer.getAddress();
  const signedRequests = [];

  // Step 1: Sign forward request for each chain
  for (const chainId of targetChains) {
    // Get nonce from forwarder contract
    const nonce = await getNonce(chainId, address);

    const domain = {
      name: 'Juicebox',
      version: '1',
      chainId: chainId,
      verifyingContract: FORWARDER[chainId]
    };

    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' }
      ]
    };

    // 48-hour deadline (maximum allowed)
    const deadline = Math.floor(Date.now() / 1000) + 48 * 60 * 60;

    const message = {
      from: address,
      to: targetContract,
      value: '0',
      gas: '1000000',
      nonce: nonce,
      deadline: deadline,
      data: calldata
    };

    // Sign the typed data
    const signature = await signer.signTypedData(domain, types, message);

    // Encode for forwarder.execute()
    const forwarderAbi = [
      'function execute((address from, address to, uint256 value, uint256 gas, uint256 nonce, uint48 deadline, bytes data) request, bytes signature)'
    ];
    const iface = new ethers.Interface(forwarderAbi);
    const encodedData = iface.encodeFunctionData('execute', [
      [message.from, message.to, message.value, message.gas, message.nonce, message.deadline, message.data],
      signature
    ]);

    signedRequests.push({
      chain: chainId,
      target: FORWARDER[chainId],
      data: encodedData,
      value: '0'
    });
  }

  // Step 2: Get quote from Relayr
  const quoteResponse = await fetch(`${RELAYR_API}/v1/bundle/prepaid`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      transactions: signedRequests,
      virtual_nonce_mode: 'Disabled'
    })
  });

  if (!quoteResponse.ok) {
    throw new Error(`Quote failed: ${quoteResponse.statusText}`);
  }

  const quote = await quoteResponse.json();
  console.log('Bundle UUID:', quote.bundle_uuid);
  console.log('Payment options:', quote.payment_info.map(p =>
    `Chain ${p.chain}: ${ethers.formatEther(p.amount)} ETH`
  ));

  return quote;
}

/**
 * Execute payment on chosen chain
 */
async function payForBundle(signer, paymentInfo) {
  const tx = await signer.sendTransaction({
    to: paymentInfo.target,
    value: paymentInfo.amount,
    data: paymentInfo.calldata
  });

  console.log('Payment tx:', tx.hash);
  await tx.wait();
  console.log('Payment confirmed');

  return tx;
}

/**
 * Poll until all transactions complete
 */
async function waitForCompletion(bundleUuid, onUpdate) {
  while (true) {
    const response = await fetch(`${RELAYR_API}/v1/bundle/${bundleUuid}`);
    const status = await response.json();

    if (onUpdate) onUpdate(status);

    const allDone = status.transactions.every(
      tx => ['Success', 'Completed', 'Failed'].includes(tx.status)
    );

    if (allDone) {
      const anyFailed = status.transactions.some(tx => tx.status === 'Failed');
      if (anyFailed) {
        const failed = status.transactions.filter(tx => tx.status === 'Failed');
        throw new Error(`Transactions failed on chains: ${failed.map(t => t.chain).join(', ')}`);
      }
      return status;
    }

    console.log('Status:', status.transactions.map(t => `Chain ${t.chain}: ${t.status}`).join(', '));
    await new Promise(r => setTimeout(r, 3000));
  }
}

/**
 * Helper: Get nonce from forwarder on specific chain
 */
async function getNonce(chainId, address) {
  const rpcUrls = {
    1: 'https://eth.llamarpc.com',
    10: 'https://mainnet.optimism.io',
    8453: 'https://mainnet.base.org',
    42161: 'https://arb1.arbitrum.io/rpc'
  };

  const provider = new ethers.JsonRpcProvider(rpcUrls[chainId]);
  const forwarder = new ethers.Contract(
    FORWARDER[chainId],
    ['function nonces(address) view returns (uint256)'],
    provider
  );

  return await forwarder.nonces(address);
}

/**
 * Complete flow example
 */
async function main() {
  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();

  // Example: Deploy to Ethereum, Optimism, and Base
  const targetChains = [1, 10, 8453];
  const targetContract = '0x...'; // JBController
  const calldata = '0x...'; // launchProjectFor encoded

  // Get quote
  const quote = await executeOmnichain(signer, targetChains, targetContract, calldata);

  // Find cheapest payment option
  const cheapest = quote.payment_info.reduce((min, p) =>
    BigInt(p.amount) < BigInt(min.amount) ? p : min
  );

  console.log(`Paying ${ethers.formatEther(cheapest.amount)} ETH on chain ${cheapest.chain}`);

  // Switch chain if needed and pay
  // ... chain switching logic ...
  await payForBundle(signer, cheapest);

  // Wait for all chains to complete
  const result = await waitForCompletion(quote.bundle_uuid, (status) => {
    console.log('Update:', status.transactions.map(t => t.status));
  });

  console.log('All transactions complete!');
  console.log('Tx hashes:', result.transactions.map(t => `${t.chain}: ${t.tx_hash}`));
}

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 getTransactionStatus(txnUuid) {
    const response = await fetch(`${this.api}/v1/transaction/${txnUuid}`);
    return await response.json();
  }

  async waitForCompletion(bundleUuid, options = {}) {
    const { pollInterval = 3000, timeout = 600000, onUpdate } = options;
    const startTime = Date.now();

    while (Date.now() - startTime < timeout) {
      const status = await this.getBundleStatus(bundleUuid);

      if (onUpdate) onUpdate(status);

      const allDone = status.transactions.every(
        tx => ['Success', 'Completed', 'Failed'].includes(tx.status)
      );

      if (allDone) {
        return status;
      }

      await new Promise(r => setTimeout(r, pollInterval));
    }

    throw new Error('Bundle completion timeout');
  }
}

Error Handling

ErrorCauseSolution
Quote failedInvalid transaction dataCheck calldata encoding
Nonce too lowNonce already usedQuery fresh nonce from forwarder
Deadline expiredSignature expiredRe-sign with new deadline
Insufficient paymentGas price changedRequest new quote
Transaction revertedContract execution failedDebug on target chain explorer

Best Practices

  1. Always quote fresh - Gas prices fluctuate, get a new quote immediately before paying
  2. Set reasonable gas limits - Too low causes reverts, too high is expensive
  3. Use 48-hour deadlines - Maximum allowed, gives time for execution
  4. Handle chain switching - Users may need to switch wallets to pay on different chain
  5. Poll with backoff - Start at 2-3s, increase to 10s after 30 seconds
  6. Verify nonces - Always query fresh nonce from forwarder contract
  7. Handle partial failures - Some chains may succeed while others fail

  • 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

  • /jb-bendystraw - Query cross-chain data after deployment
  • /jb-omnichain-ui - Build UIs using Relayr
  • /jb-project - Project deployment configurations
README.md

No README available.

Permissions & Security

Security level L1: Low-risk skills with minimal permissions. Review inputs and outputs before running in production.

Requirements

  • OpenClaw CLI installed and configured.
  • Language: Markdown
  • License: MIT
  • Topics:

FAQ

How do I install jb-relayr?

Run openclaw add @mejango/juicy:jb-relayr in your terminal. This installs jb-relayr into your OpenClaw Skills catalog.

Does this skill run locally or in the cloud?

OpenClaw Skills execute locally by default. Review the SKILL.md and permissions before running any skill.

Where can I verify the source code?

The source repository is available at https://github.com/openclaw/skills/tree/main/skills/mejango/juicy. Review commits and README documentation before installing.