JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 27
  • Score
    100M100P100Q50874F
  • License MIT

Non-custodial spending controls for AI agent wallets. Enforce limits without holding keys.

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

npm version License: MIT

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/sdk

Dependencies 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-scripts or wait for our upcoming modular packages (@policylayer/sdk-core, @policylayer/adapter-ethers, etc.).

Runtime requirements:

  • Node.js 18+ — Uses Node's crypto module
  • 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 accepted
  • logLevel: '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 decisionSigningKeys with your org's public key from dashboard
  • Set expectedPolicyHash to 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 token
  1. Gate 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).

  2. Gate 2: Before signing, SDK verifies the approval token. If the intent was modified after approval, fingerprints won't match → rejected.

  3. 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 USDC

TypeScript

Full TypeScript support:

import {
  PolicyWallet,
  PolicyConfig,
  PolicyError,
  PolicyCounters,
  PolicyMetadata,
  SendIntent,
  SendResult,
  DecisionProof,
  WalletAdapter,
  WalletAdapterMetadata,
  ResolvedIntent,
  LogLevel,
} from '@policylayer/sdk';

License

MIT