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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.xoxno.com/developers/relayer-api/transaction-relaying.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
