# Transaction Relaying

The `relay` action implements the MultiversX **Relayer V3** pattern. The client submits a user-signed transaction that includes a `relayer` field; the relayer service validates the shard assignment, adds its own Ed25519 signature (`relayerSignature`), and broadcasts the co-signed transaction to the P2P network via gossipsub.

This enables gasless transactions where the relayer, not the user, pays the network fee.

***

## How It Works

1. **Client determines the sender's shard** and selects the matching relayer address.
2. **Client builds the transaction** with the `relayer` field set to that address.
3. **User signs the transaction** — the signature must cover the `relayer` field.
4. **Client sends the `relay` action** over WebSocket.
5. **Relayer validates** that `shard_of(sender) == shard_of(relayer)`. Mismatches are rejected with `shard mismatch`.
6. **Relayer signs** using the BIP39-derived Ed25519 key for the matching shard.
7. **Relayer broadcasts** the fully co-signed transaction to validators.
8. **Relayer responds** with the transaction hash or an error.

{% hint style="warning" %}
The user must sign the transaction **including** the `relayer` field. If the transaction is signed before the `relayer` field is added, the user's signature will be invalid and the relay will fail.
{% endhint %}

***

## Shard Routing

MultiversX assigns accounts to shards based on the last byte of the public key decoded from the bech32 address. The relayer maintains a separate Ed25519 key per shard; the sender's shard must match the relayer's shard. A mismatched relayer address produces a `shard mismatch` error.

```javascript
// Relayer addresses by shard (mainnet)
const RELAYER_ADDRESSES = {
  0: 'erd1l0x0n5yxsfcy93gm0vyvx9m9f7cte9h9vuq4am33ugpw3d5r3hvqx6f59h',
  1: 'erd12yxd5phejzw83gn8qh6jfz6q9a0ekyyhkfd3c49r03mxw25l3a5swq3nf7',
  2: 'erd13jxp0yjh7gjvzgrg5mj7e8rzhn5lzcye45l0p6e5996d543r7vrq9e50za'
};

/**
 * Derive the shard index for a bech32 MultiversX address.
 * Requires a bech32 decode library (e.g., the `bech32` npm package).
 */
function getShardFromAddress(bech32Address) {
  const decoded = bech32.decode(bech32Address);
  const pubKeyBytes = bech32.fromWords(decoded.words);
  return pubKeyBytes[pubKeyBytes.length - 1] % 3;
}

function getRelayerForSender(senderAddress) {
  const shard = getShardFromAddress(senderAddress);
  const relayer = RELAYER_ADDRESSES[shard];
  if (!relayer) throw new Error(`No relayer configured for shard ${shard}`);
  return relayer;
}
```

***

## Transaction Object

The `relayer` field is mandatory for the `relay` action.

```json
{
  "nonce": 42,
  "value": "1000000000000000000",
  "receiver": "erd1...",
  "sender": "erd1...",
  "gasPrice": 1000000000,
  "gasLimit": 50000,
  "data": "base64encodeddata",
  "chainID": "1",
  "version": 2,
  "relayer": "erd1..."
}
```

| Field       | Type   | Required | Description                                                                  |
| ----------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `nonce`     | number | Yes      | Sender's current transaction counter                                         |
| `value`     | string | Yes      | EGLD amount in atomic units                                                  |
| `receiver`  | string | Yes      | Receiver's bech32 address                                                    |
| `sender`    | string | Yes      | Sender's bech32 address                                                      |
| `gasPrice`  | number | Yes      | Gas price in atomic units. Minimum: `1000000000`                             |
| `gasLimit`  | number | Yes      | Maximum gas units                                                            |
| `data`      | string | No       | Base64-encoded smart contract call payload                                   |
| `chainID`   | string | Yes      | `"1"` for mainnet                                                            |
| `version`   | number | Yes      | Transaction version. Use `2`                                                 |
| `signature` | string | Yes      | Hex-encoded user signature covering the full transaction including `relayer` |
| `relayer`   | string | **Yes**  | Shard-specific relayer address. Must match the sender's shard.               |

