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/sdkDependencies by adapter:
| Adapter | Additional Install |
|---|---|
| Ethers | npm install ethers@^6 |
| Viem | — (bundled) |
| Solana | — (bundled: @solana/web3.js) |
| Coinbase | — (bundled: @coinbase/coinbase-sdk) |
| Privy | — (bundled: @privy-io/node) |
| Dynamic | npm install @dynamic-labs/sdk-react-core @dynamic-labs/ethereum |
Note: The SDK bundles Solana, Coinbase, and Privy dependencies. If you only use Ethers/Viem and want a lighter install, use
npm install --ignore-scriptsor wait for our upcoming modular packages (@policylayer/sdk-core,@policylayer/adapter-ethers, etc.).
Runtime requirements:
- Node.js 18+ — Uses Node's
cryptomodule - Browser: Requires Node polyfills (crypto, buffer) if bundling for browser. For pure browser use, run PolicyWallet calls server-side or use a framework with Node polyfills.
Module format: CJS only. Works with webpack, esbuild, vite (with Node polyfills for browser builds).
Quick Start by Adapter
Ethers
import { PolicyWallet, createEthersAdapter } from '@policylayer/sdk';
const adapter = await createEthersAdapter(
process.env.PRIVATE_KEY!,
process.env.RPC_URL!
);
const wallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY!,
});
await wallet.send({
chain: 'ethereum',
asset: 'eth',
to: '0xRecipient...',
amount: '1000000000000000000', // 1 ETH in wei
});Viem
import { PolicyWallet, createViemAdapter } from '@policylayer/sdk';
import { mainnet } from 'viem/chains';
const adapter = await createViemAdapter(
'0xPrivateKey...' as `0x${string}`,
process.env.RPC_URL!,
mainnet
);
const wallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY!,
});Solana
import { PolicyWallet, createSolanaAdapter } from '@policylayer/sdk';
const adapter = await createSolanaAdapter(
process.env.SOLANA_PRIVATE_KEY!, // base58 encoded
'https://api.mainnet-beta.solana.com'
);
const wallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY!,
});
await wallet.send({
chain: 'solana',
asset: 'sol',
to: 'RecipientPubkey...',
amount: '1000000000', // 1 SOL in lamports
});Coinbase CDP
import { PolicyWallet, createNewCoinbaseWallet } from '@policylayer/sdk';
// Create a new wallet (handles Coinbase.configure internally)
const { adapter, walletData } = await createNewCoinbaseWallet(
{
apiKeyName: process.env.COINBASE_API_KEY_NAME!,
privateKey: process.env.COINBASE_PRIVATE_KEY!,
},
'base-mainnet'
);
// Save walletData for future sessions (contains wallet seed)
console.log('Save this:', JSON.stringify(walletData));
const wallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY!,
});
// To restore an existing wallet:
// import { createCoinbaseAdapter } from '@policylayer/sdk';
// const adapter = await createCoinbaseAdapter(savedWalletData, 'base-mainnet');Privy
import { PolicyWallet, createPrivyAdapter } from '@policylayer/sdk';
const adapter = await createPrivyAdapter({
appId: process.env.PRIVY_APP_ID!,
appSecret: process.env.PRIVY_APP_SECRET!,
walletId: 'user-wallet-id',
address: '0xWalletAddress',
rpcUrl: 'https://eth.llamarpc.com',
chainId: 1,
});
const wallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY!,
});Dynamic
import { PolicyWallet, createDynamicAdapter } from '@policylayer/sdk';
const adapter = await createDynamicAdapter(
walletClient, // from Dynamic SDK
publicClient, // viem PublicClient
);
const wallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY!,
});Production Hardening
Strongly recommended for production:
const wallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY!,
// Verify API responses are genuinely from PolicyLayer
decisionSigningKeys: [process.env.POLICYLAYER_PUBLIC_KEY!],
// Detect if someone swapped your policy
expectedPolicyHash: 'sha256:abc123...',
// Silence logs in production
logLevel: 'silent',
});Security defaults (if not configured):
decisionSigningKeys: None — API responses accepted without signature verification (TLS only)expectedPolicyHash: None — any policy acceptedlogLevel:'info'— logs Gate 1/2 status to console
Where to find these values:
decisionSigningKeys: Dashboard → Settings → Decision Signing Key (copy the public key)expectedPolicyHash: Dashboard → Policies → click policy → copy hash from details panel
Checklist:
- Set
decisionSigningKeyswith your org's public key from dashboard - Set
expectedPolicyHashto your policy's hash - Set
logLevel: 'silent'in production - Gate 2 errors are truncated to 200 chars in logs (no sensitive data leaked)
Asset Binding
For non-native assets (ERC-20, SPL tokens), provide tokenAddress:
await wallet.send({
chain: 'ethereum',
asset: 'usdc',
to: '0xRecipient...',
amount: '1000000', // 1 USDC
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC contract
});Note: tokenAddress is included in the intent fingerprint. If you approve a transaction for one token contract, it cannot be swapped for a different contract.
Asset Resolver
For convenience, configure an asset resolver to avoid passing tokenAddress every time:
const wallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY!,
assetResolver: (asset, chain) => {
const tokens: Record<string, Record<string, string>> = {
ethereum: {
usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
usdt: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
},
base: {
usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
},
};
return tokens[chain]?.[asset];
},
});
// Now tokenAddress is resolved automatically
await wallet.send({ chain: 'base', asset: 'usdc', to: '0x...', amount: '1000000' });Chain Binding
The SDK verifies your adapter's chain matches the intent's chain before signing.
Default chain IDs:
| Chain | ID |
|---|---|
| ethereum | 1 |
| base | 8453 |
| base-sepolia | 84532 |
| polygon | 137 |
| arbitrum | 42161 |
| optimism | 10 |
| sepolia | 11155111 |
| solana | 101 |
Override for custom chains:
const wallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY!,
chainIds: {
'blast-mainnet': 81457,
'zksync-era': 324,
'scroll': 534352,
},
});
await wallet.send({ chain: 'blast-mainnet', asset: 'eth', to: '0x...', amount: '...' });Timeouts & Retries
Defaults:
timeoutMs: 30000 (30 seconds)maxRetries: 2 (total attempts = 3)- Worst-case latency: ~90 seconds with exponential backoff
For low-latency agents:
const wallet = new PolicyWallet(adapter, {
apiUrl: 'https://api.policylayer.com',
apiKey: process.env.POLICYLAYER_API_KEY!,
timeoutMs: 8000, // Fail fast
maxRetries: 0, // No retries
});Error Handling
import { PolicyError } from '@policylayer/sdk';
try {
await wallet.send(intent);
} catch (error) {
if (error instanceof PolicyError) {
switch (error.code) {
case 'POLICY_DECISION_DENY':
console.log('Blocked by policy:', error.message);
console.log('Remaining budget:', error.details?.counters?.remainingDaily);
break;
case 'ADAPTER_TX_FAILED':
console.log('Transaction failed:', error.message);
console.log('Provider error:', error.details?.cause);
break;
case 'NETWORK_ERROR':
console.log('API unreachable, retry later');
break;
default:
console.log(`Error [${error.code}]: ${error.message}`);
}
}
}All Error Codes
Policy errors:
| Code | Meaning |
|---|---|
POLICY_DECISION_DENY |
Policy rejected the transaction |
POLICY_DECISION_REVIEW |
Transaction requires manual review |
INTENT_FINGERPRINT_MISMATCH |
Transaction was tampered with |
POLICY_HASH_MISMATCH |
Policy changed unexpectedly |
AUTH_EXPIRED |
Approval token expired (60s window) |
AUTH_CONSUMED |
Token already used (replay prevention) |
AUTH_INVALID |
Gate 2 rejected the token |
NETWORK_ERROR |
Failed to reach policy API |
VALIDATION_ERROR |
Gate 1 request failed |
VERIFICATION_ERROR |
Gate 2 request failed |
Input errors:
| Code | Meaning |
|---|---|
INVALID_AMOUNT_FORMAT |
Amount must be whole-number string |
TOKEN_ADDRESS_REQUIRED |
Non-native asset needs tokenAddress |
UNKNOWN_CHAIN |
Chain not in chainIds map |
CHAIN_MISMATCH |
Adapter on wrong chain |
Adapter errors:
| Code | Meaning |
|---|---|
ADAPTER_TX_FAILED |
Transaction execution failed |
ADAPTER_TX_NOT_FOUND |
Transaction not found after waiting |
ADAPTER_TX_HASH_MISSING |
Transaction hash not in response |
ADAPTER_NO_PROVIDER |
Signer/wallet missing provider |
ADAPTER_NO_ACCOUNT |
WalletClient missing account |
ADAPTER_TOKEN_ADDRESS_REQUIRED |
Token address needed for transfer |
ADAPTER_INVALID_KEY |
Invalid private key format |
ADAPTER_NO_RPC_URL |
No RPC URL configured for chain |
ADAPTER_UNSUPPORTED_NETWORK |
Network not supported by adapter |
ADAPTER_RPC_ERROR |
RPC call failed |
ADAPTER_NO_FETCH |
Fetch implementation required |
ADAPTER_AMOUNT_OVERFLOW |
Amount exceeds safe integer range |
ADAPTER_INVALID_AMOUNT |
Amount cannot be negative |
TRANSFER_FAILED |
Coinbase transfer failed |
Policy denial reasons (in error.message):
| 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 (includes
chain,asset,to,amount,tokenAddress).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.
Amount Formatting
Amounts must be strings in the smallest unit:
// USDC (6 decimals): 1 USDC = 1000000
amount: '1000000'
// ETH (18 decimals): 1 ETH = 1000000000000000000
amount: '1000000000000000000'
// SOL (9 decimals): 1 SOL = 1000000000
amount: '1000000000'Helper with ethers:
import { parseUnits } from 'ethers';
amount: parseUnits('100', 6).toString() // 100 USDCTypeScript
Full TypeScript support:
import {
PolicyWallet,
PolicyConfig,
PolicyError,
PolicyCounters,
PolicyMetadata,
SendIntent,
SendResult,
DecisionProof,
WalletAdapter,
WalletAdapterMetadata,
ResolvedIntent,
LogLevel,
} from '@policylayer/sdk';Links
License
MIT