Combine @abstraxn/server-signer and @abstraxn/relayer so your backend agent can sign meta-transactions with an Abstraxn Server Wallet and submit them gaslessly through the relayer.
How it works
authenticate() → Abstraxn server wallet session
createRelayerSigner() → Abstraxn signs meta-tx (not a local private key)
buildRelayerTxEIP712() → encode + sign authorization
sendRelayerTx() → relayer pays gas and calls executeMetaTransaction
| Step | Who signs / pays |
|---|
| Meta-tx authorization | Abstraxn Server Wallet |
| On-chain execution | Relayer (gas tank) |
Install
npm install @abstraxn/server-signer @abstraxn/relayer ethers
1. Authenticate the server wallet
Use the same userIdentity string every time — it must match the value you used when the wallet was first created.
import { ServerSignerClient } from '@abstraxn/server-signer';
const client = new ServerSignerClient({
apiKey: process.env.ABSTRAXN_API_KEY!,
});
// First run: omit accessKey — SDK creates wallet and returns session.accessKey
// Later runs: pass the saved accessKey
const session = await client.authenticate({
userIdentity: 'my-backend-agent-001',
accessKey: process.env.SERVER_WALLET_ACCESS_KEY,
});
if (session.didCreate) {
// Save session.accessKey — required to recover this wallet later
await saveToSecretsManager(session.accessKey);
}
userIdentity is permanent. Changing it creates a new wallet. accessKey must match the key from the first authenticate() for that identity.
2. Create a relayer signer
createRelayerSigner() returns a signer the relayer SDK can use. Signing is handled remotely by Abstraxn — your backend never holds the wallet private key.
import { JsonRpcProvider, type Signer } from 'ethers';
const rpcUrl = process.env.RPC_URL!;
const me = await client.whoami<{ result: { address: string; organizationId: string } }>();
const { signer } = client.createRelayerSigner({
rpcUrl,
chainId: 137,
organizationId: me.result.organizationId,
fromAddress: me.result.address,
});
3. Build and send a gasless transaction
Pick the build method that matches your contract (see Relayer guide):
| Method | When to use |
|---|
buildRelayerTxEIP712 | Default — most Abstraxn meta-tx contracts |
buildRelayerTx | Contracts that verify EIP-191 signMessage hashes |
import { Relayer } from '@abstraxn/relayer';
import { ChainId } from '@abstraxn/core-types';
const relayer = new Relayer({
relayerUrl: process.env.RELAYER_URL!,
chainId: ChainId.POLYGON_MAINNET,
provider: new JsonRpcProvider(rpcUrl),
signer: signer as unknown as Signer,
});
const buildTxParams = {
contractAddress: '0xYourContract',
abi: yourContractAbi,
method: 'yourMethod',
args: ['arg1'],
};
// EIP-712 (recommended)
const txData = await relayer.buildRelayerTxEIP712(buildTxParams);
// — or EIP-191 personal sign for legacy contracts:
// const txData = await relayer.buildRelayerTx(buildTxParams);
const { transactionId } = await relayer.sendRelayerTx(txData);
const status = await relayer.getRelayerTxStatus(transactionId);
Use buildRelayerTx only if your contract does not verify EIP-712 MetaTransaction typed data.
Prerequisites
- Same
userIdentity on every authenticate() call for that wallet
- Contract exposes
getNonce(address) and your target method in the ABI
- Contract is whitelisted on your relayer app
- Relayer gas tank has balance on the target chain
fromAddress / server wallet address owns the on-chain meta-tx nonce
Environment variables
| Variable | Description |
|---|
ABSTRAXN_API_KEY | App API key from dashboard |
SERVER_WALLET_ACCESS_KEY | 64-char hex from first authenticate() (omit on first run) |
RPC_URL | Chain JSON-RPC URL |
RELAYER_URL | Relayer URL from dashboard |
Next Steps