JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 10
  • Score
    100M100P100Q51163F
  • 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 Node.js Version

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

Peer 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 adapter

Configuration

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 token
  1. Gate 1: SDK sends transaction intent to PolicyLayer API. Policy engine checks limits, reserves budget, returns signed approval token with intent fingerprint.

  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.

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';

License

MIT