# Openfort Backend Wallets

> >

- **Type:** Skill
- **Install:** `agentstack add skill-openfort-xyz-agent-skills-backend-wallet`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [openfort-xyz](https://agentstack.voostack.com/s/openfort-xyz)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [openfort-xyz](https://github.com/openfort-xyz)
- **Source:** https://github.com/openfort-xyz/agent-skills/tree/main/skills/backend-wallet

## Install

```sh
agentstack add skill-openfort-xyz-agent-skills-backend-wallet
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Openfort Backend Wallets (Developer Custody)

> ⚠️ **Test keys vs. live keys — read before creating any wallet.**
> Openfort runs two fully isolated universes: **test mode** (`sk_test_…` / `pk_test_…`, testnet only) and **live mode** (`sk_live_…` / `pk_live_…`, mainnet, real funds). Objects created in one universe **cannot** be read, signed with, or recovered from the other.
> - A wallet created with a **test (dev) secret key exists only on testnet**. It is **not** usable in production and must **never** custody or move real funds.
> - To go to production, switch to your **live** secret key and create **fresh** wallets — never migrate or reuse test wallets across modes.
> - Secret keys (`sk_…`) are server-side only. Never commit them or expose them to a client.
> See https://openfort.io/docs/configuration/api-keys.

Backend wallets are server-controlled EOAs for automated blockchain operations — no user interaction required. Private keys are stored in **hardware-backed secure enclaves** and never leave the secure environment.

**When to use backend wallets** (vs embedded wallets):
- Server-side automation: treasury ops, batch minting, payroll, airdrops
- AI agent wallets: autonomous trading, payment processing
- Programmatic signing: no browser or user present
- Cross-border payments: automated stablecoin disbursement

**When to use embedded wallets instead:**
- User-facing wallets where the user controls the key
- Browser/mobile signing flows with user approval

## Setup

```bash
npm install @openfort/openfort-node
# EVM peer dependency (required for sendTransaction):
npm install viem
# Solana peer dependencies (required for Solana operations):
npm install @solana/kit @solana-program/system @solana-program/compute-budget @solana-program/token @solana/kora @solana/transaction-confirmation
```

### Environment Variables

```env
OPENFORT_SECRET_KEY=sk_test_...            # Secret API key (required)
OPENFORT_WALLET_SECRET=...              # Base64-encoded EC P-256 private key (required for mutations)
OPENFORT_PUBLISHABLE_KEY=pk_test_...    # Required for Solana operations (Kora gasless)
OPENFORT_BASE_URL=https://api.openfort.io  # Optional, defaults to production
```

### Initialize

```ts
import Openfort from '@openfort/openfort-node'

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
  publishableKey: process.env.OPENFORT_PUBLISHABLE_KEY,
})

