> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rhinestone.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Relay a Safe transaction

> Collect Safe owner signatures, relay the encoded Safe transaction, and track its onchain outcome.

Relay an existing Safe transaction through Rhinestone's native Safe endpoint. This flow submits owner-signed `execTransaction` calldata directly; it does not create or submit a Rhinestone SDK intent.

## Construct and sign the Safe transaction

A sponsored relay requires all five Safe gas-refund fields to be zero. The Safe can still send native value in its inner call: `value` comes from the Safe's balance, not from the relayer or sponsorship budget.

```ts theme={null}
import Safe from "@safe-global/protocol-kit";
import {
  createPublicClient,
  http,
  parseEther,
  zeroAddress,
  type Hex,
} from "viem";

const rpcUrl = process.env.RPC_URL;
const apiKey = process.env.RHINESTONE_API_KEY;
const safeAddress = process.env.SAFE_ADDRESS;
const ownerKeys = (process.env.SAFE_OWNER_PRIVATE_KEYS ?? "")
  .split(",")
  .filter(Boolean) as Hex[];

if (!rpcUrl || !apiKey || !safeAddress || ownerKeys.length === 0) {
  throw new Error(
    "RPC_URL, RHINESTONE_API_KEY, SAFE_ADDRESS, and SAFE_OWNER_PRIVATE_KEYS are required",
  );
}

const publicClient = createPublicClient({ transport: http(rpcUrl) });
const chainId = await publicClient.getChainId();

const protocolKit = await Safe.init({
  provider: rpcUrl,
  signer: ownerKeys[0],
  safeAddress,
});

let safeTransaction = await protocolKit.createTransaction({
  transactions: [
    {
      to: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
      value: parseEther("0.001").toString(),
      data: "0x",
    },
  ],
  options: {
    safeTxGas: "0",
    baseGas: "0",
    gasPrice: "0",
    gasToken: zeroAddress,
    refundReceiver: zeroAddress,
  },
});

for (const signer of ownerKeys) {
  const ownerKit = await protocolKit.connect({ signer });
  safeTransaction = await ownerKit.signTransaction(safeTransaction);
}

const threshold = await protocolKit.getThreshold();
if (safeTransaction.signatures.size < threshold) {
  throw new Error(`Safe requires ${threshold} owner signatures`);
}

const safeTxHash = await protocolKit.getTransactionHash(safeTransaction);
const data = await protocolKit.getEncodedTransaction(safeTransaction);
```

Build against the Safe's current onchain nonce. The API recomputes `safeTxHash` from the encoded transaction at that nonce and rejects a stale nonce, mismatched hash, invalid signatures, or an unsupported Safe proxy.

<Warning>
  Anyone who obtains the fully signed calldata can submit it. Treat it as an
  executable transaction until the Safe nonce changes.
</Warning>

Standard ECDSA signatures and correctly encoded EIP-1271 contract signatures are compatible. A `v = 1` pre-validated signature does not work merely because its signer is a Safe owner: the Safe sees `SafeRelayExecutor` as `msg.sender`, not that owner. It works only after that owner calls `approveHash(safeTxHash)` onchain.

## Relay the transaction

Send the Safe address as `to`, the complete `execTransaction` calldata as `data`, and the exact hash the owners signed:

```ts theme={null}
const apiBaseUrl = "https://v1.orchestrator.rhinestone.dev";

const response = await fetch(`${apiBaseUrl}/safe-transactions`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": apiKey,
  },
  body: JSON.stringify({
    chainId,
    to: safeAddress,
    data,
    safeTxHash,
  }),
});

const result = (await response.json()) as {
  taskId?: string;
  error?: string;
};
if (!response.ok || !result.taskId) {
  throw new Error(
    `Safe relay rejected (${response.status}): ${JSON.stringify(result)}`,
  );
}

const taskId = result.taskId;
```

A successful submission returns HTTP `201` and `{ taskId }`. The `taskId` is a decimal string and identifies this exact Safe transaction. Resubmitting the same transaction is idempotent and returns the same task, including after a terminal outcome. The submitted `chainId` comes from `RPC_URL`, preventing the example from signing on one network and relaying on another.