***

## What the User Signs vs What Gets Broadcast

The user signs the transaction body before `relayerSignature` exists. The relayer appends `relayerSignature` to the already-signed object.

**Signed by the user:**

```json
{
  "nonce": 42,
  "value": "0",
  "receiver": "erd1contract...",
  "sender": "erd1user...",
  "gasPrice": 1000000000,
  "gasLimit": 50000,
  "data": "c3dhcA==",
  "chainID": "1",
  "version": 2,
  "relayer": "erd1l0x0n5yxsfcy93gm0vyvx9m9f7cte9h9vuq4am33ugpw3d5r3hvqx6f59h"
}
```

**Broadcast to the network (relayer adds `relayerSignature`):**

```json
{
  "nonce": 42,
  "value": "0",
  "receiver": "erd1contract...",
  "sender": "erd1user...",
  "gasPrice": 1000000000,
  "gasLimit": 50000,
  "data": "c3dhcA==",
  "chainID": "1",
  "version": 2,
  "signature": "user-signature-hex...",
  "relayer": "erd1l0x0n5yxsfcy93gm0vyvx9m9f7cte9h9vuq4am33ugpw3d5r3hvqx6f59h",
  "relayerSignature": "relayer-signature-hex..."
}
```

***

## Relay Request

```javascript
ws.send(JSON.stringify({
  action: 'relay',
  requestId: 'relay-001',
  tx: {
    nonce: 42,
    value: '0',
    receiver: 'erd1qqqqqqqqqqqqqpgq...',
    sender: 'erd1user...',
    gasPrice: 1000000000,
    gasLimit: 50000,
    data: 'c3dhcA==',
    chainID: '1',
    version: 2,
    signature: 'user-signature-hex...',
    relayer: 'erd1l0x0n5yxsfcy93gm0vyvx9m9f7cte9h9vuq4am33ugpw3d5r3hvqx6f59h'
  }
}));
```

***

## Response

### Success

```json
{
  "action": "relay",
  "status": "completed",
  "requestId": "relay-001",
  "hashes": ["abc123def456..."],
  "failed": []
}
```

### Partial Success (Batch)

```json
{
  "action": "relay",
  "status": "partial",
  "requestId": "relay-batch-001",
  "hashes": ["hash1..."],
  "failed": [
    { "index": 1, "reason": "shard mismatch: sender_shard=0, relayer_shard=1" }
  ]
}
```

### Response Fields

| Field       | Type   | Description                                              |
| ----------- | ------ | -------------------------------------------------------- |
| `action`    | string | Always `"relay"`                                         |
| `status`    | string | `"completed"`, `"partial"`, or `"failed"`                |
| `requestId` | string | Echoed from the request                                  |
| `hashes`    | array  | Transaction hashes for successfully relayed transactions |
| `failed`    | array  | Objects with `index` and `reason` for each failure       |

***

## Error Types

| Error String                                      | Cause                                                             | Resolution                                                        |
| ------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- |
| `missing relayer`                                 | The `relayer` field is absent from the transaction object         | Add the `relayer` field before signing                            |
| `shard mismatch: sender_shard=X, relayer_shard=Y` | The sender's shard and the relayer's shard do not match           | Use `getRelayerForSender()` to select the correct relayer address |
| `unconfigured relayer`                            | The `relayer` address is not one of the recognized shard relayers | Use an address from the relayer addresses table above             |
| `signing capacity exceeded, try again later`      | The relayer service is at maximum signing throughput              | Implement exponential backoff and retry                           |

***

## Complete Integration Example