// Or use env vars directly (auto-detected):
// OPENFORT_SECRET_KEY, OPENFORT_WALLET_SECRET, OPENFORT_PUBLISHABLE_KEY
const openfort = new Openfort()
```

### Authentication Model

All mutating backend wallet requests (`POST`, `DELETE`, `PUT` on `/accounts/backend/*`) are authenticated with **two layers**:

1. **API Key** — Bearer token in `Authorization` header (`sk_test_...` or `sk_live_...`)
2. **Wallet Auth (X-Wallet-Auth)** — Signed with the wallet secret. The SDK generates this automatically.

The SDK handles auth generation transparently — just provide `walletSecret` at init.

> **Important**: Wallet-auth requests are **not retried** on failure. All other requests use automatic retry with exponential backoff.

---

## EVM Backend Wallets

### Create

```ts
const account = await openfort.accounts.evm.backend.create({
  wallet: 'pla_...', // Optional — associates the wallet with a player
})
// account.id       — 'acc_...'
// account.address  — '0x...' (viem Address type)
// account.walletId — 'wal_...'
// account.custody  — 'Developer'
```

### List & Get

```ts
// List all EVM backend wallets (paginated)
const { accounts, total, nextPageToken } = await openfort.accounts.evm.backend.list({
  limit: 50,  // 1-100, default 10, optional
  skip: 0,    // optional
})

// Get by ID or address
const account = await openfort.accounts.evm.backend.get({ id: 'acc_...' })
// OR
const account = await openfort.accounts.evm.backend.get({ address: '0x...' })
// Throws AccountNotFoundError if not found
```

### Send Transaction (EVM — Gasless with EIP-7702)

`sendTransaction` handles the full EIP-7702 delegation + gasless flow automatically:

1. **First call on a chain**: registers EIP-7702 delegation on-chain (upgrades EOA to smart account), then sends transaction
2. **Subsequent calls**: skips delegation, sends directly
3. **Multiple interactions** are batched atomically in a single transaction (enabled by smart account delegation)

```ts
const account = await openfort.accounts.evm.backend.create()

const result = await openfort.accounts.evm.backend.sendTransaction({
  account: account,           // Required — account object from create() or get()
  chainId: 84532,             // Required — target chain ID (resolved via viem/chains)
  interactions: [              // Required — array of contract calls (batched atomically)
    {
      to: '0xRecipientAddress',  // Required — destination address
      value: '0',                // Optional, default '0' — Wei amount as string
      data: '0x',                // Optional, default '0x' — calldata
    },
    // Add more interactions for atomic batching
  ],
  policy: 'pol_...',              // Optional — fee sponsorship ID for gasless tx
  rpcUrl: 'https://sepolia.base.org',  // Optional — custom RPC (required for chains not in viem/chains)
})

console.log('TX Hash:', result.response?.transactionHash)
console.log('Status:', result.response?.status)
console.log('Gas Used:', result.response?.gasUsed)
```

> **Note**: If `chainId` is not found in `viem/chains` and no `rpcUrl` is provided, a `DelegationError` is thrown.

### Sign Data

The account object supports multiple signing methods:

```ts
const account = await openfort.accounts.evm.backend.get({ address: '0x...' })

// Sign a raw hash (32-byte hex)
const sig1 = await account.sign({ hash: '0xabcdef...' })

// Sign a human-readable message (EIP-191 personal_sign)
const sig2 = await account.signMessage({ message: 'Hello World' })

// Sign a serializable transaction
const sig3 = await account.signTransaction({
  to: '0x...',
  value: 100n,
  chainId: 84532,
})

// Sign EIP-712 typed data
const sig4 = await account.signTypedData({
  domain: { name: 'MyApp', version: '1', chainId: 84532 },
  types: { Transfer: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }] },
  primaryType: 'Transfer',
  message: { to: '0x...', amount: 100n },
})
```

Or use the lower-level API directly:

```ts
const signature = await openfort.accounts.evm.backend.sign({
  id: account.id,
  data: '0x...',  // hex-encoded data to sign
})
```

### Update to Delegated Account (EIP-7702)

Manually register EIP-7702 delegation without sending a transaction:

```ts
const delegatedAccount = await openfort.accounts.evm.backend.update({
  walletId: account.walletId,
  accountType: 'Delegated Account', // Required for EIP-7702 upgrade
  chainId: 84532,
  implementationType: 'Calibur',
  accountId: account.id,
})
```

### Import Private Key

The SDK handles E2E encryption internally — just provide the raw private key:

```ts
const imported = await openfort.accounts.evm.backend.import({
  privateKey: '0xYourPrivateKeyHex', // hex string (with or without 0x prefix)
})
// imported.id, imported.address
```

> **Under the hood**: The SDK encrypts your private key with RSA-OAEP (SHA-256) using the server's public key before transit. The server holds the corresponding private key in a KMS HSM (non-extractable).

#### Low-level encryption helpers (advanced)

For manual encryption workflows (e.g., custom import pipelines), the SDK also exports:

```ts
import {
  generateRSAKeyPair,
  encryptForImport,
  decryptExportedPrivateKey,
  IMPORT_ENCRYPTION_PUBLIC_KEY,
} from '@openfort/openfort-node'

// These are synchronous functions:
const keyPair = generateRSAKeyPair()              // Returns { publicKey, privateKeyPem }
const encrypted = encryptForImport('0xKey', IMPORT_ENCRYPTION_PUBLIC_KEY) // Returns base64 string
const decrypted = decryptExportedPrivateKey(encryptedBase64, keyPair.privateKeyPem) // Returns hex string
```

### Export Private Key

The SDK handles E2E decryption internally — returns the private key directly:

```ts
const privateKey = await openfort.accounts.evm.backend.export({
  id: account.id,
})
// privateKey is hex string (no 0x prefix)
```

### Delete

```ts
await openfort.accounts.evm.backend.delete(account.id)
// Permanently deletes wallet and private key — irreversible
```

---

## Solana Backend Wallets

> **All Solana transactions are gasless by default** via Kora fee payer protocol. The user's wallet never needs SOL for gas. Requires `publishableKey` to be configured.

### Create

```ts
const account = await openfort.accounts.solana.backend.create({
  wallet: 'pla_...', // Optional — associates the wallet with a player
})
// account.id      — 'acc_...'
// account.address — Base58 Solana address
// account.custody — 'Developer'
```

### List & Get

```ts
const { accounts, total, nextPageToken } = await openfort.accounts.solana.backend.list({
  limit: 50,  // 1-100, default 10, optional
  skip: 0,    // optional
})

const account = await openfort.accounts.solana.backend.get({
  address: 'Base58Address...',
})
// Also: get({ id: 'acc_...' })
// Throws AccountNotFoundError if not found
```

### Transfer SOL

```ts
const result = await account.transfer({
  to: 'FDx9mfVqTvXUaSPQDELwDtGgMqxirmAFsEK2s4YsKfsc',
  amount: 1_000_000n,       // In lamports (1 SOL = 1_000_000_000 lamports)
  cluster: 'devnet',         // 'devnet' | 'mainnet-beta'
  // token defaults to 'sol'
  computeUnitLimit: 200_000,   // Optional — auto-estimated via simulation if omitted
  computeUnitPrice: 50_000n,   // Optional — micro-lamports priority fee
})
console.log('Signature:', result.signature)
```

### Transfer SPL Tokens (USDC, etc.)

```ts
// By token name
const usdcResult = await account.transfer({
  to: 'FDx9...',
  amount: 1_000_000n,  // In token base units (USDC: 6 decimals)
  token: 'usdc',
  cluster: 'devnet',
})

// By mint address
const splResult = await account.transfer({
  to: 'FDx9...',
  amount: 2_000_000n,
  token: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU', // Mint address
  cluster: 'devnet',
})
```

### Send Transaction with Instructions

```ts
import { getTransferSolInstruction } from '@solana-program/system'
import { address, createNoopSigner } from '@solana/kit'

const account = await openfort.accounts.solana.backend.get({
  address: 'o24A5URLU3JNKg7AoeUrPsfsAo1NQeeAB4uQViAkpjq',
})

const ix = getTransferSolInstruction({
  source: createNoopSigner(address(account.address)),
  destination: address('FDx9mfVqTvXUaSPQDELwDtGgMqxirmAFsEK2s4YsKfsc'),
  amount: 10n,
})

const result = await openfort.accounts.solana.backend.sendTransaction({
  account,
  cluster: 'devnet',
  instructions: [ix],
  // computeUnitLimit — auto-estimated via simulation if omitted
  // computeUnitPrice — defaults to 50_000n micro-lamports
  // rpcUrl, wsUrl — custom endpoints (optional)
})
console.log('Signature:', result.signature)
```

**Gasless flow under the hood:**
1. Kora provides fee payer address + blockhash
2. Transaction built with Kora as fee payer
3. Compute budget auto-estimated via `simulateTransaction` (falls back to 200k CU)
4. User signs → Kora co-signs → RPC submits
5. Confirmed via WebSocket subscription (60s timeout)

### Send Raw Transaction (Pre-built Base64)

```ts
const result = await openfort.accounts.solana.backend.sendRawTransaction({
  account,
  cluster: 'devnet',
  transaction: base64EncodedTransaction,
})
// Internally decompiles, extracts instructions, re-wraps in gasless flow
```

### Sign Data (Solana)

```ts
// Lower-level API
const signature = await openfort.accounts.solana.backend.sign(
  account.id,
  'hex_encoded_data',
)

// Account object methods
const sig1 = await account.signMessage({ message: 'Hello Solana' })
const sig2 = await account.signTransaction({ transaction: base64Tx })
```

### Import / Export (Solana)

Same simplified flow as EVM — encryption is handled internally. Solana import accepts base58, hex with 0x, or raw hex. Auto-expands 32-byte seeds to 64-byte keypairs. Export returns base58 (standard Solana format).

```ts
// Import
const imported = await openfort.accounts.solana.backend.import({
  privateKey: '4YFq9y5f5hi77Bq8kDCE6VgqoAq...', // base58, hex with 0x, or raw hex
})

// Export
const privateKey = await openfort.accounts.solana.backend.export({
  id: account.id,
})
// privateKey is base58-encoded (standard Solana format)
```

---

## Gas Sponsorship (Fee Sponsorship)

Gasless transactions require a **two-step setup**: create a policy (rules), then create a fee sponsorship (strategy) linked to that policy. This can be done via the SDK or the [Openfort Dashboard](https://dashboard.openfort.io).

### Strategy Types

| Strategy | Description |
|----------|-------------|
| `pay_for_user` | Developer fully sponsors gas — user pays nothing |
| `charge_custom_tokens` | User pays in ERC-20 tokens (fixed or dynamic exchange rate) |
| `fixed_rate` | User pays a fixed token amount per transaction |

### Create via SDK (Programmatic)

```ts
// Step 1: Create a policy with criteria rules
const policy = await openfort.policies.create({
  scope: 'project',          // 'project' (all accounts) or 'account' (single account)
  description: 'Sponsor gas on Base for USDC contract',
  rules: [{
    action: 'accept',
    operation: 'sponsorEvmTransaction',
    criteria: [
      { type: 'evmNetwork', operator: 'in', chainIds: [8453] },
      { type: 'evmAddress', operator: 'in', addresses: ['0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'] },
    ],
  }],
})

// Step 2: Create a fee sponsorship linked to that policy
const sponsorship = await openfort.feeSponsorship.create({
  name: 'Base USDC Gas Sponsorship',
  strategy: { sponsorSchema: 'pay_for_user' },
  policyId: policy.id,
})

// Step 3: Use sponsorship ID in transactions
await openfort.accounts.evm.backend.sendTransaction({
  account,
  chainId: 8453,
  interactions: [{ to: '0x...', value: '0', data: '0x...' }],
  policy: sponsorship.id,  // pol_... from sponsorship
})
```

### Create via Dashboard

1. Go to [Openfort Dashboard](https://dashboard.openfort.io) → **Policies**
2. Create a policy with your desired criteria rules
3. Go to **Fee Sponsorship** → Create and link to the policy
4. Copy the fee sponsorship ID (`pol_...`)
5. Use it in your code:

```ts
await openfort.accounts.evm.backend.sendTransaction({
  account,
  chainId: 84532,
  interactions: [{ to: '0x...', value: '0', data: '0x' }],
  policy: process.env.OPENFORT_FEE_SPONSORSHIP_ID!, // pol_... from dashboard
})
```

> **Auto-discovery**: When no explicit `policy` is passed to `sendTransaction`, project-scoped fee sponsorships are auto-discovered and the first matching one is applied.

### Fee Sponsorship CRUD

```ts
// List
const sponsorships = await openfort.feeSponsorship.list()

// Get
const sponsorship = await openfort.feeSponsorship.get('pol_...')

// Update
await openfort.feeSponsorship.update('pol_...', { name: 'New Name' })

// Enable / Disable
await openfort.feeSponsorship.disable('pol_...')
await openfort.feeSponsorship.enable('pol_...')

// Delete (soft delete)
await openfort.feeSponsorship.delete('pol_...')
```

### Charge Custom Tokens Strategy

```ts
const sponsorship = await openfort.feeSponsorship.create({
  name: 'Pay gas with USDC',
  strategy: {
    sponsorSchema: 'charge_custom_tokens',
    tokenContract: 'con_...',        // Contract ID from dashboard
    tokenContractAmount: '1000000',  // Amount in token base units
  },
  policyId: policy.id,
})
```

---

## Policy Engine

Policies define rules that control which operations are allowed or rejected. They are evaluated server-side before any signing or transaction execution.

**Key concepts:**
- **Scope**: `'project'` (all accounts) or `'account'` (single account)
- **Priority**: Higher priority policies evaluated first
- **Fail-closed**: No matching rule = operation rejected
- **Rules**: Each policy has 1-10 ru

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [openfort-xyz](https://github.com/openfort-xyz)
- **Source:** [openfort-xyz/agent-skills](https://github.com/openfort-xyz/agent-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-openfort-xyz-agent-skills-backend-wallet
- Seller: https://agentstack.voostack.com/s/openfort-xyz
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
