Package Exports
- @policylayer/sdk
- @policylayer/sdk/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@policylayer/sdk) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
@policylayer/sdk
Non-custodial spending controls for AI agent wallets
AI agents need wallet access to make payments. But unrestricted access means one bug, one prompt injection, or one infinite loop away from draining your entire treasury.
PolicyLayer enforces spending limits on AI agent wallets without holding your private keys. Your keys stay on your server. We just say yes or no.
Installation
npm install @policylayer/sdkPeer dependencies: If using the Ethers adapter, install ethers@^6.0.0.
Quick Start
import { PolicyWallet, createEthersAdapter } from '@policylayer/sdk';
import { Wallet, JsonRpcProvider } from 'ethers';
// Your existing wallet setup
const provider = new JsonRpcProvider(process.env.RPC_URL);
const signer = new Wallet(process.env.PRIVATE_KEY, provider);
// Wrap with policy enforcement
const adapter = createEthersAdapter(signer);
const wallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY,
});
// Send with automatic limit enforcement
const result = await wallet.send({
chain: 'base',
asset: 'usdc',
to: '0xRecipient...',
amount: '1000000', // 1 USDC (6 decimals)
});
console.log(`Sent: ${result.hash}`);If the transaction exceeds your configured limits, it throws a PolicyError before signing. No gas wasted. No funds lost.
Features
- Spending limits — Per-transaction, daily, and hourly caps
- Recipient whitelists — Only send to approved addresses
- Non-custodial — Private keys never leave your infrastructure
- Two-gate enforcement — Cryptographic tamper detection
- Audit trail — Complete proof of every policy decision
- Multi-chain — Ethereum, Base, Solana, Polygon, Arbitrum, and more
- Any wallet SDK — Works with ethers, viem, Coinbase, Privy, Dynamic
Supported Adapters
| Adapter | Use Case | Factory Function |
|---|---|---|
| Ethers | ethers.js v6 | createEthersAdapter(signer) |
| Viem | viem library | createViemAdapter(walletClient, publicClient) |
| Solana | Solana blockchain | createSolanaAdapter(keypair, connection) |
| Coinbase | Coinbase CDP | createCoinbaseAdapter(walletData, options) |
| Privy | Privy embedded wallets | createPrivyAdapter(...) |
| Dynamic | Dynamic.xyz | createDynamicAdapter(...) |
API Reference
PolicyWallet
const wallet = new PolicyWallet(adapter, config);
// Core methods
await wallet.send(intent); // Send with policy enforcement
await wallet.getAddress(); // Get wallet address
await wallet.getBalance(token?); // Get balance (native or token)
wallet.getLatestDecisionProof(); // Get audit proof of last decision
wallet.getAdapter(); // Access underlying adapterConfiguration
interface PolicyConfig {
apiUrl: string; // Required: Policy API endpoint
apiKey: string; // Required: Your API key from dashboard
// Optional
skipConfirmation?: boolean; // Skip waiting for block confirmation
expectedPolicyHash?: string; // Verify policy hasn't changed
logLevel?: 'silent' | 'error' | 'warn' | 'info' | 'debug';
metadata?: { // Custom metadata passed to API
orgId?: string;
walletId?: string;
agentId?: string;
};
fetch?: typeof fetch; // Custom fetch (required in Node <18)
}SendIntent
interface SendIntent {
chain: string; // 'ethereum', 'base', 'solana', etc.
asset: string; // 'eth', 'usdc', 'sol', etc.
to: string; // Recipient address
amount: string; // Amount in smallest unit (wei, lamports)
memo?: string; // Optional memo
}SendResult
interface SendResult {
hash: string; // Transaction hash
fee: bigint; // Gas fee paid
allowed: boolean; // Whether policy approved
counters?: PolicyCounters; // Remaining budget info
decisionProof?: DecisionProof; // Cryptographic proof
}Error Handling
import { PolicyWallet, PolicyError } from '@policylayer/sdk';
try {
await wallet.send(intent);
} catch (error) {
if (error instanceof PolicyError) {
console.log(`Blocked: ${error.message}`);
console.log(`Code: ${error.code}`);
// For policy denials, the reason is in the message
if (error.code === 'POLICY_DECISION_DENY') {
// error.message contains: "Transaction blocked: DAILY_LIMIT"
// error.details.counters has remaining budget info
console.log(`Remaining: ${error.details?.counters?.remainingDaily}`);
}
}
}Error Codes
SDK errors (thrown by the SDK itself):
| Code | Meaning |
|---|---|
POLICY_DECISION_DENY |
Policy rejected the transaction |
POLICY_DECISION_REVIEW |
Transaction requires manual review |
INTENT_FINGERPRINT_MISMATCH |
Transaction was tampered with |
AUTH_EXPIRED |
Approval token expired (60s window) |
AUTH_CONSUMED |
Token already used (replay prevention) |
NETWORK_ERROR |
Failed to reach policy API |
INVALID_AMOUNT_FORMAT |
Amount must be whole-number string |
Policy denial reasons (in error.message when code is POLICY_DECISION_DENY):
| Reason | Meaning |
|---|---|
DAILY_LIMIT |
Daily spending cap reached |
PER_TX_LIMIT |
Single transaction too large |
HOURLY_LIMIT |
Hourly rate limit hit |
TX_FREQUENCY_LIMIT |
Too many transactions per hour |
RECIPIENT_NOT_WHITELISTED |
Address not on approved list |
How It Works
Agent → Gate 1 (Validate) → Gate 2 (Verify) → Sign & Broadcast
↓ ↓
Check limits Detect tampering
Reserve amount Single-use tokenGate 1: SDK sends transaction intent to PolicyLayer API. Policy engine checks limits, reserves budget, returns signed approval token with intent fingerprint.
Gate 2: Before signing, SDK verifies the approval token. If the intent was modified after approval, fingerprints won't match → rejected.
Execute: Only after both gates pass does the SDK sign with your local private key and broadcast.
Security guarantees:
- Intent fingerprinting detects any modification
- Single-use tokens prevent replay attacks
- Keys never transmitted to PolicyLayer
Amount Formatting
Amounts must be strings in the smallest unit (wei for ETH, lamports for SOL):
// USDC has 6 decimals
amount: '1000000' // = 1 USDC ✓
amount: '1' // = 0.000001 USDC ✗
// ETH has 18 decimals
amount: '1000000000000000000' // = 1 ETH ✓Use parseUnits from ethers for convenience:
import { parseUnits } from 'ethers';
await wallet.send({
chain: 'base',
asset: 'usdc',
to: '0x...',
amount: parseUnits('100', 6).toString(), // 100 USDC
});TypeScript Support
Full TypeScript support with exported types:
import {
PolicyWallet,
PolicyConfig,
PolicyError,
SendIntent,
SendResult,
DecisionProof,
WalletAdapter,
} from '@policylayer/sdk';Links
License
MIT