gearsCustom Tx Manager

Build your own transaction manager service with @revibase/transaction-manager and an Ed25519 keypair.

A custom transaction manager is a server-side signer.

It verifies transaction intents.

It enforces your allow/deny policy.

Then it signs Solana message bytes (Ed25519).

1

Install

Terminal
pnpm add @revibase/transaction-manager
2

Generate an Ed25519 keypair

Generate one keypair for the manager.

Keep the private key server-side only.

circle-info

Node 20+ recommended.

This script uses WebCrypto.

Terminal
node -e '
(async () => {
  const toBase58 = (bytes) => {
    const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
    let digits = [0];
    for (const b of bytes) {
      let carry = b;
      for (let j = 0; j < digits.length; ++j) {
        carry += digits[j] << 8;
        digits[j] = carry % 58;
        carry = (carry / 58) | 0;
      }
      while (carry) {
        digits.push(carry % 58);
        carry = (carry / 58) | 0;
      }
    }
    return digits.reverse().map((d) => alphabet[d]).join("");
  };

  const keyPair = await crypto.subtle.generateKey(
    { name: "Ed25519" },
    true,
    ["sign", "verify"]
  );

  const [pubRaw, privJwk] = await Promise.all([
    crypto.subtle.exportKey("raw", keyPair.publicKey),
    crypto.subtle.exportKey("jwk", keyPair.privateKey),
  ]);

  console.log({
    publicKey: toBase58(new Uint8Array(pubRaw)),
    privateKey: JSON.stringify(privJwk),
  });
})();
'

You’ll use:

  • publicKey in wallet settings.

  • privateKey as TX_MANAGER_PRIVATE_KEY in your server env.

3

Backend callback (/api/transactionManager/sign)

Expose a public HTTPS endpoint.

Example:

https://your-transaction-manager.com/sign

Backend examples

Expose:

  • POST /api/transactionManager/sign

Set env vars:

  • TX_MANAGER_PRIVATE_KEY: manager private key (JWK JSON string).

  • TX_MANAGER_PUBLIC_KEY: manager public key (base58).

  • TX_MANAGER_URL: public HTTPS URL of your signing endpoint.

  • RPC_URL: optional Solana RPC URL.

app/api/transactionManager/sign/route.ts
import { verifyTransaction } from "@revibase/transaction-manager";
import { createSolanaRpc, getBase58Encoder } from "gill";
import { enforcePolicies } from "@/lib/policy";

const rpc = createSolanaRpc(process.env.RPC_URL ?? "https://api.mainnet-beta.solana.com");

const transactionManagerConfig = {
  publicKey: process.env.TX_MANAGER_PUBLIC_KEY!, // base58
  url: process.env.TX_MANAGER_URL!, // public HTTPS URL of this endpoint
};

export async function POST(req: Request) {
  try {
    const { publicKey, payload } = (await req.json()) as {
      publicKey: string;
      payload: unknown[];
    };

    if (publicKey !== transactionManagerConfig.publicKey) {
      return Response.json({ error: "Invalid transaction manager public key" }, { status: 400 });
    }

    const jwk = JSON.parse(process.env.TX_MANAGER_PRIVATE_KEY!);
    const privateKey = await crypto.subtle.importKey(
      "jwk",
      jwk,
      { name: "Ed25519" },
      false,
      ["sign"],
    );

    const signatures: string[] = [];

    for (const payloadItem of payload) {
      const { transactionMessage, verificationResults } = await verifyTransaction(
        rpc,
        transactionManagerConfig,
        payloadItem,
      );

      await enforcePolicies(verificationResults);

      const signatureBytes = await crypto.subtle.sign(
        { name: "Ed25519" },
        privateKey,
        transactionMessage,
      );

      signatures.push(getBase58Encoder().encode(signatureBytes));
    }

    return Response.json({ signatures });
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e);
    return Response.json({ error: msg }, { status: 500 });
  }
}
4

Policy checks

Use verificationResults to enforce allowlists and limits.

circle-info

The example below allows only native SOL transfers coming from "https://app.revibase.com"

It rejects any non-system-program instruction.

It also caps each transfer at 1 SOL.

policy.ts
import type { VerificationResults } from "@revibase/transaction-manager";
import {
  SYSTEM_PROGRAM_ADDRESS,
  identifySystemInstruction,
  parseTransferSolInstruction,
  parseTransferSolWithSeedInstruction,
  SystemInstruction,
} from "gill";

const ALLOWED_ORIGINS = new Set(["https://app.revibase.com"]);
const MAX_TRANSFER_LAMPORTS = 1_000_000_000n; // 1 SOL

export async function enforcePolicies(results: VerificationResults) {
  const { signers, instructions } = results;

  for (const signer of signers) {
    const origin = signer.client?.origin;
    if (origin && !ALLOWED_ORIGINS.has(origin)) {
      throw new Error("Unauthorized app origin");
    }
  }

  for (const ix of instructions) {
    if (ix.programAddress !== SYSTEM_PROGRAM_ADDRESS) {
      throw new Error("Unauthorized program");
    }

    const ixKind = identifySystemInstruction(ix.data);

    if (
      ixKind !== SystemInstruction.TransferSol &&
      ixKind !== SystemInstruction.TransferSolWithSeed
    ) {
      throw new Error("Unauthorized instruction");
    }

    const parsed =
      ixKind === SystemInstruction.TransferSol
        ? parseTransferSolInstruction(ix)
        : parseTransferSolWithSeedInstruction(ix);

    if (parsed.data.amount > MAX_TRANSFER_LAMPORTS) {
      throw new Error("Transfer limit exceeded");
    }
  }
}

Typical checks:

  • Allow only specific app origins.

  • Allow only specific programs.

  • Enforce amount limits.

  • Block config changes.

5

Add Custom Transaction Manager

Open:

https://app.revibase.com/wallet/settings

Go to Settings.

Under Security, tap Edit.

6

Add a member

In Edit configuration, go to Members.

Tap Add.

7

Select Transaction Manager

Choose Transaction Manager.

8

Choose Custom Setup

Select Custom Setup (Advanced).

Enter your Public Signing Endpoint URL.

Enter your Public Key.

Tap Add to Wallet.

9

Save

Confirm this new member appears under Members.

Tap Save.

10

Make the manager the only member with Initiate permission.

This forces every transaction to include the manager signature.

In Edit configuration:

  • Disable Initiate for every other member.

  • Keep your Vote threshold.

  • Ensure at least one member can Execute.

Last updated