The @abstraxn/relayer package empowers your dApp to transform user intent into on-chain execution—gaslessly, securely, and with real-time updates.

Installation

You can install our SDK by running the following command in your terminal:
npm install @abstraxn/relayer@latest

Setup & Configuration

To obtain the Relayer URL and API Key: To begin, you’ll need a Relayer URL and API Key. Retrieve these from your Abstraxn Dashboard:
  • Visit https://dashboard.abstraxn.com/
  • Log in or sign up
  • Go to Apps → click Create New App → enter name, chain, description
  • Within the app, navigate to Relayer → click Add Relayer, assign a name, select app
  • Click View Details → copy your Relayer URL and API Key
⚠️ Warning: Keep your API key secret. Store it in environment variables on the server side — never embed it into public client code.

Basic Integration (Node / Server)

import { Relayer } from "@abstraxn/relayer";
import { ChainId } from "@abstraxn/core-types";

const relayerConfig = {
  relayerUrl: "https://your-relayer-url.com",
  chainId: ChainId.MAINNET,
  signer: yourSignerInstance,
  provider: yourProviderInstance,
};

const relayer = new Relayer(relayerConfig);

// Example: Building a relayer transaction
const buildTxParams = {
  contractAddress: "0xYourContractAddress",
  abi: yourContractAbi,
  method: "yourMethodName",
  args: ["arg1", "arg2"],
};

// Build and send transaction to relayer
relayer.buildRelayerTx(buildTxParams).then(async (txData) => {
  console.log("Build Relayer Transaction Response: ", txData);

  const response = await relayer.sendRelayerTx(txData);
  // response =  {message: string; transactionId: string; }
  const status = await relayer.getRelayerTxStatus(response.transactionId);
  return status;
  // status = {hash: string | null;data: string | null;status: string;receipt: TransactionReceipt | null;createdAt: string;reason?: string | null;}
});

Real-Time WebSocket Integration

Real-time updates (v0.1.5+). Server or client can subscribe to live events for instant UX.
const relayer = new Relayer({
  relayerUrl: process.env.Abstraxn_URL,
  apiKey: process.env.Abstraxn_KEY,
  chainId: ChainId.MAINNET,
  signer,
  provider,
  webSocket: {
    enabled: true,
    autoConnect: true,
    reconnection: true,
  },
});

const txData = await relayer.buildRelayerTx(params);

const resp = await relayer.sendRelayerTxWithRealTimeUpdates({
  ...txData,
  enableRealTimeUpdates: true,
  webSocketEvents: {
    onTransactionUpdate: (update) => {
      console.log("TX Update:", update);
    },
    onError: (err) => console.error("WS Error:", err),
  },
});

console.log("Relayer transaction submitted:", resp.transactionId);
Status flow moves: initiated → pending → confirmed / failed / rejected

Safe Transaction Execution

Execute Safe transactions with multiple owners using the Relayer SDK. This example demonstrates how to build a Safe transaction, collect signatures from multiple owners, and execute it through the relayer with real-time WebSocket updates.
import { useState } from "react";
import { ethers } from "ethers";
import { Relayer } from "@abstraxn/relayer";

/* ================= CONFIG ================= */

const RPC_URL = "";
const SAFE_ADDRESS = "";
const TARGET = "";
const CHAIN_ID = 137n;

const OWNER1_PK = "";
const OWNER2_PK = "";

/* ================= SAFE ABI ================= */

const SAFE_ABI = [
  "function nonce() view returns (uint256)",
  "function execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes) payable returns (bool)",
];

/* ================= TYPES ================= */

type SafeTx = {
  to: string;
  value: bigint;
  data: string;
  operation: number;
  safeTxGas: bigint;
  baseGas: bigint;
  gasPrice: bigint;
  gasToken: string;
  refundReceiver: string;
  nonce: bigint;
};

type SignatureItem = {
  signer: string;
  signature: string;
};

/* ================= EIP-712 TYPE ================= */

type TypedDataField = {
  name: string;
  type: string;
};

