> For the complete documentation index, see [llms.txt](https://sodex.com/documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sodex.com/documentation/for-developers/developers/valuechain-evm/relayer.md).

# Transaction Relayer

The Relayer broadcasts supported ValueChain contract calls for an integration and pays native SOSO gas. It is available on **Mainnet only**. There is no Testnet Relayer.

Submit `target`, ABI-encoded `calldata`, and your assigned `integration_id`. Do not send a signed raw transaction. On chain, **`msg.sender` is the Relayer wallet**, not your user and not `integration_id`. Functions that require the user as `msg.sender` will not work unless the calldata already carries the user's authorization (permit, `onBehalf`, or equivalent).

Use Relayer when the contract is on your allowlist and you want sponsored gas. If you have native SOSO and must be `msg.sender`, use [`eth_sendRawTransaction`](/documentation/for-developers/api-reference/json-rpc/eth-sendrawtransaction.md) instead. Relayer does not attach `msg.value`, does not pay ERC-20 amounts, and does not set allowances. It chooses nonce and EIP-1559 fees; you cannot set `gasPrice` or `value`.

## Integration flow

1. [Register an integration ID](#register-an-integration-id) and obtain approval for the contracts your integration needs.
2. Encode the target contract call locally.
3. Submit the target and calldata to the Relayer.
4. Persist the returned transaction hash.
5. Poll transaction status until it is confirmed or confirmed with an execution failure. If it is replaced, continue with the replacement hash.

Only registered integrations and allowed contracts are accepted. The estimated gas for a submitted call must remain within the service limit.

## Register an integration ID

Before submitting transactions, contact a moderator (Mod) in the official SoDEX Discord or Telegram community to request Relayer access. Relayer is Mainnet only.

1. Provide the target contract addresses, calls, and expected submission volume.
2. Ask the Mod to add your integration and confirm the assigned `integration_id` and approved target contracts.
3. Set `INTEGRATION_ID` to the confirmed value in your application and pass it as `integration_id` in each submission.

A locally chosen integration name is not sufficient; wait for the Mod to confirm registration before submitting transactions.

## Access Requirements

Submission requires an allowed integration ID, an allowed EIP-55 target address, valid calldata, and a gas estimate within the service limit. The request body is `target`, `calldata`, and `integration_id` only. `POST /tx` and `GET /tx/status` do not require an admin token; contract-level signatures and permissions remain necessary.

## Submission Recovery

Persist the integration ID, target, calldata, and submission time before sending. Keep calldata containing permits in protected storage; do not publish signatures or private keys in support logs.

| Observed result                                                       | Next action                                                                                                               |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Explicit validation or authorization rejection                        | Correct the stated problem before submitting again.                                                                       |
| Successful response with `result.tx_hash`                             | Save the hash and query status. Do not submit the same call again to obtain status.                                       |
| Status reports replacement                                            | Save `replaced_by_hash` and continue querying that hash, retaining the original hash for correlation.                     |
| Timeout while querying a known hash                                   | Resume the status query with the saved hash.                                                                              |
| `transaction not found` or `status_code` `0`                          | This hash is not a Relayer record (wrong hash or not submitted here). Do not treat it as an on-chain failure.             |
| Submission timeout, lost response, or success response without a hash | Treat submission as unknown. Stop automatic resubmission and reconcile with the operator and the target contract's state. |

The documented status endpoint requires `tx_hash`; the submission body has no client request ID or idempotency key. `integration_id` identifies the integration, not a unique request. Without a hash, there is no documented client-side lookup by calldata or submission time.

For an unknown submission, provide the operator with the integration ID, target, approximate submission time, and any response/error received. Verify the intended operation against the contract's state or events before deciding whether another submission is safe. Permit expiration, a client timeout, or absence from a recent query alone does not prove that the first call was never executed.

## Rate limits and errors

Transaction submission is limited per source IP across second, five-minute, and hourly windows. Default limits are `2`, `10`, and `50` submissions respectively. Status queries are not subject to this submission limit.

Relayer responses include a string `status`, `message`, and `result`. Validation and authorization errors can be returned in a successful HTTP response, so always evaluate the response body instead of relying on the HTTP status alone.

{% content-ref url="/pages/n2KMAHU4m5idw1gap6xC" %}
[Relayer API](/documentation/for-developers/api-reference/relayer-api.md)
{% endcontent-ref %}

## Submit and track a transaction

Base URL: `https://mainnet-gw.sodex.dev/api/v1/relayer`. Use an approved integration ID, an EIP-55 checksummed target, and calldata encoded with the target contract's ABI. Any required contract-level authorization must be included in the call.

```bash
curl -sS https://mainnet-gw.sodex.dev/api/v1/relayer/tx \
  -H 'Content-Type: application/json' \
  --data '{"target":"YOUR_CHECKSUMMED_CONTRACT_ADDRESS","calldata":"YOUR_ABI_ENCODED_CALLDATA","integration_id":"YOUR_APPROVED_INTEGRATION_ID"}'
```

Save `result.tx_hash`, then poll status. Do not submit the same call again to obtain status.

```bash
curl -sS --get https://mainnet-gw.sodex.dev/api/v1/relayer/tx/status \
  --data-urlencode 'tx_hash=YOUR_RELAYED_TRANSACTION_HASH'
```

`status_code` `3` (confirmed) and `4` (confirmed with execution failure) mean the transaction is included. That is the [Included](/documentation/for-developers/developers/valuechain-evm/transaction-finality.md) stage; do not apply extra confirmation depth. `3` is EVM success (`status` `"0x1"`); `4` is EVM revert (`status` `"0x0"`). Follow `replaced_by_hash` when `status_code` is `2`.

The Node.js 18+ example below submits once on Mainnet and follows replacement hashes. Save as `relay.mjs`. Running it without `TX_HASH` broadcasts a transaction.

```bash
export INTEGRATION_ID='YOUR_APPROVED_INTEGRATION_ID'
export TARGET='YOUR_CHECKSUMMED_CONTRACT_ADDRESS'
export CALLDATA='YOUR_ABI_ENCODED_CALLDATA'
node relay.mjs
```

To query an existing transaction without submitting another call:

```bash
TX_HASH='YOUR_RELAYED_TRANSACTION_HASH' node relay.mjs
```

```javascript
const baseURL = "https://mainnet-gw.sodex.dev/api/v1/relayer";

async function request(path, options = {}) {
  const response = await fetch(`${baseURL}${path}`, {
    ...options,
    signal: AbortSignal.timeout(15000),
  });
  const body = await response.json();
  if (!response.ok || body.status !== "1") {
    throw new Error(body.message || `HTTP ${response.status}`);
  }
  return body.result;
}

async function main() {
  let hash = process.env.TX_HASH;
  if (!hash) {
    const { TARGET: target, CALLDATA: calldata, INTEGRATION_ID: integration_id } = process.env;
    if (!target || !calldata || !integration_id) {
      throw new Error("Set TARGET, CALLDATA, and INTEGRATION_ID, or set TX_HASH to resume");
    }
    const result = await request("/tx", {
      method: "POST",
      headers: { "Content-Type": "application/json", "Accept": "application/json" },
      body: JSON.stringify({ target, calldata, integration_id }),
    });
    hash = result.tx_hash;
    if (!hash) throw new Error("Submission returned no transaction hash; reconcile before resubmitting");
    console.log("Submitted transaction:", hash); // Retain this hash for later queries.
  }

  for (let attempt = 0; attempt < 60; attempt++) {
    const tx = await request(`/tx/status?tx_hash=${encodeURIComponent(hash)}`);
    console.log(hash, tx.status);
    switch (tx.status_code) {
      case 1: // Pending
        break;
      case 2: // Replaced
        if (!tx.replaced_by_hash) throw new Error("Replacement hash unavailable; re-query this transaction");
        hash = tx.replaced_by_hash;
        console.log("Replacement transaction:", hash);
        break;
      case 3: // Confirmed
        console.log("Transaction confirmed:", hash);
        return;
      case 4: // Confirmed with execution failure
        throw new Error(`Transaction execution failed: ${hash}`);
      default:
        throw new Error(`Unknown transaction status: ${tx.status_code}; hash: ${hash}`);
    }
    await new Promise((resolve) => setTimeout(resolve, 5000));
  }
  throw new Error(`Polling limit reached. Resume with TX_HASH=${hash}`);
}

main().catch((error) => {
  console.error(error.message);
  process.exitCode = 1;
});
```

The example submits once and follows replacement hashes. A polling limit or request timeout does not establish transaction failure. Resume status queries using the last recorded hash; resolve an ambiguous submission before sending another call.