## Observe success or failure

Poll the status route until it returns a terminal code:

```ts theme={null}
const terminalStatuses = new Set([200, 400, 500]);
let relayStatus: {
  taskId: string;
  status: number;
  transactionHash?: Hex;
};

while (true) {
  const response = await fetch(
    `${apiBaseUrl}/safe-transactions/${taskId}/status`,
    { headers: { "x-api-key": apiKey } },
  );
  relayStatus = (await response.json()) as typeof relayStatus;

  if (!response.ok) {
    throw new Error(
      `Status request failed (${response.status}): ${JSON.stringify(relayStatus)}`,
    );
  }
  if (terminalStatuses.has(relayStatus.status)) break;

  await new Promise((resolve) => setTimeout(resolve, 3_000));
}

if (relayStatus.status !== 200) {
  throw new Error(`Safe relay failed with status ${relayStatus.status}`);
}

console.log("Safe transaction included:", relayStatus.transactionHash);
```

| Status | State     | Meaning                                                                                                     |
| ------ | --------- | ----------------------------------------------------------------------------------------------------------- |
| `100`  | Pending   | Rhinestone accepted the task but has not submitted an onchain fill.                                         |
| `110`  | Submitted | A relayer submitted the fill; a transaction hash may already be present.                                    |
| `200`  | Included  | The Safe transaction succeeded onchain.                                                                     |
| `400`  | Cancelled | The task expired or otherwise failed before successful execution.                                           |
| `500`  | Failed    | Safe execution or the relayer failed. A transaction hash is present only when a transaction landed onchain. |

Submission acceptance is not execution success. Persist `taskId`, poll through temporary network failures, and treat only `200` as successful. A terminal task is not revived by resubmission; construct and sign a new Safe transaction for a genuine retry.

## Sponsorship and Safe-paid gas

The end-to-end example requests sponsorship by signing all gas-refund fields as zero. On mainnets, the sponsorship budget covers the estimated wrapped relay gas plus any applicable relay fee. It does not fund the Safe's inner native `value`, token transfers, or contract calls. Complete [Sponsorship setup](/transactions/sponsorship/set-up) and fund its budget before submitting. A request without available sponsorship is rejected rather than silently changed to Safe-paid gas.

Safe-paid gas is supported for a single `execTransaction`, but it requires a complete Safe fee-preview integration before you collect signatures. Fetch `GET /relay/gas-price?chainId=<chainId>` with **Intents: Read** access immediately before the preview, and use the returned `gasPriceWei` unchanged as the transaction's signed `gasPrice`. Reject the quote after `validUntil`.

The gas-price endpoint is an input to fee calculation, not a fee estimator. Your preview integration must calculate `safeTxGas` and `baseGas` for the complete Rhinestone relay path, including the `SafeRelayExecutor` wrapper overhead. A direct Safe estimate or Protocol Kit's defaults do not include that wrapper overhead and can underfund the relayer. Sign the complete preview output with the native zero address as `gasToken` and `refundReceiver`, and fund the Safe for the maximum signed refund plus any inner native transfer.

If your integration cannot account for the wrapper overhead, use the sponsored flow above. Safe-paid submissions do not consume the project's sponsorship budget. Batches and Safe deployments are sponsored-only.

## Submission failures

| Response | Check                                                                                                                                |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `400`    | Confirm the Safe version, current nonce, signature threshold, hash, encoded `execTransaction`, chain support, and gas-refund fields. |
| `401`    | Send a valid project API key in `x-api-key` from your server.                                                                        |
| `403`    | Give the key the required Intents access and enable mainnets when applicable.                                                        |
| `409`    | Another different transaction already occupies this Safe nonce on the chain.                                                         |
| `422`    | Top up or re-enable sponsorship, or adjust the project's sponsorship limits.                                                         |

The endpoint also accepts canonical Safe deployment, passkey-signer deployment, and `MultiSendCallOnly` payloads under stricter rules. This guide uses the supported single-transaction path; do not send arbitrary calldata or a generic SDK intent payload to `/safe-transactions`.