```javascript
import bech32 from 'bech32';

const RELAYER_ADDRESSES = {
  0: 'erd1l0x0n5yxsfcy93gm0vyvx9m9f7cte9h9vuq4am33ugpw3d5r3hvqx6f59h',
  1: 'erd12yxd5phejzw83gn8qh6jfz6q9a0ekyyhkfd3c49r03mxw25l3a5swq3nf7',
  2: 'erd13jxp0yjh7gjvzgrg5mj7e8rzhn5lzcye45l0p6e5996d543r7vrq9e50za'
};

class RelayClient {
  constructor() {
    this.ws = new WebSocket('wss://relayer.xoxno.com/ws');
    this.pending = new Map();
    this.counter = 0;

    this.ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      if (msg.action === 'relay' && msg.requestId) {
        const resolve = this.pending.get(msg.requestId);
        if (resolve) {
          this.pending.delete(msg.requestId);
          resolve(msg);
        }
      }
    };
  }

  ready() {
    return new Promise((resolve) => {
      if (this.ws.readyState === WebSocket.OPEN) return resolve();
      this.ws.onopen = resolve;
    });
  }

  getShardFromAddress(bech32Address) {
    const decoded = bech32.decode(bech32Address);
    const pubKeyBytes = bech32.fromWords(decoded.words);
    return pubKeyBytes[pubKeyBytes.length - 1] % 3;
  }

  getRelayerForSender(senderAddress) {
    const shard = this.getShardFromAddress(senderAddress);
    const relayer = RELAYER_ADDRESSES[shard];
    if (!relayer) throw new Error(`No relayer for shard ${shard}`);
    return relayer;
  }

  async relay(signedTx, timeoutMs = 30000) {
    const requestId = `relay-${++this.counter}`;

    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        this.pending.delete(requestId);
        reject(new Error('Relay timeout'));
      }, timeoutMs);

      this.pending.set(requestId, (result) => {
        clearTimeout(timer);
        resolve(result);
      });

      this.ws.send(JSON.stringify({ action: 'relay', requestId, tx: signedTx }));
    });
  }

  /**
   * Build a gasless transaction, sign it with the user's wallet, and relay it.
   *
   * @param {object} txParams - nonce, value, receiver, sender, gasLimit, data, chainID
   * @param {Function} sign - async function that takes a transaction object and returns signature hex
   * @returns {string} transaction hash
   */
  async executeGasless(txParams, sign) {
    const relayer = this.getRelayerForSender(txParams.sender);

    // Build transaction with relayer field BEFORE signing
    const tx = {
      ...txParams,
      gasPrice: 1000000000,
      version: 2,
      relayer
    };

    // User signs the complete transaction including the relayer field
    tx.signature = await sign(tx);

    const result = await this.relay(tx);

    if (result.status === 'failed') {
      throw new Error(result.failed[0]?.reason ?? 'Relay failed');
    }

    return result.hashes[0];
  }
}

// Usage
const client = new RelayClient();
await client.ready();

const hash = await client.executeGasless(
  {
    nonce: 42,
    value: '0',
    receiver: 'erd1qqqqqqqqqqqqqpgq...',
    sender: 'erd1user...',
    gasLimit: 50000,
    data: 'c3dhcA==',
    chainID: '1'
  },
  async (tx) => userWallet.sign(tx)
);

console.log('Gasless transaction hash:', hash);
```

***

## Relay vs Broadcast

| Aspect          | Relay                           | Broadcast                         |
| --------------- | ------------------------------- | --------------------------------- |
| `relayer` field | Required                        | Not used                          |
| User signs      | Transaction including `relayer` | Transaction without `relayer`     |
| Relayer adds    | `relayerSignature`              | Nothing                           |
| Gas paid by     | Relayer                         | User                              |
| Use case        | Gasless / meta-transactions     | Standard self-signed transactions |

***

## Related Pages

* [Transaction Broadcasting](https://docs.xoxno.com/developers/relayer-api/transaction-broadcasting) — Submit fully signed transactions
* [Account Subscriptions](https://docs.xoxno.com/developers/relayer-api/account-subscriptions) — Track nonce after relay
* [Gas Statistics](https://docs.xoxno.com/developers/relayer-api/gas-stats) — Set an appropriate `gasLimit`
* [Error Reference](https://github.com/XOXNO/docs/blob/main/developers/relayer-api/error-reference.md) — All error codes and resolutions
