Install
$ agentstack add skill-openfort-xyz-agent-skills-backend-wallet ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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
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
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
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:
- API Key — Bearer token in
Authorizationheader (sk_test_...orsk_live_...) - 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
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
// 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:
- First call on a chain: registers EIP-7702 delegation on-chain (upgrades EOA to smart account), then sends transaction
- Subsequent calls: skips delegation, sends directly
- Multiple interactions are batched atomically in a single transaction (enabled by smart account delegation)
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:
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:
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:
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:
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:
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:
const privateKey = await openfort.accounts.evm.backend.export({
id: account.id,
})
// privateKey is hex string (no 0x prefix)
Delete
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
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
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
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.)
// 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
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:
- Kora provides fee payer address + blockhash
- Transaction built with Kora as fee payer
- Compute budget auto-estimated via
simulateTransaction(falls back to 200k CU) - User signs → Kora co-signs → RPC submits
- Confirmed via WebSocket subscription (60s timeout)
Send Raw Transaction (Pre-built Base64)
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)
// 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).
// 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.
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)
// 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
- Go to Openfort Dashboard → Policies
- Create a policy with your desired criteria rules
- Go to Fee Sponsorship → Create and link to the policy
- Copy the fee sponsorship ID (
pol_...) - Use it in your code:
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
// 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
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
- Source: openfort-xyz/agent-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.