Skip to main content

Use Openfort Backend Wallets

Openfort backend wallets are developer-controlled EOAs whose private keys are held by Openfort. Your server calls a typed SDK to sign hashes, messages, EIP-712 typed data, and transactions — the key never leaves Openfort’s infrastructure. This makes them a clean fit for autonomous agents, treasuries, and any server-side automation on SKALE.
Openfort’s Account Abstraction features (paymasters, EIP-7702 delegated accounts) are not currently available on SKALE Chains. This guide uses the EOA signing path: account.signTransaction(...) followed by eth_sendRawTransaction via viem. Gas on SKALE Base is paid in CREDIT — fund the wallet once and it can transact freely.

Prerequisites

Overview

Wallet typeControlBest for
Backend walletDeveloper (server-side API)Agents, treasury, automation, server signing
Embedded walletEnd user (auth + recovery)Consumer apps where the user holds the keys
Backend wallets give you programmatic signing without operating an HSM or rolling your own KMS. The Openfort Node SDK exposes sign, signMessage, signTypedData, and signTransaction directly on the account object returned by create().

Implementation

1

Install dependencies

npm install @openfort/openfort-node viem dotenv
2

Configure environment variables

From the Openfort dashboard, grab a secret API key (sk_test_... or sk_live_...) and generate a wallet secret. The wallet secret is a base64-encoded EC P-256 private key — Openfort holds the matching public key to verify your signing requests.Create a .env file:
OPENFORT_API_KEY=sk_test_...
OPENFORT_WALLET_SECRET=...
OPENFORT_ACCOUNT_ID=     # leave blank for now; you'll fill it in after step 4
Never commit the wallet secret. It authenticates every backend wallet write operation — treat it like a root credential.
3

Define the SKALE chain

Create chain.ts:
import { defineChain } from "viem";

export const skaleBaseSepolia = defineChain({
  id: 324705682,
  name: "SKALE Base Sepolia",
  nativeCurrency: { name: "Credits", symbol: "CREDIT", decimals: 18 },
  rpcUrls: {
    default: {
      http: ["https://base-sepolia-testnet.skalenodes.com/v1/jubilant-horrible-ancha"],
    },
  },
  blockExplorers: {
    default: {
      name: "SKALE Base Sepolia Explorer",
      url: "https://base-sepolia-testnet-explorer.skalenodes.com",
    },
  },
  testnet: true,
});
For mainnet, swap id: 1187947933, rpcUrls.default.http: ["https://skale-base.skalenodes.com/v1/base"], and testnet: false.
4

Create a backend wallet

Create create-wallet.ts:
import "dotenv/config";
import Openfort from "@openfort/openfort-node";

const openfort = new Openfort(process.env.OPENFORT_API_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
});

const account = await openfort.accounts.evm.backend.create();
console.log("Account ID:    ", account.id);
console.log("Account address:", account.address);
Run it:
npx tsx create-wallet.ts
Two things to record from the output:
  • Address (0x…) — where you’ll send CREDIT in the next step.
  • Account ID (acc_…) — the stable identifier for this wallet across runs. Paste it into OPENFORT_ACCOUNT_ID in your .env. Subsequent scripts retrieve the same wallet via openfort.accounts.evm.backend.get({ id: process.env.OPENFORT_ACCOUNT_ID }) — without it, every run would create a new wallet.
You can also find the ID later via openfort.accounts.evm.backend.list() or by looking the wallet up in the Openfort dashboard.
5

Fund the wallet with CREDIT

On testnet, request CREDIT for the address at the SKALE Base Sepolia faucet. On mainnet, buy CREDIT at base.skalenodes.com/credits and transfer to the wallet address.
6

Sign and broadcast a transaction

Create send-transaction.ts:
import "dotenv/config";
import Openfort from "@openfort/openfort-node";
import {
  createPublicClient,
  http,
  parseEther,
  type TransactionSerializedEIP1559,
} from "viem";
import { skaleBaseSepolia } from "./chain";

const openfort = new Openfort(process.env.OPENFORT_API_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
});

const publicClient = createPublicClient({
  chain: skaleBaseSepolia,
  transport: http(),
});

// Reuse the wallet created in the previous step (ID is in .env).
const account = await openfort.accounts.evm.backend.get({
  id: process.env.OPENFORT_ACCOUNT_ID!,
});

const nonce = await publicClient.getTransactionCount({
  address: account.address,
});
const { maxFeePerGas, maxPriorityFeePerGas } =
  await publicClient.estimateFeesPerGas();

const signedTx = await account.signTransaction({
  to: "0x000000000000000000000000000000000000dEaD",
  value: parseEther("0.0001"),
  nonce,
  gas: 21000n,
  maxFeePerGas,
  maxPriorityFeePerGas,
  chainId: skaleBaseSepolia.id,
});

const hash = await publicClient.sendRawTransaction({
  serializedTransaction: signedTx as TransactionSerializedEIP1559,
});
console.log("Tx hash:", hash);
console.log(
  "Explorer:",
  `${skaleBaseSepolia.blockExplorers.default.url}/tx/${hash}`,
);

