Skip to main content

Integration Guide

Complete walkthrough for integrating SpendSafe into your autonomous agent. From dashboard setup to production deployment in under an hour.


What You'll Build

By the end of this guide, you'll have:

  • SpendSafe dashboard configured with spending policies
  • PolicyWallet SDK integrated into your agent code
  • Policy-enforced transactions working end-to-end
  • Production deployment checklist completed

Time required: 30 minutes


Prerequisites

  • Node.js 18+ installed
  • Existing autonomous agent or wallet integration
  • RPC provider access (Alchemy, Infura, QuickNode, or public RPC)

Step 1: Dashboard Setup

1.1 Create Account

  1. Visit app.spendsafe.ai
  2. Sign up with email or social login
  3. Verify email (check spam folder)

1.2 Create Organisation

  1. Click "Create Organisation"
  2. Enter organisation name (e.g., "Acme AI")
  3. Select plan tier (Free tier available for testing)
  4. Save

1.3 Create Agent

  1. Navigate to Agents → "Create Agent"
  2. Configure agent:
FieldValueNotes
NameCustomer Support BotDescriptive name for audit logs
DescriptionHandles refund requestsOptional context
  1. Click "Create Agent"
  2. Copy the API key (shown once)
    • Format: sk_live_xxxxxxxxxxxxx
    • Store in password manager immediately
    • You cannot view this key again

1.4 Configure Spending Policies

  1. Open agent detail page
  2. Click "Add Asset Rule"
  3. Configure limits:

Example: ETH on Ethereum Sepolia

SettingValueMeaning
ChainEthereum SepoliaTestnet for initial testing
AssetETHNative token
Daily Limit1 ETHMaximum per 24-hour period
Hourly Limit0.1 ETHMaximum per hour
Per-Transaction Limit0.01 ETHMaximum per single transaction
Max Transactions/Hour10Frequency throttle

Example: USDC on Base

SettingValueMeaning
ChainBaseProduction L2 with low fees
AssetUSDCStablecoin
Daily Limit1000 USDC$1,000 daily cap
Hourly Limit100 USDC$100 hourly cap
Per-Transaction Limit50 USDC$50 per transaction
Allowed Recipients0xTrust1..., 0xTrust2...Optional whitelist
  1. Click "Save Policy"
  2. Copy the policy hash shown next to each asset
    • Format: ba500a3fee18ba269cd...
    • Used for integrity verification (optional but recommended)

Step 2: Install SDK

npm install @spendsafe/sdk

What gets installed:

  • @spendsafe/sdk - PolicyWallet wrapper with built-in adapters

Adapter dependencies:

Choose the wallet SDK you're using and install its peer dependency:

# ethers.js (most popular)
npm install ethers

# viem (modern TypeScript)
npm install viem

# Solana
npm install @solana/web3.js

# Coinbase, Dynamic, Privy (see adapter docs)

See Adapter Overview for complete adapter documentation.


Step 3: Configure Environment

Create .env file in your project root:

# Wallet Configuration (stays local, never transmitted)
PRIVATE_KEY=0xabc123...
RPC_URL=https://... # Your RPC endpoint (chain-specific)

# SpendSafe API
SPENDSAFE_API_URL=https://api.spendsafe.ai
SPENDSAFE_API_KEY=sk_live_xxxxxxxxxxxxx

# Optional: Policy hash for integrity verification
SPENDSAFE_POLICY_HASH=ba500a3fee18ba269cd...

# Optional: Metadata for audit logs
ORG_ID=acme-ai
WALLET_ID=support-wallet
AGENT_ID=refunds-bot-001

# Environment
NODE_ENV=development

Security notes:

  • Add .env to .gitignore
  • Store API keys in secret manager (AWS Secrets Manager, HashiCorp Vault, Doppler)

Step 4: Integrate SDK

4.1 Create Wallet Factory

Create src/wallet.ts:

import { PolicyWallet } from '@spendsafe/sdk';
import { createYourAdapter } from '@spendsafe/sdk/adapters';
import * as dotenv from 'dotenv';

dotenv.config();

