npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@policylayer/sdk

v1.4.0

Published

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

Readme

@policylayer/sdk

Non-custodial spending controls for AI agent wallets

npm version License: MIT Node.js Server Only

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

Install per adapter:

# Core (just Viem adapter)
npm install @policylayer/sdk

# Ethers
npm install @policylayer/sdk ethers

# Solana
npm install @policylayer/sdk @solana/web3.js @solana/spl-token bs58 tweetnacl

# Coinbase CDP
npm install @policylayer/sdk @coinbase/coinbase-sdk decimal.js

# Privy
npm install @policylayer/sdk @privy-io/node

# Dynamic
npm install @policylayer/sdk @dynamic-labs/sdk-react-core @dynamic-labs/ethereum

Light install: Only Viem is bundled. Adapter-specific dependencies are optional peer deps—install only what you need.

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).

Production requirement: You must configure decisionSigningKeys in production (NODE_ENV=production) for transactions to succeed. Get your signing key from the PolicyLayer Dashboard.

const wallet = new PolicyWallet(adapter, {
  apiUrl: 'https://api.policylayer.com',
  apiKey: process.env.POLICYLAYER_API_KEY!,
  decisionSigningKeys: [process.env.POLICYLAYER_SIGNING_KEY!], // Required in production
});

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

// IMPORTANT: walletData contains the wallet seed - store securely!
// Use KMS, Secrets Manager, or encrypted storage. Never log in production.
await secretsManager.store('coinbase-wallet', 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

Note: Dynamic is browser-first. Run PolicyWallet calls in a server action/API route, or add Node polyfills (crypto, buffer) to your bundler config.

// In a Next.js API route or server action:
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!,
});

X402 Payment Protocol

The SDK supports the X402 Payment Required protocol for handling HTTP 402 responses. When an API returns 402 Payment Required, x402 headers specify payment details. PolicyLayer validates these payments through the two-gate enforcement model before execution.

Basic Usage

import { X402Client, PolicyConfig } from '@policylayer/sdk';

const policyConfig: PolicyConfig = {
  apiUrl: 'https://api.policylayer.com',
  apiKey: process.env.POLICYLAYER_API_KEY!,
};

const x402Client = new X402Client(policyConfig);

// Make API request that returns 402
const response = await fetch('https://api.example.com/resource');

if (response.status === 402) {
  // Complete two-gate flow: validate → verify → execute
  const paymentResponse = await x402Client.pay(response, 'https://api.example.com/resource');
  console.log('Payment completed:', paymentResponse.status);
}

Manual Control Flow

For more control over the payment process:

// Step 1: Parse 402 response and validate with Gate 1
const validation = await x402Client.handle402Response(response);
console.log('Payment approved:', validation.approved);
console.log('Amount:', validation.intent.normalizedAmount);

// Step 2: Execute payment (includes Gate 2 verification)
const paymentResponse = await x402Client.executePayment(validation, {
  facilitatorUrl: 'https://facilitator.example.com/pay',
  headers: {
    'X-Custom-Header': 'value'
  }
});

Client-Side Validation

Add stricter client-side limits before policy validation:

const x402Client = new X402Client(policyConfig, {
  maxAmountPerRequest: '100000000', // Max 100 USDC (6 decimals)
  allowedRecipients: [
    '0x1234...', // Only allow payments to specific addresses
  ],
  blockedRecipients: [
    '0xbad...', // Block specific addresses
  ],
});

Duplicate detection is enforced server-side; no client flag is required.

Required Headers

X402 responses must include:

  • X-Payment-Amount: Amount as decimal string (e.g., "10.5")
  • X-Payment-Currency: Currency code (USDC, ETH, DAI, etc.)
  • X-Payment-Recipient: Wallet address
  • X-Payment-Chain: Chain name (base, base-sepolia, ethereum, arbitrum, optimism)
  • X-Payment-Facilitator (optional): Payment facilitator URL

Supported Currencies

Built-in decimal support for:

| Currency | Decimals | |----------|----------| | USDC | 6 | | USDT | 6 | | DAI | 18 | | ETH | 18 | | WETH | 18 |

Manual Header Parsing

import { parseX402Headers, normaliseAmount, hashHeaders } from '@policylayer/sdk';

// Parse headers from Response or Record
const headers = parseX402Headers(response.headers);
console.log(headers);
// {
//   amount: '10.5',
//   currency: 'USDC',
//   recipient: '0x...',
//   chain: 'base-sepolia',
//   facilitator: 'https://...'
// }

// Normalise amount to bigint (precision-safe)
const amountBigInt = normaliseAmount('10.5', 'USDC');
console.log(amountBigInt); // 10500000n (6 decimals)

// Create hash for duplicate detection
const headersHash = hashHeaders(headers);
console.log(headersHash); // SHA-256 hex string

Error Handling

import { X402HeaderError, X402ValidationError, X402PaymentError } from '@policylayer/sdk';

try {
  await x402Client.pay(response);
} catch (error) {
  if (error instanceof X402HeaderError) {
    // Missing or invalid headers
    console.error('Header error:', error.message, error.field);
  } else if (error instanceof X402ValidationError) {
    // Validation failed (amount, decimals, currency)
    console.error('Validation error:', error.message, error.code);
  } else if (error instanceof X402PaymentError) {
    // Payment execution failed
    console.error('Payment error:', error.message, error.code);
  }
}

How It Works

  1. Parse Headers: Extract payment details from 402 response headers
  2. Normalise Amount: Convert decimal string to bigint using currency-specific decimals (precision-safe, no parseFloat)
  3. Gate 1 (Validate): Send intent to PolicyLayer API, receive authorisation token
  4. Gate 2 (Verify): Verify authorisation before payment execution
  5. Execute Payment: Send payment to facilitator with X-PolicyLayer-Auth header binding authorisation token

Authorisation Binding

The authorisation token from Gate 1 is bound to the facilitator request via the X-PolicyLayer-Auth header. This ensures the payment is traceable and policy-enforced.

// Authorisation token automatically added to facilitator request
const response = await x402Client.executePayment(validation);
// Request includes: { headers: { 'X-PolicyLayer-Auth': '<token>' } }

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)

Fail-closed tip: If you can't set decisionSigningKeys yet, at least set logLevel: 'warn' and monitor for "signature verification skipped" warnings in your telemetry. This helps you track when you're running without signature verification.


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}`);
    }
  }
}

Telemetry Integration

Forward policy decisions to your observability stack:

try {
  const result = await wallet.send(intent);

  // Log successful transactions with proof
  telemetry.track('policy.allowed', {
    hash: result.hash,
    policyHash: result.decisionProof?.policyHash,
    remainingDaily: result.counters?.remainingDaily,
  });
} catch (error) {
  if (error instanceof PolicyError) {
    telemetry.track('policy.error', {
      code: error.code,
      message: error.message,
      policyHash: error.details?.policyHash,
      reason: error.details?.reason,
      counters: error.details?.counters,
    });
  }
}

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_USED | Token already used (replay prevention) | | AUTH_INVALID | Gate 2 rejected the token | | AUTH_MISMATCH | Gate 2 fingerprint mismatch | | 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';

Links

License

MIT