const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log("Mined in block:", receipt.blockNumber, "status:", receipt.status);
account.signTransaction(...) returns a 0x02...-prefixed serialized EIP-1559 transaction — the same wire format every Ethereum client produces. Broadcasting means submitting that signed payload to the network over JSON-RPC (eth_sendRawTransaction) so validators see it, validate it, and include it in a block. The signature proves the wallet authorized the transaction; broadcasting is what puts it onchain. viem’s sendRawTransaction does the JSON-RPC call for you.The explorer link the script prints is your own verification — open it and you’ll see the transaction, the signer address, the value, and the block it was included in.
7

Sign messages and EIP-712 typed data

The same account exposes signMessage (EIP-191) and signTypedData (EIP-712), useful for agent attestations and offchain auth:
const signature = await account.signMessage({
  message: "agent-id:42 acting on behalf of user:alice",
});

const typedSignature = await account.signTypedData({
  domain: { name: "MyApp", version: "1", chainId: skaleBaseSepolia.id },
  types: {
    Order: [
      { name: "from", type: "address" },
      { name: "amount", type: "uint256" },
    ],
  },
  primaryType: "Order",
  message: { from: account.address, amount: 1000n },
});

Confine the wallet with a policy

When a backend wallet drives an agent, the SDK credentials sit on a server the agent has access to. A policy turns the Openfort API itself into a guardrail: signing requests that don’t match an allowlist are rejected server-side, regardless of what the agent code asks for.
Openfort’s evmNetwork policy criterion is currently limited to chains where Openfort runs Account Abstraction — SKALE chain IDs aren’t accepted there yet. Use the chain-agnostic criteria (evmAddress, ethValue, evmData) instead, which describe the transaction’s content rather than the network. Your code already pins the chain via defineChain, so the agent can’t redirect to another network without changing your code.
Create policy.ts:
import "dotenv/config";
import Openfort from "@openfort/openfort-node";
import { parseEther } from "viem";

const openfort = new Openfort(process.env.OPENFORT_API_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
});

const ALLOWED_RECIPIENT = "0x0000000000000000000000000000000000000001";

// Account-scoped policy: rules are evaluated in order; the first match wins.
const policy = await openfort.policies.create({
  scope: "account",
  accountId: "acc_...",         // the backend wallet you want to confine
  description: "Agent allowlist: single recipient, value <= 0.001 ETH",
  enabled: true,
  rules: [
    {
      action: "accept",
      operation: "signEvmTransaction",
      criteria: [
        {
          type: "evmAddress",
          operator: "in",
          addresses: [ALLOWED_RECIPIENT],
        },
        {
          type: "ethValue",
          operator: "<=",
          ethValue: parseEther("0.001").toString(),
        },
      ],
    },
    // Default deny — anything not matched above is rejected.
    {
      action: "reject",
      operation: "signEvmTransaction",
      criteria: [],
    },
  ],
});

console.log("Policy:", policy.id);
Once the script runs, the policy is visible in the Openfort dashboard — same place you’d inspect it, edit it, or disable it without code. Dry-run before deploying. openfort.policies.evaluate() reports what the policy would decide for a hypothetical operation without signing anything:
const decision = await openfort.policies.evaluate({
  operation: "signEvmTransaction",
  accountId: "acc_...",
  payload: {
    chainId: skaleBaseSepolia.id,
    to: ALLOWED_RECIPIENT,
    value: parseEther("0.0005").toString(),
  },
});
console.log(decision.allowed, decision.reason, decision.matchedRuleId);
Enforcement. With the policy active, account.signTransaction(...) calls that violate the rules fail with a Forbidden API error before any signature is produced. The agent cannot route around it.
Criterion typeWhat it checks
evmAddressThe transaction’s to against an allow/deny list
ethValueNative value (wei) against a numeric bound
evmDataCalldata against a pattern (e.g. specific function selectors)
evmMessagesignEvmMessage text against an RE2 regex
evmTypedDataFieldEIP-712 field values for signEvmTypedData
A common pattern is one policy per agent role: a “settlement” wallet allowlists the payment contract, a “gas-refill” wallet allowlists a single treasury address, and so on. Update or disable a policy at any time via openfort.policies.update(id, ...).

What you’ve built

A SKALE-connected agent or service can now:
  • Create a wallet on demand via the Openfort API
  • Sign and broadcast transactions on SKALE using standard EIP-1559 RLP — the same wire format every Ethereum client uses
  • Sign offchain messages and EIP-712 payloads for attestations or session auth
  • Operate under a policy that rejects out-of-bounds signing requests server-side, before any signature is produced
Each backend wallet is a standalone EOA — spin up one per agent, per tenant, or per workflow, each with its own policy.

Next steps

  • Openfort Node SDK — full SDK reference, including signing policies and key rotation
  • SKALE Chains — chain IDs and RPC endpoints for every active SKALE Chain
  • Build an Agent — pair a backend wallet with x402 payments for autonomous services