export async function createPolicyWallet() {
// 1. Create wallet adapter for your chosen SDK
// Replace with: createEthersAdapter, createViemAdapter, createSolanaAdapter, etc.
const adapter = await createYourAdapter({
privateKey: process.env.PRIVATE_KEY!,
rpcUrl: process.env.RPC_URL!,
});

// 2. Wrap with PolicyWallet
const wallet = new PolicyWallet(adapter, {
apiUrl: process.env.SPENDSAFE_API_URL!,
apiKey: process.env.SPENDSAFE_API_KEY!,

// Optional: Verify policy hash matches dashboard
expectedPolicyHash: process.env.SPENDSAFE_POLICY_HASH,

// Optional: Metadata for audit logs
metadata: {
orgId: process.env.ORG_ID!,
walletId: process.env.WALLET_ID!,
agentId: process.env.AGENT_ID!,
},

// Optional: Logging level
logLevel: process.env.NODE_ENV === 'production' ? 'error' : 'info',
});

return wallet;
}

Adapter examples:

// ethers.js
import { createEthersAdapter } from '@spendsafe/sdk/adapters';
const adapter = await createEthersAdapter(privateKey, rpcUrl);

// viem
import { createViemAdapter } from '@spendsafe/sdk/adapters';
const adapter = await createViemAdapter({ privateKey, rpcUrl });

// Solana
import { createSolanaAdapter } from '@spendsafe/sdk/adapters';
const adapter = await createSolanaAdapter(keypair, rpcUrl);

See Adapter Guides for complete documentation.

4.2 Send Your First Transaction

Create src/send-payment.ts:

import { createPolicyWallet } from './wallet';
import { PolicyError } from '@spendsafe/sdk';

async function main() {
const wallet = await createPolicyWallet();

try {
const result = await wallet.send({
chain: 'ethereum', // Lowercase chain name
asset: 'eth', // Lowercase asset symbol
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
amount: '10000000000000000', // 0.01 ETH in wei (string required)
memo: 'Test transaction #1', // Optional, included in audit logs
});

console.log('✅ Transaction successful');
console.log('Hash:', result.hash);
console.log('Gas fee:', result.fee.toString(), 'wei');
console.log('Remaining daily budget:', result.counters.remainingDaily);
console.log('Remaining hourly budget:', result.counters.remainingHourly);
console.log('Transactions this hour:', result.counters.txCountLastHour);

// Decision proof for audit reconciliation
console.log('Policy hash:', result.decisionProof.policyHash);
console.log('Signature:', result.decisionProof.signature);

} catch (error) {
if (error instanceof PolicyError) {
// Policy blocked the transaction
console.error('❌ Policy denied transaction');
console.error('Error code:', error.code);
console.error('Reason:', error.message);

// Inspect counters to understand why
if (error.details?.counters) {
console.error('Today spent:', error.details.counters.todaySpent);
console.error('Remaining daily:', error.details.counters.remainingDaily);
console.error('Remaining hourly:', error.details.counters.remainingHourly);
}

process.exit(1);
}

// Other errors: network failure, RPC timeout, insufficient funds
console.error('❌ Transaction failed:', error.message);
throw error;
}
}

main();

Run:

npx tsx src/send-payment.ts

Expected output (success):

✅ Transaction successful
Hash: 0xabc123...
Gas fee: 21000000000000 wei
Remaining daily budget: 990000000000000000
Remaining hourly budget: 90000000000000000
Transactions this hour: 1
Policy hash: ba500a3fee18ba269cd...
Signature: 0xdef456...

Expected output (policy denial):

❌ Policy denied transaction
Error code: POLICY_DECISION_DENY
Reason: Daily limit exceeded
Today spent: 1000000000000000000
Remaining daily: 0
Remaining hourly: 0

Step 5: Test Policy Enforcement

5.1 Test Transaction Approval

// Transaction within all limits
await wallet.send({
chain: 'ethereum',
asset: 'eth',
to: '0xRecipient...',
amount: '5000000000000000', // 0.005 ETH (under 0.01 ETH limit)
});
// Expected: ✅ Success

5.2 Test Daily Limit

// Attempt to exceed daily limit
try {
await wallet.send({
chain: 'ethereum',
asset: 'eth',
to: '0xRecipient...',
amount: '2000000000000000000', // 2 ETH (exceeds 1 ETH daily limit)
});
} catch (error) {
console.log(error.code); // 'POLICY_DECISION_DENY'
console.log(error.message); // 'Daily limit exceeded'
}