const SAFE_TX_TYPE: {
  SafeTx: TypedDataField[];
} = {
  SafeTx: [
    { name: "to", type: "address" },
    { name: "value", type: "uint256" },
    { name: "data", type: "bytes" },
    { name: "operation", type: "uint8" },
    { name: "safeTxGas", type: "uint256" },
    { name: "baseGas", type: "uint256" },
    { name: "gasPrice", type: "uint256" },
    { name: "gasToken", type: "address" },
    { name: "refundReceiver", type: "address" },
    { name: "nonce", type: "uint256" },
  ],
} as const;

/* ================= HELPERS ================= */

function sortSignatures(sigs: SignatureItem[]): string[] {
  return sigs
    .sort((a, b) =>
      a.signer.toLowerCase().localeCompare(b.signer.toLowerCase()),
    )
    .map((s) => s.signature);
}

function packSignatures(signatures: string[]): string {
  return "0x" + signatures.map((sig) => sig.slice(2)).join("");
}

/* ================= APP ================= */

export default function SafeTx() {
  const [txHash, setTxHash] = useState<string>("");
  const [loading, setLoading] = useState<boolean>(false);

  const executeMetaTx = async (): Promise<void> => {
    try {
      setLoading(true);

      /* ---------- Provider & wallets ---------- */
      const provider = new ethers.JsonRpcProvider(RPC_URL);
      const owner1 = new ethers.Wallet(OWNER1_PK);
      const owner2 = new ethers.Wallet(OWNER2_PK);

      const safe = new ethers.Contract(SAFE_ADDRESS, SAFE_ABI, provider);

      console.log("safe", safe);

      /* ---------- Build SafeTx ---------- */
      const nonce: bigint = await safe.nonce();
      console.log("nonce", nonce);

      const safeTx: SafeTx = {
        to: TARGET,
        value: 0n,
        data: "0x",
        operation: 0, // CALL
        safeTxGas: 0n,
        baseGas: 0n,
        gasPrice: 0n,
        gasToken: ethers.ZeroAddress,
        refundReceiver: ethers.ZeroAddress,
        nonce,
      };

      const domain = {
        chainId: CHAIN_ID,
        verifyingContract: SAFE_ADDRESS,
      };

      /* ---------- Owners sign ---------- */
      const sig1: string = await owner1.signTypedData(
        domain,
        SAFE_TX_TYPE,
        safeTx,
      );

      const sig2: string = await owner2.signTypedData(
        domain,
        SAFE_TX_TYPE,
        safeTx,
      );

      /* ---------- Sort & pack ---------- */
      const ordered = sortSignatures([
        { signer: await owner1.getAddress(), signature: sig1 },
        { signer: await owner2.getAddress(), signature: sig2 },
      ]);

      const signatureBytes: string = packSignatures(ordered);

      console.log("signatureBytes", signatureBytes);

      /* ---------- Execute via relayer ---------- */
      const relayerConfig = {
        relayerUrl: "",
        isSafeTx: true,
        webSocket: {
          enabled: true,
          autoConnect: true,
          reconnection: true,
        },
      };
      const relayer = new Relayer(relayerConfig);

      await relayer.sendSafeRelayerTxWithRealTimeUpdates({
        safeAddress: SAFE_ADDRESS,
        safeExecTxPayload: {
          safeTx: safeTx,
          signatureBytes: signatureBytes,
        },
        chainId: Number(CHAIN_ID),
        enableRealTimeUpdates: true,
        webSocketEvents: {
          onTransactionUpdate: (update) => {
            console.log("update", update);
            console.log(`Transaction ${update.txId} status: ${update.status}`);

            // Set transaction hash when available
            if (update.hash) {
              setTxHash(update.hash);
            }

            if (update.status === "confirmed") {
              console.log("🎉 Transaction confirmed!", update.blockNumber);
            } else if (
              update.status === "failed" ||
              update.status === "rejected"
            ) {
              console.error("Transaction failed:", update.reason);
              alert(`Transaction failed: ${update.reason || "Unknown error"}`);
            }
          },
          onError: (error) => {
            console.error("WebSocket error:", error);
          },
        },
      });
    } catch (err) {
      console.error(err);
      alert("Meta-transaction failed — see console");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div style={{ padding: 20 }}>
      <h2>Safe Manual Meta-Tx (2 Owners, TS)</h2>

      <button onClick={executeMetaTx} disabled={loading}>
        {loading ? "Executing..." : "Execute Meta-Tx"}
      </button>

      {txHash && (
        <p>
          Tx Hash:
          <br />
          <a
            href={`https://polygonscan.com/tx/${txHash}`}
            target="_blank"
            rel="noreferrer"
          >
            {txHash}
          </a>
        </p>
      )}
    </div>
  );
}

Key Components

  • Safe Transaction Building: Creates a Safe transaction with the required parameters including nonce, target address, and operation type
  • Multi-Owner Signing: Collects EIP-712 signatures from multiple Safe owners
  • Signature Sorting: Sorts signatures by signer address (required by Safe protocol)
  • Relayer Execution: Uses sendSafeRelayerTxWithRealTimeUpdates to execute the transaction through the relayer
  • Real-Time Updates: WebSocket events provide instant feedback on transaction status
⚠️ Security: Never expose private keys in client-side code. This example is for demonstration purposes. In production, handle signing on the server side or use secure wallet connections.
💡 Configuration: Make sure to set the isSafeTx: true flag in the relayer configuration when working with Safe transactions. This ensures the relayer properly handles Safe-specific transaction formats.

API Reference

  • new Relayer (options)
    • options.relayerUrl: string (required)
    • options.apiKey: string (required)
    • options.chainId: number | ChainId
    • options.signer (ethers/web3 signer)
    • options.provider (ethers/web3 provider)
    • options.webSocket?: { enabled: boolean, autoConnect?: boolean, reconnection?: boolean }
  • buildRelayerTx(params: BuildRelayerTxParams) → prepares payload for relay
  • buildRelayerTxEIP712(params) → prepare EIP-712 signed payload flows
  • sendRelayerTx(payload) → submit prepared payload to relayer service
  • sendRelayerTxWithRealTimeUpdates(payloadWithWsOptions) → submit + subscribe to WS updates
  • getRelayerTxStatus(transactionId) → poll current state
  • subscribeToTransaction(transactionId, events) → register handlers for updates
  • unsubscribeFromTransaction(transactionId)
  • connectWebSocket() / disconnectWebSocket() / isWebSocketConnected()

Polling vs Real-Time — When to use which?

ModeWhen to useProsCons
PollingSimple server workflows, short-lived tasksEasy to implement, works behind proxiesLatency, repeated requests
WebSocketReal-time UIs, mobile clients, instant feedbackInstant updates, better UXRequires persistent connection, reconnect handling

Integrations & Best Practices

  • Server-side relay orchestration: Keep API keys and critical signing on the server.
  • Intent validation: Validate user intent before creating relayer payloads. Agents should sanitize and verify prior to send.
  • Retries & idempotency: Implement idempotency keys / dedupe when re-sending in case of network issues.
  • Fallbacks: If WebSocket connectivity fails, gracefully fallback to polling.
  • Monitoring: Log transactionId, status, and relayer response codes for observability.

Security Notes

  • Never expose apiKey in client-side code.
  • Use short-lived server tokens and rotate keys when possible.
  • Validate contract ABI and method inputs to avoid unintended on-chain calls.
  • Enforce authorization: ensure only approved apps/users can request relayer submits.

Troubleshooting

Q: sendRelayerTx returns an error / 4xx or 5xx - Verify relayerUrl and apiKey are correct. - Check server time skew (if signatures used). - Inspect payload (ABI, method, args) for mismatches. Q: WebSocket won’t connect - Ensure your environment allows outgoing WS connections. - Check CORS / firewall rules. - Confirm webSocket.enabled and autoConnect flags are set. Q: Transactions stuck in pending - Check blockchain network congestion and relayer health page (dashboard). - Verify gas estimation and paymaster configuration if used.

Quickstart Checklist

  • Create an App on dashboard → Add Relayer → copy Relayer URL + API Key
  • Install @abstraxn/relayer in your server environment
  • Instantiate Relayer with secure credentials
  • buildRelayerTx()sendRelayerTx() or sendRelayerTxWithRealTimeUpdates()
  • Monitor via getRelayerTxStatus() or WebSocket events
  • Integrate with Smart Accounts, Paymaster, Bundler for full agent flows

TL;DR

The @abstraxn/relayer SDK enables agent-driven, gasless on-chain execution with both polling and real-time capabilities. Keep keys server-side, validate intent, and prefer WebSocket for best UX.