Smart Account: Lazy Deploy + Gas Sponsorship
Scenario: You're building a wallet-as-a-service product. Each user gets a dedicated Gnosis Safe address derived deterministically from their user ID β so you can tell users their address before paying any deployment gas. The Safe isn't deployed until the user's first transaction. A single "relayer" EOA (funded MPC key) sponsors gas for all Safe interactions across all users.
What you will learn:
- How to predict a Smart Account address before deploying it (counterfactual addressing)
- How to lazily deploy a Gnosis Safe only when the first transaction happens
- How to sponsor gas so users never need to hold ETH
- How to sign and relay Safe transactions using MPC keys
Prerequisites: Basic familiarity with Ethereum transactions and TypeScript.
Architecture overviewβ
MPC Key (one root key for your whole product)
β
βββ [address index 0] β Relayer EOA β holds ETH, pays gas for everyone
β
βββ [address index 1] β User #1's owner key β User #1's Gnosis Safe
βββ [address index 2] β User #2's owner key β User #2's Gnosis Safe
βββ [address index N] β User #N's owner key β User #N's Gnosis Safe
One MPC root key generates all addresses deterministically. The relayer is just another derived address β you fund it with ETH and it pays gas on behalf of every user. Each user's Safe is controlled by their unique derived key and is not deployed until the first transaction is needed.
Section 1 β Project setupβ
Before anything else, install the SDK and import the modules you need.
import { WorkspaceClient, ComponentModule } from 'caller-sdk';
const workspace = new WorkspaceClient({ apiKey: process.env.WR_API_KEY! });
const RPC_URL = 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY';
const KEY_ID = process.env.WR_KEY_ID!; // your MPC root key ID
Key concept β WorkspaceClient: This is the entry point for all White Rabbit component calls. Every component call goes through workspace.call(ComponentModule.COMPONENT_NAME, inputs).promise(). The .promise() at the end waits for the result over a live stream β no polling required.
Key concept β KEY_ID: This is the identifier for your MPC root key, created once in the White Rabbit dashboard. All user addresses and the relayer address are derived from this single key using BIP-44 derivation paths. The actual private key material never leaves the MPC nodes.
Section 2 β Resolving an EVM address from an MPC keyβ
This is a shared helper used throughout the example. Given a BIP-44 address index, it derives the corresponding EVM address.
async function resolveEvmAddress(addressIndex: number) {
// Step A: compute the BIP-44 derivation path for this index
const { derivationPath } = await workspace
.call(ComponentModule.GET_EVM_DERIVATION_PATH, { addressIndex })
.promise();
// Step B: derive the public key from the MPC share at this path
const { publicKey } = await workspace
.call(ComponentModule.COMPUTE_PUBLIC_KEY, { keyId: KEY_ID, derivationPath })
.promise();
// Step C: hash the public key to get the EVM address (Keccak-256 of uncompressed pubkey)
const { address } = await workspace
.call(ComponentModule.COMPUTE_EVM_ADDRESS, { publicKey })
.promise();
return { address, derivationPath };
}
Key concept β BIP-44 derivation paths: BIP-44 is the standard for generating multiple addresses from one root key. The path m/44'/60'/0'/0/N gives you address index N. GET_EVM_DERIVATION_PATH builds this path for you β you only need to provide addressIndex.
Key concept β COMPUTE_PUBLIC_KEY: The MPC node holds a key share (a fragment of the private key). This component asks the MPC network to compute the public key at the given derivation path β without ever reconstructing the full private key.
Key concept β COMPUTE_EVM_ADDRESS: An Ethereum address is the last 20 bytes of the Keccak-256 hash of the public key. This component does that conversion for you and returns a checksummed 0x... address.
These three steps always go together: GET_EVM_DERIVATION_PATH β COMPUTE_PUBLIC_KEY β COMPUTE_EVM_ADDRESS. Think of it as: which path β derive pubkey β convert to address.
Section 3 β Predicting the Safe address before deployingβ
A Gnosis Safe's address is deterministic β computed from its owners, threshold, and a salt. This means you can tell a user their Smart Account address before spending any gas.
async function getUserSafeAddress(userId: number) {
// Derive the user's unique owner address (each user gets their own index)
const { address: ownerAddress, derivationPath: ownerPath } =
await resolveEvmAddress(userId); // userId maps to BIP-44 address index
// Predict the Safe address using CREATE2 β no transaction needed, no gas spent
const { safeAddress } = await workspace
.call(ComponentModule.GET_EVM_SAFE_ADDRESS, {
jsonRpcUrl: RPC_URL,
owners: [ownerAddress], // 1-of-1 Safe: only the user's key can authorize txs
threshold: 1,
saltNonce: String(userId), // using userId as salt = same address for this user forever
})
.promise();
console.log(`User ${userId} Safe address: ${safeAddress}`);
// You can store this in your DB immediately β no gas required yet
return { safeAddress, ownerAddress, ownerPath };
}
Key concept β Counterfactual addresses: A Smart Account can have a known address before it exists on-chain. The Safe proxy factory uses CREATE2, an EVM opcode that computes the contract address from the factory address, a salt, and the contract bytecode. Because all three are deterministic, the address is always the same for the same inputs.
Key concept β saltNonce: Using userId as the salt guarantees that the same user always gets the same Safe address, on every chain. You can give this address to the user on day one without spending a cent on deployment.
Key concept β 1-of-1 Safe: For a simple wallet product, a 1-owner/1-threshold Safe means only one signature is needed to execute transactions. You can upgrade to multi-owner Safes (e.g. 2-of-3 for team accounts) without changing the address β just change the owners and threshold when deploying.
Section 4 β First use: deploy + execute in one transaction (Multicall)β
Without multicall, the first-time user flow costs two transactions: one to deploy the Safe, then wait for confirmation, then another to execute the actual transfer. That means two gas fees and a block-time delay between them.
With multicall, both happen atomically in one transaction:
Transaction (single gas fee)
βββ Call 1: SafeProxyFactory.createProxyWithNonce(...) β Safe is now deployed
βββ Call 2: Safe.execTransaction(...) β first tx executes immediately
Because multicall executes calls in order within a single block, Call 2 can call the Safe contract that Call 1 just created. The user pays half the gas, and the flow completes in one block instead of two.
// Canonical Multicall3 address β same on every EVM chain
const MULTICALL3 = '0xcA11bde05977b3631167028862bE2a173976CA11';
Key concept β Multicall3: Multicall3 is a utility contract deployed at the same address on every major EVM chain (Ethereum, Polygon, Arbitrum, Base, etc.). Its aggregate3 function takes an array of { target, callData, allowFailure } items and executes them one by one inside a single transaction. If allowFailure is false, the entire multicall reverts when any call fails β keeping both actions atomic.
async function deployAndExecuteMulticall(
safeAddress: string,
ownerAddress: string,
userId: number,
ownerPath: number[],
relayer: { address: string; derivationPath: number[] },
innerTo: string, // what the Safe will call (e.g. the USDC contract)
innerCalldata: string, // what the Safe will do (e.g. transfer calldata)
innerValue = '0',
) {
// ββ Step 1: Get chain ID (needed for Safe EIP-712 signature domain) ββββββββββ
const { chainId } = await workspace
.call(ComponentModule.GET_EVM_CHAIN_ID, { jsonRpcUrl: RPC_URL })
.promise();
// ββ Step 2: Build deployment calldata ββββββββββββββββββββββββββββββββββββββββ
// This produces the raw calldata for SafeProxyFactory.createProxyWithNonce(...)
const { deploymentTransaction } = await workspace
.call(ComponentModule.BUILD_EVM_GNOSIS_SAFE_DEPLOYMENT, {
jsonRpcUrl: RPC_URL,
owners: [ownerAddress],
threshold: 1,
saltNonce: userId,
})
.promise();
// ββ Step 3: Build the Safe owner signature for the first tx ββββββββββββββββββ
// A fresh Safe's nonce is always 0 β there have been no prior transactions.
// Because we already know the Safe's counterfactual address (from Section 3),
// we can build and sign this before the Safe even exists on-chain.
const { messageHash } = await workspace
.call(ComponentModule.BUILD_EVM_SAFE_SIGNATURE, {
safeAddress,
chainId,
to: innerTo,
value: Number(innerValue),
data: innerCalldata,
operation: 0, // CALL
nonce: 0, // always 0 for the very first transaction on a fresh Safe
})
.promise();
const { signature: ownerSignature } = await workspace
.call(ComponentModule.SIGN_WITH_KEY_SHARE, {
keyId: KEY_ID,
derivationPath: ownerPath,
messageHash,
})
.promise();
// ββ Step 4: Build execTransaction calldata ββββββββββββββββββββββββββββββββββββ
// This encodes the Safe.execTransaction(...) call that will run inside multicall.
const { calldata: execCalldata } = await workspace
.call(ComponentModule.BUILD_EVM_SAFE_TRANSACTION, {
safeAddress,
innerTo,
innerCalldata,
innerValue,
signatures: [ownerSignature],
})
.promise();
// ββ Step 5: Build the two multicall items ββββββββββββββββββββββββββββββββββββ
// Both items are built in parallel β they don't depend on each other yet.
const [{ call: deployCall }, { call: execCall }] = await Promise.all([
// Item 1 β deploy the Safe (targets the SafeProxyFactory)
workspace.call(ComponentModule.BUILD_EVM_MULTICALL_ITEM, {
target: deploymentTransaction.to, // SafeProxyFactory address
callData: deploymentTransaction.data,
allowFailure: false, // revert the whole multicall if deployment fails
}).promise(),
// Item 2 β execute the first user tx through the Safe (targets the Safe itself)
workspace.call(ComponentModule.BUILD_EVM_MULTICALL_ITEM, {
target: safeAddress, // the Safe contract (will exist after item 1 runs)
callData: execCalldata,
allowFailure: false, // revert if the Safe tx fails too
}).promise(),
]);
// ββ Step 6: Bundle into one multicall ββββββββββββββββββββββββββββββββββββββββ
// Order matters: deploy must come before execute.
const { calldata: multicallCalldata } = await workspace
.call(ComponentModule.BUILD_EVM_MULTICALL, {
calls: [deployCall, execCall],
})
.promise();
// ββ Step 7: Relayer signs and broadcasts one EVM transaction βββββββββββββββββ
// From the network's perspective this is just one transaction β one gas fee,
// one block confirmation, and both actions succeed or both fail together.
const { unsignedTransaction, serializedHash } = await workspace
.call(ComponentModule.BUILD_EVM_TRANSACTION, {
jsonRpcUrl: RPC_URL,
from: relayer.address, // relayer pays the gas
to: MULTICALL3, // the Multicall3 contract runs our bundled calls
value: '0',
calldata: multicallCalldata,
})
.promise();
const { signature } = await workspace
.call(ComponentModule.SIGN_WITH_KEY_SHARE, {
keyId: KEY_ID,
derivationPath: relayer.derivationPath,
messageHash: serializedHash,
})
.promise();
const { signedTransaction } = await workspace
.call(ComponentModule.SIGN_EVM_TRANSACTION, { unsignedTransaction, signature })
.promise();
const { transactionHash } = await workspace
.call(ComponentModule.BROADCAST_EVM_TRANSACTION, {
jsonRpcUrl: RPC_URL,
signedTransaction,
})
.promise();
console.log(`Deploy + execute bundled tx: ${transactionHash}`);
const receipt = await workspace
.call(ComponentModule.WAIT_FOR_EVM_TRANSACTION, {
jsonRpcUrl: RPC_URL,
transactionHash,
})
.promise();
console.log(`Confirmed in block ${receipt.blockNo}, gas saved vs two-tx: ~40%`);
return transactionHash;
}
Key concept β Nonce is 0 for the first Safe transaction: Because the Safe has never processed a transaction before, its on-chain nonce is 0. You don't need to call GET_EVM_GNOSIS_SAFE_NONCE β you can hard-code nonce: 0. This is what makes it possible to sign the Safe transaction before the Safe exists: the only unknowns (address, chainId, nonce) are all already known counterfactually.
Key concept β Atomicity: Both calls are wrapped in the same EVM transaction. If the Safe deployment succeeds but execTransaction fails (e.g. USDC balance too low), the entire multicall reverts β the Safe is not deployed. This prevents a state where the Safe exists but the intended first action never happened. Set allowFailure: false on both items to enforce this.
Key concept β Why BUILD_EVM_MULTICALL_ITEM in parallel: The two items don't depend on each other's component outputs β they only depend on each other during on-chain execution (item 1 must run before item 2). So you can call BUILD_EVM_MULTICALL_ITEM for both at the same time with Promise.all, then pass them in the correct order to BUILD_EVM_MULTICALL.
Key concept β Gas savings: Deploying a Safe costs roughly 280k gas. Executing a Safe transaction costs roughly 80kβ120k gas. Two separate transactions = two base fees (~21k gas each) + execution costs. One multicall = one base fee. On a busy network (e.g. Ethereum mainnet), this can save 30β40% in total gas cost for the first-use flow.
Section 5 β Subsequent transactions (Safe already deployed)β
After the first use, the Safe exists on-chain and every future transaction goes through this function directly β no deployment needed. The Safe acts as the sender on-chain, so from any token or contract's perspective it's the Safe address making the call, not the relayer or the user's key.
The key difference from Section 4: here we read the real nonce from the chain (GET_EVM_GNOSIS_SAFE_NONCE) because the Safe has already processed at least one transaction and its nonce is no longer 0.
async function executeSafeTransaction(
safeAddress: string,
ownerPath: number[], // user's key β signs the Safe's internal tx
relayer: { address: string; derivationPath: number[] }, // pays gas
innerTo: string, // the contract the Safe is calling
innerCalldata: string, // the encoded function call
innerValue = '0', // ETH value of the inner call (usually 0 for token transfers)
) {
// Fetch chain ID and Safe nonce in parallel (both are needed for the signature)
const [{ chainId }, { nonce }] = await Promise.all([
workspace.call(ComponentModule.GET_EVM_CHAIN_ID, { jsonRpcUrl: RPC_URL }).promise(),
workspace.call(ComponentModule.GET_EVM_GNOSIS_SAFE_NONCE, {
jsonRpcUrl: RPC_URL,
safeAddress,
}).promise(),
]);
// Build the EIP-712 typed-data hash that the Safe owner must sign.
// This is the "intent" β what the Safe is authorized to do.
const { messageHash } = await workspace
.call(ComponentModule.BUILD_EVM_SAFE_SIGNATURE, {
safeAddress,
chainId,
to: innerTo,
value: Number(innerValue),
data: innerCalldata,
operation: 0, // 0 = CALL (standard), 1 = DELEGATECALL (advanced)
nonce,
})
.promise();
// The Safe owner (user's key) signs the Safe transaction hash β NOT the EVM tx hash
const { signature: ownerSignature } = await workspace
.call(ComponentModule.SIGN_WITH_KEY_SHARE, {
keyId: KEY_ID,
derivationPath: ownerPath,
messageHash, // EIP-712 hash, not a transaction hash
})
.promise();
// Encode execTransaction(...) β the on-chain call that submits the Safe tx
const { calldata: execCalldata, to: safeTo } = await workspace
.call(ComponentModule.BUILD_EVM_SAFE_TRANSACTION, {
safeAddress,
innerTo,
innerCalldata,
innerValue,
signatures: [ownerSignature], // collect all owner signatures here for multi-sig
})
.promise();
// Now the relayer wraps execTransaction in a real EVM transaction (pays gas)
const { unsignedTransaction, serializedHash } = await workspace
.call(ComponentModule.BUILD_EVM_TRANSACTION, {
jsonRpcUrl: RPC_URL,
from: relayer.address,
to: safeTo, // the Safe contract address
value: '0',
calldata: execCalldata,
})
.promise();
// Relayer signs the EVM transaction (separate from the Safe signature above)
const { signature: relayerSignature } = await workspace
.call(ComponentModule.SIGN_WITH_KEY_SHARE, {
keyId: KEY_ID,
derivationPath: relayer.derivationPath,
messageHash: serializedHash,
})
.promise();
const { signedTransaction } = await workspace
.call(ComponentModule.SIGN_EVM_TRANSACTION, {
unsignedTransaction,
signature: relayerSignature,
})
.promise();
const { transactionHash } = await workspace
.call(ComponentModule.BROADCAST_EVM_TRANSACTION, {
jsonRpcUrl: RPC_URL,
signedTransaction,
})
.promise();
console.log(`Safe tx submitted: ${transactionHash}`);
const receipt = await workspace
.call(ComponentModule.WAIT_FOR_EVM_TRANSACTION, {
jsonRpcUrl: RPC_URL,
transactionHash,
})
.promise();
console.log(`Confirmed in block ${receipt.blockNo}, gas used: ${receipt.gasUsed}`);
return transactionHash;
}
Key concept β Two separate signatures: This is the most important thing to understand about Gnosis Safe:
- Owner signature (
BUILD_EVM_SAFE_SIGNATURE+SIGN_WITH_KEY_SHAREwith owner key): This proves the Safe owner authorized the inner action (e.g. "transfer 100 USDC to Alice"). It's an EIP-712 typed-data signature β not a transaction. - Relayer signature (
BUILD_EVM_TRANSACTION+SIGN_WITH_KEY_SHAREwith relayer key): This is the actual EVM transaction that callsexecTransaction()on the Safe contract. The relayer pays the gas.
Key concept β Safe nonce: Every Safe transaction increments a nonce on-chain. This prevents replay attacks β you can't reuse an old signature to execute the same transaction again. Always read the current nonce with GET_EVM_GNOSIS_SAFE_NONCE immediately before building the signature.
Key concept β Chain ID: The EIP-712 domain includes the chain ID. This means a signature made for Ethereum mainnet is invalid on Polygon β preventing cross-chain replay attacks.
Key concept β innerTo vs safeTo: The innerTo is where the Safe sends the call (e.g. the USDC token contract). The safeTo is the Safe's own address β that's what the relayer calls on-chain (safeAddress.execTransaction(...)).
Section 6 β Encoding the inner action (ERC-20 transfer)β
Before calling executeSafeTransaction, you need to encode what the Safe should actually do. Here's how to encode an ERC-20 transfer:
const ERC20_TRANSFER_ABI = [
{
type: 'function',
name: 'transfer',
inputs: [
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ name: '', type: 'bool' }],
stateMutability: 'nonpayable',
},
];
async function encodeErc20Transfer(recipient: string, amount: string) {
const { calldata } = await workspace
.call(ComponentModule.BUILD_EVM_CALLDATA, {
abi: ERC20_TRANSFER_ABI,
args: [recipient, amount],
})
.promise();
return calldata; // "0xa9059cbb000000..."
}
Key concept β ABI encoding: A contract function call on Ethereum isn't sent as readable text β it's encoded as binary data (the "calldata"). BUILD_EVM_CALLDATA takes the ABI definition of a function and its arguments, and produces the correct hex-encoded calldata. The ABI is the contract's interface spec β you can find it in the project's source code or on Etherscan.
Key concept β Amounts in smallest units: ERC-20 token amounts are always in the token's smallest unit. USDC has 6 decimal places, so 100 USDC = 100_000_000 (100 Γ 10βΆ). ETH has 18 decimal places, so 1 ETH = 1_000_000_000_000_000_000 (1 Γ 10ΒΉβΈ). Always convert before passing to components.
Final section β Putting it all togetherβ
The orchestrator checks deployment status first, then takes the right path:
- Not deployed (first use) β
deployAndExecuteMulticallβ one transaction, one gas fee - Already deployed β
executeSafeTransactionβ direct Safe execution
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
async function sendUsdcFromUserSafe(
userId: number, // the user's ID in your system
recipient: string, // where to send the tokens
amountUsdc: number // human-readable amount, e.g. 100 for 100 USDC
) {
// USDC has 6 decimal places β convert to smallest unit
const amount = String(amountUsdc * 1_000_000);
// 1. Resolve the relayer (index 0) and the user's Safe address β in parallel
const [relayer, { safeAddress, ownerAddress, ownerPath }] = await Promise.all([
resolveEvmAddress(0), // relayer is always index 0
getUserSafeAddress(userId), // Safe address (predicted, may not exist yet)
]);
console.log(`Relayer (gas payer): ${relayer.address}`);
console.log(`User ${userId} Safe: ${safeAddress}`);
// 2. Check whether the Safe has been deployed yet
const { deployed } = await workspace
.call(ComponentModule.IS_EVM_SAFE_DEPLOYED, {
jsonRpcUrl: RPC_URL,
address: safeAddress,
})
.promise();
// 3. Encode the inner ERC-20 transfer the Safe will execute
const calldata = await encodeErc20Transfer(recipient, amount);
if (!deployed) {
// ββ First use: bundle deploy + execute into one multicall transaction ββββββ
// Saves ~40% gas vs two separate transactions, and confirms in one block.
console.log('First use β bundling deployment + transfer into one transactionβ¦');
return deployAndExecuteMulticall(
safeAddress, ownerAddress, userId, ownerPath, relayer,
USDC, calldata,
);
} else {
// ββ Subsequent use: Safe already exists, just execute βββββββββββββββββββββ
console.log('Safe deployed β executing transfer directlyβ¦');
return executeSafeTransaction(safeAddress, ownerPath, relayer, USDC, calldata);
}
}
// β ββ Usage βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Send 100 USDC from user #42's Safe to a recipient
const txHash = await sendUsdcFromUserSafe(42, '0xRecipientAddress...', 100);
console.log(`Done! Transaction: ${txHash}`);
How everything connects:
resolveEvmAddress(0)andgetUserSafeAddress(userId)run in parallel to save timeIS_EVM_SAFE_DEPLOYEDis the single branch point: first use or not?encodeErc20Transfer(...)builds the inner calldata (same for both paths)- First use β
deployAndExecuteMulticall(...):- Builds deployment + exec calldata, signs the Safe tx at nonce 0, bundles into multicall, relayer broadcasts once
- Subsequent uses β
executeSafeTransaction(...):- Reads current nonce, signs Safe tx, relayer broadcasts normally
Full flow diagramβ
sendUsdcFromUserSafe(userId, recipient, amount)
β
βββ resolveEvmAddress(0) ββββββββ GET_EVM_DERIVATION_PATH
β [relayer] COMPUTE_PUBLIC_KEY
β COMPUTE_EVM_ADDRESS
β
βββ getUserSafeAddress(userId) ββ resolveEvmAddress(userId)
β [safeAddress, ownerPath] GET_EVM_SAFE_ADDRESS
β
βββ IS_EVM_SAFE_DEPLOYED ββββ branch point
β
βββ encodeErc20Transfer() βββββββ BUILD_EVM_CALLDATA
β
β ββ NOT deployed (first use) ββββββββββββββββββββββββββββββββββββββββββ
β β β
β β deployAndExecuteMulticall() β
β β GET_EVM_CHAIN_ID β
β β BUILD_EVM_GNOSIS_SAFE_DEPLOYMENT β deployCall β
β β BUILD_EVM_SAFE_SIGNATURE (nonce=0) β
β β SIGN_WITH_KEY_SHARE (owner key) β
β β BUILD_EVM_SAFE_TRANSACTION β execCall β
β β BUILD_EVM_MULTICALL_ITEM Γ2 βββ run in parallel β
β β BUILD_EVM_MULTICALL ([deployCall, execCall]) β
β β BUILD_EVM_TRANSACTION (to: MULTICALL3, relayer pays) β
β β SIGN_WITH_KEY_SHARE (relayer key) β
β β SIGN_EVM_TRANSACTION β
β β BROADCAST_EVM_TRANSACTION β one tx, one gas fee β
β β WAIT_FOR_EVM_TRANSACTION β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββ ββ deployed (subsequent use) ββββββββββββββββββββββββββββββββββββββββββ
β β
β executeSafeTransaction() β
β GET_EVM_CHAIN_ID + GET_EVM_GNOSIS_SAFE_NONCE β read real nonce β
β BUILD_EVM_SAFE_SIGNATURE β
β SIGN_WITH_KEY_SHARE (owner key) β
β BUILD_EVM_SAFE_TRANSACTION β
β BUILD_EVM_TRANSACTION (to: safeAddress, relayer pays) β
β SIGN_WITH_KEY_SHARE (relayer key) β
β SIGN_EVM_TRANSACTION β
β BROADCAST_EVM_TRANSACTION β
β WAIT_FOR_EVM_TRANSACTION β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Common questionsβ
Can I add more owners to the Safe later?
Yes β use MANAGE_SAFE_OWNERS to add/remove owners or change the threshold at any time. The Safe's address doesn't change.
What if I want to use the same Safe address on multiple chains?
The same owners + threshold + saltNonce produces the same address on every EVM chain (Ethereum, Polygon, Arbitrum, Base, etc.). Deploy separately on each chain.
How do I fund the relayer?
Send ETH to relayer.address from any wallet. Monitor its balance and top it up when it runs low.
The relayer EOA at derivation index 0 must hold enough ETH to cover gas for deployments and Safe transactions. An empty relayer means your users can't transact.