5.3 Test Per-Transaction Limit

// Exceed per-transaction limit
try {
await wallet.send({
chain: 'ethereum',
asset: 'eth',
to: '0xRecipient...',
amount: '50000000000000000', // 0.05 ETH (exceeds 0.01 ETH per-tx limit)
});
} catch (error) {
console.log(error.code); // 'POLICY_DECISION_DENY'
console.log(error.message); // 'Per-transaction limit exceeded'
}

5.4 Test Recipient Whitelist

// If whitelist configured, test non-whitelisted address
try {
await wallet.send({
chain: 'ethereum',
asset: 'eth',
to: '0xUnknownAddress...',
amount: '1000000000000000',
});
} catch (error) {
console.log(error.code); // 'POLICY_DECISION_DENY'
console.log(error.message); // 'Recipient not whitelisted'
}

5.5 Test Frequency Limit

// Exceed max transactions per hour
for (let i = 0; i < 15; i++) {
try {
await wallet.send({
chain: 'ethereum',
asset: 'eth',
to: '0xRecipient...',
amount: '1000000000000000', // 0.001 ETH
});
} catch (error) {
if (i >= 10) {
console.log(error.code); // 'POLICY_DECISION_DENY'
console.log(error.message); // 'Transaction frequency limit exceeded'
}
}
}

5.6 Verify in Dashboard

  1. Open agent → Transactions tab
  2. Verify all test transactions appear (approved and denied)
  3. Check decision reasons match SDK error codes
  4. Inspect counters (daily spent, hourly spent, transaction count)
  5. Verify policy hash matches your configuration

5.7 Use the Policy Simulator

Test policies safely without broadcasting transactions:

  1. Visit app.spendsafe.ai/simulator
  2. Choose mode:
    • Mock: Test with fake data (no agent required)
    • Live: Test with real agent policies and counters
  3. Configure transaction (asset, amount, recipient)
  4. View policy limits and current usage
  5. Click "Run Simulation"

Simulator features:

  • Dry-run transactions - Test without blockchain broadcast
  • Tampering detection - Simulate intent modification between gates
  • Expiry simulation - Test token expiry (65s delay)
  • Policy violations - See exactly why transactions are denied
  • Real-time feedback - Gate 1 and Gate 2 results with explanations
  • Quick scenarios - Pre-built test cases for common situations

Use the simulator to:

  • Understand policy behavior before coding
  • Debug policy denials
  • Learn how two-gate enforcement works
  • Test edge cases safely

Step 6: Production Error Handling

6.1 Comprehensive Error Handling

import { PolicyError } from '@spendsafe/sdk';

async function sendPaymentWithHandling(to: string, amount: string) {
try {
const result = await wallet.send({
chain: 'ethereum',
asset: 'eth',
to,
amount,
});

return { success: true, hash: result.hash };

} catch (error) {
if (error instanceof PolicyError) {
switch (error.code) {
case 'POLICY_DECISION_DENY':
// Policy blocked - log for audit, notify admin
await auditLog.record({
event: 'policy_denial',
reason: error.message,
counters: error.details?.counters,
});

await notifyAdmin(`Policy blocked transaction: ${error.message}`);
return { success: false, reason: 'policy_denied' };

case 'POLICY_HASH_MISMATCH':
// Configuration drift - alert DevOps
await alertDevOps({
message: 'Policy hash mismatch detected',
expectedHash: process.env.SPENDSAFE_POLICY_HASH,
actualHash: error.details?.policyHash,
});

return { success: false, reason: 'config_drift' };

case 'RATE_LIMIT_VALIDATE_INTENT':
// Rate limit - respect Retry-After header
const retryAfter = error.details?.retryAfter || 60;
console.log(`Rate limited. Retry after ${retryAfter}s`);

await sleep(retryAfter * 1000);
return sendPaymentWithHandling(to, amount); // Retry

case 'AUTH_INVALID':
case 'AUTH_EXPIRED':
// Token issue - retry with fresh token
console.log('Auth token invalid. Retrying...');
return sendPaymentWithHandling(to, amount);

default:
// Unknown policy error
await errorLog.record(error);
return { success: false, reason: 'policy_error' };
}
}

// Non-policy errors
if (error.code === 'INSUFFICIENT_FUNDS') {
await notifyFinance('Agent wallet balance too low');
return { success: false, reason: 'insufficient_funds' };
}

// Network/RPC errors - log and propagate for retry
await errorLog.record(error);
throw error;
}
}

Step 7: Multi-Asset & Multi-Chain

7.1 ERC-20 Token Transfers

// USDC transfer (6 decimals)
await wallet.send({
chain: 'ethereum',
asset: 'usdc',
to: '0xRecipient...',
amount: '1000000', // 1 USDC
});

// DAI transfer (18 decimals)
await wallet.send({
chain: 'ethereum',
asset: 'dai',
to: '0xRecipient...',
amount: '1000000000000000000', // 1 DAI
});

// Custom ERC-20 token (use contract address)
await wallet.send({
chain: 'ethereum',
asset: '0xYourTokenContract...',
to: '0xRecipient...',
amount: '1000000000000000000',
});

Important: Amounts must be strings in base units (smallest denomination):

  • ETH: 18 decimals → "1000000000000000000" = 1 ETH
  • USDC: 6 decimals → "1000000" = 1 USDC
  • WBTC: 8 decimals → "100000000" = 1 WBTC

7.2 Multiple Chains

// Ethereum mainnet
const ethWallet = new PolicyWallet(
await createEthersAdapter(
process.env.ETH_PRIVATE_KEY!,
'https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY'
),
{ apiKey: process.env.ETH_API_KEY! }
);

// Base mainnet
const baseWallet = new PolicyWallet(
await createEthersAdapter(
process.env.BASE_PRIVATE_KEY!,
'https://mainnet.base.org'
),
{ apiKey: process.env.BASE_API_KEY! }
);

// Send on different chains
await ethWallet.send({
chain: 'ethereum',
asset: 'eth',
to: '0x...',
amount: '1000000000000000000',
});

await baseWallet.send({
chain: 'base',
asset: 'usdc',
to: '0x...',
amount: '1000000',
});

Note: Each chain requires separate agent configuration in dashboard with chain-specific policies.


Step 8: Production Deployment

8.1 Create Production Agent

  1. Dashboard → Create new agent
  2. Configure production policies (stricter limits recommended)
  3. Copy production API key (sk_live_...)
  4. Copy production policy hash
  5. Configure production RPC (paid tier recommended for reliability)

8.2 Production Environment

# .env.production
SPENDSAFE_API_URL=https://api.spendsafe.ai
SPENDSAFE_API_KEY=sk_live_xxxxxxxxxxxxx
SPENDSAFE_POLICY_HASH=production-hash-value
RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY # Paid tier recommended
NODE_ENV=production

Security checklist:

  • API keys stored in secret manager (AWS Secrets Manager, Vault, Doppler)
  • Private keys encrypted at rest
  • Production RPC using paid tier (better reliability)
  • Policy hash verification enabled
  • Error logging configured
  • Rate limiting accounted for (100 req/min per org)

Troubleshooting

POLICY_HASH_MISMATCH

Cause: Dashboard policy changed since you copied hash Fix: Copy new hash from dashboard to .env or remove expectedPolicyHash

POLICY_DECISION_DENY - Recipient not whitelisted

Cause: Recipient not in allowed list Fix: Add recipient to dashboard whitelist or remove whitelist restriction

RATE_LIMIT_VALIDATE_INTENT

Cause: >100 requests/minute per organisation Fix: Implement exponential backoff, respect Retry-After header

AUTH_INVALID

Cause: Token tampered or expired Fix: Retry transaction (SDK generates new token automatically)

NETWORK_ERROR

Cause: Cannot reach SpendSafe API Fix: Check network connectivity, verify API URL correct

Debug Mode

// Enable verbose logging
const wallet = new PolicyWallet(adapter, {
logLevel: 'debug', // Logs all Gate 1/2 requests/responses
});

Next Steps


Support

Dashboard: app.spendsafe.ai Simulator: app.spendsafe.ai/simulator Documentation: docs.spendsafe.ai Email: support@spendsafe.ai

Need help? Email us with:

  • Error code + message
  • Timestamp + agent name
  • Decision proof (if available)