SDK Overview
SDK Overview
Install, configure, and operate the PolicyLayer SDK.
Installation
Coming Soon
The SDK package will be available on npm soon. For now, local development requires workspace installation.
Initialising PolicyWallet
import { createEthersAdapter, PolicyWallet } from '@policylayer/sdk';
export async function createPolicyWallet() {
const adapter = await createEthersAdapter(
process.env.PRIVATE_KEY!,
process.env.RPC_URL!
);
return new PolicyWallet(adapter, {
apiUrl: process.env.POLICYLAYER_API_URL!,
apiKey: process.env.POLICYLAYER_API_KEY!,
expectedPolicyHash: process.env.POLICYLAYER_POLICY_HASH, // optional
metadata: {
orgId: process.env.ORG_ID!,
walletId: 'my-wallet',
label: 'my-agent'
},
logLevel: process.env.NODE_ENV === 'production' ? 'error' : 'info',
});
}
Required Parameters
apiUrl– PolicyLayer API base URL (e.g.http://localhost:3001for local development)apiKey– Required agent key; scopes policies and counters
Optional Parameters
expectedPolicyHash– SHA-256 of the active policy for integrity checks (fails closed if mismatch)metadata– Object forwarded to API for analytics/audit logs (orgId,walletId,label, custom fields)logLevel– Controls verbosity:'silent' | 'error' | 'warn' | 'info' | 'debug'(default:'info')assetDecimals– Map of token symbol/address → decimals; overrides formatting for custom assetsfetch– Provide fetch implementation for Node 18- environments (Node 18+ includes global fetch)logger– Supply custom logger (pino/winston) to redirect SDK logs
Configuration Reference
| Field | Type | Required | Description |
|---|---|---|---|
apiUrl | string | ✅ | PolicyLayer API base URL |
apiKey | string | ✅ | Agent authentication credential |
expectedPolicyHash | string | ❌ | Optional SHA-256 for integrity verification |
metadata | object | ❌ | Custom fields for audit logs (orgId, walletId, label, etc.) |
assetDecimals | Object (symbol → decimals) | ❌ | Custom decimal overrides per asset |
chainIds | Object (chain → chainId) | ❌ | Custom chain ID mappings |
assetResolver | Function | ❌ | (chain, asset) => tokenAddress for token resolution |
decisionSigningKeys | Object[] | ❌ | Public keys for ECDSA signature verification |
timeoutMs | number | ❌ | API request timeout (default: 30000) |
maxRetries | number | ❌ | Retry attempts for network errors (default: 3) |
fetch | typeof fetch | ❌ | Fetch polyfill (required for Node 18-) |
logger | Logger | ❌ | Custom logger instance |
logLevel | LogLevel | ❌ | Log verbosity level |
Supported Chains (Default)
The SDK includes built-in chain ID mappings:
| Chain Name | Chain ID | Notes |
|---|---|---|
ethereum | 1 | Mainnet |
eth-sepolia | 11155111 | Testnet |
base | 8453 | Mainnet |
base-sepolia | 84532 | Testnet |
polygon | 137 | Mainnet |
polygon-mumbai | 80001 | Testnet |
arbitrum | 42161 | Mainnet |
optimism | 10 | Mainnet |
avalanche | 43114 | Mainnet |
bsc | 56 | BNB Chain |
solana | 0 | Special (non-EVM) |
Override via chainIds config if needed.
Asset Decimals Resolution
The SDK resolves asset decimals in this order:
- User-provided
assetDecimalsconfig - Built-in defaults (ETH=18, USDC=6, USDT=6, DAI=18, WBTC=8)
- Adapter’s default (typically 18)
Note: The SDK requires a working
fetch. Node 18+ exposes it globally; older runtimes must passconfig.fetch.
⚠️ Deprecated:
policyIdparameter is deprecated (policy group now derived from API key server-side). Remove from new integrations.
Sending a Transaction
const wallet = await createPolicyWallet();
try {
const result = await wallet.send({
chain: 'ethereum', // lowercase: 'ethereum', 'base', etc.
asset: 'eth', // lowercase: 'eth', 'usdc', 'dai', etc.
to: '0xRecipient...',
amount: '25000000000000000', // ALWAYS string in base units (wei)
memo: 'Invoice #982', // optional, included in intent fingerprint
});
console.log('Hash:', result.hash);
console.log('Fee (wei):', result.fee.toString());
console.log('Allowed:', result.allowed);
if (result.counters) {
console.log('Counters:', result.counters);
}
if (result.decisionProof) {
console.log('Decision proof:', result.decisionProof);
}
} catch (error) {
if (error instanceof PolicyError) {
console.error(error.code, error.message);
console.error('Counters:', error.details?.counters);
console.error('Decision proof:', error.details?.decisionProof);
} else {
throw error;
}
}
⚠️ CRITICAL: Amount Format
Amounts MUST be strings containing only digits (no decimals, no scientific notation):
- ETH:
"1000000000000000000"= 1 ETH (18 decimals)- USDC:
"1000000"= 1 USDC (6 decimals)- DAI:
"1000000000000000000"= 1 DAI (18 decimals)Invalid examples that will throw
INVALID_AMOUNT_FORMAT:
"1.5"- Decimals not allowed"1e18"- Scientific notation not allowed1000000- Must be string, not number (throwsINVALID_AMOUNT_TYPE)Convert to base units before calling:
parseEther("1.5")→"1500000000000000000"
SendIntent Structure
interface SendIntent {
chain: string; // Required: Chain identifier (lowercase): 'ethereum', 'base', 'solana', etc.
asset: string; // Required: Asset identifier (lowercase): 'eth', 'usdc', or contract address
to: string; // Required: Recipient address
amount: string; // Required: Amount in base units - DIGITS ONLY (no decimals)
memo?: string; // Optional: Included in intent fingerprint via SHA-256
tokenAddress?: string; // Optional: Required for non-native ERC-20s without assetResolver
}
SendResult Structure
interface SendResult {
hash: string; // Transaction hash
fee: bigint; // Gas fee paid (in base units, e.g. wei)
allowed: boolean; // Always true (failures throw PolicyError)
status: 'success'; // Always 'success' (failures throw PolicyError)
receipt?: TransactionReceipt; // Transaction receipt (undefined if skipConfirmation)
counters?: PolicyCounters; // Optional: Live spending counters (if available)
decisionProof?: DecisionProof; // Optional: Decision proof components (if available)
}
See TypeScript Interface Reference below for complete PolicyCounters and DecisionProof structures.
Note:
result.feeis the gas fee paid for the transaction (in wei for EVM chains), not the transaction amount.Note: Failures throw
PolicyErrorrather than returning a failed result. Theallowedandstatusfields are alwaystrueand'success'respectively on return.
Fail-Closed Behaviour
PolicyWallet.send fails closed—no signing occurs until both policy gates succeed. If Gate 1, Gate 2, or blockchain broadcast fails, the SDK throws an error and never signs the transaction.
Validating Before Sending (Dry Run)
Use wallet.validate() to check if a transaction would be allowed without actually sending it. This is useful for:
- UI feedback – Show users if their transaction will succeed before they confirm
- Pre-flight checks – Validate inputs in batch jobs before committing to execution
- Budget planning – Check remaining limits without consuming them
Usage
const intent = {
chain: 'ethereum',
asset: 'eth',
to: '0xRecipient...',
amount: '25000000000000000', // 0.025 ETH
};
const result = await wallet.validate(intent);
if (result.allowed) {
console.log('Transaction would be allowed');
console.log('Remaining daily:', result.counters?.remainingDaily);
// Optionally proceed with wallet.send(intent)
} else {
console.log('Transaction would be denied:', result.reason);
console.log('Today spent:', result.counters?.todaySpent);
}
ValidateResult Structure
interface ValidateResult {
allowed: boolean; // Would the transaction pass policy?
reason?: string; // Denial reason (if not allowed)
counters?: PolicyCounters; // Current spending state
decisionProof?: DecisionProof; // Decision proof components
}
Key Differences from send()
| Aspect | validate() | send() |
|---|---|---|
| Counters consumed | ❌ No | ✅ Yes |
| Auth token issued | ❌ No | ✅ Yes |
| Transaction signed | ❌ No | ✅ Yes |
| Blockchain broadcast | ❌ No | ✅ Yes |
| Audit logged | ❌ No | ✅ Yes |
| Returns on denial | { allowed: false } | Throws PolicyError |
When to Use
- Before confirmation dialogs – Check policy before asking user to confirm
- Form validation – Validate amount field as user types (debounced)
- Batch planning – Pre-validate a list of payments before executing any
- Debugging – Test policy configuration without side effects
Note:
validate()returns the current counter state. Between validation and execution, counters may change if other transactions occur. Always handlesend()errors gracefully.
Error Codes
Policy Errors
| Code | Meaning | Next Step |
|---|---|---|
POLICY_DECISION_DENY | Policy blocked the transfer | Check counters; adjust limits or whitelist |
POLICY_DECISION_REVIEW | Manual review required (future) | Wait for human approval |
POLICY_DECISION_UNKNOWN | Unrecognised decision from policy API | Check API logs; contact support |
POLICY_HASH_MISMATCH | Dashboard hash ≠ SDK expectation | Update expectedPolicyHash in config |
POLICY_HASH_MISSING | Gate 1 didn’t return policy hash | Check API implementation |
Authentication Errors
| Code | Meaning | Next Step |
|---|---|---|
AUTH_INVALID | Gate 2 rejected authorisation token | Token tampered or invalid; retry send |
AUTH_EXPIRED | Token expired (>60s since Gate 1) | Generate fresh token via new send |
AUTH_USED | Token already consumed (replay attempt) | Generate fresh token via new send |
AUTH_MISMATCH | Intent fingerprint mismatch | Tampering detected; investigate |
AUTH_TOKEN_MISSING | Gate 1 didn’t return auth token | Check API implementation |
Validation Errors
| Code | Meaning | Next Step |
|---|---|---|
VALIDATION_ERROR | Gate 1/2 validation failed | Check request format |
INTENT_FINGERPRINT_MISMATCH | Intent modified between gates | Tampering detected; investigate |
INVALID_AMOUNT_TYPE | Amount not provided as string | Fix: amount: "1000000" (string, not number) |
INVALID_AMOUNT_EMPTY | Amount is empty string | Provide valid amount |
INVALID_AMOUNT_FORMAT | Amount contains non-digit chars | Digits only: "1000000" (no decimals like "1.5", no "1e18") |
INVALID_INTENT_FIELD | Required field missing/invalid | Check SendIntent structure |
Network/System Errors
| Code | Meaning | Next Step |
|---|---|---|
NETWORK_ERROR | Network request failed | Check API connectivity; retry |
FETCH_UNAVAILABLE | No fetch implementation found | Provide config.fetch for Node 18- |
VERIFICATION_ERROR | Gate 2 verification failed | Check logs; retry |
UNKNOWN_ERROR | Unexpected failure | Inspect error details; contact support |
Chain/Token Errors
| Code | Meaning | Next Step |
|---|---|---|
UNKNOWN_CHAIN | Chain identifier not recognised | Check chain name (lowercase: ‘ethereum’, ‘base’, etc.) |
CHAIN_MISMATCH | Adapter connected to wrong chain | Create adapter for correct chain |
TOKEN_ADDRESS_REQUIRED | Non-native asset without address | Provide tokenAddress or configure assetResolver |
Signature Errors
| Code | Meaning | Next Step |
|---|---|---|
SIGNATURE_MISSING | Response missing signature | Check decisionSigningKeys config |
INVALID_SIGNATURE | Signature verification failed | Potential tampering; investigate |
Transaction Errors
| Code | Meaning | Next Step |
|---|---|---|
TX_EXECUTION_FAILED | Transaction reverted on-chain | Check recipient, amount, gas; review revert reason |
Adapter Errors
| Code | Meaning | Adapter |
|---|---|---|
ADAPTER_NO_PROVIDER | Signer missing provider | Ethers |
ADAPTER_TX_FAILED | Transaction execution failed | All |
ADAPTER_TX_NOT_FOUND | Transaction not found after waiting | All |
ADAPTER_TOKEN_ADDRESS_REQUIRED | Transfer needs token address | All |
ADAPTER_INVALID_KEY | Invalid private key format | Solana |
ADAPTER_NO_ACCOUNT | Wallet account missing | Viem |
ADAPTER_NO_RPC_URL | Chain has no configured RPC | Coinbase |
ADAPTER_UNSUPPORTED_NETWORK | Network not supported | Coinbase |
ADAPTER_RPC_ERROR | RPC call failed | Privy |
ADAPTER_NO_FETCH | Missing fetch implementation | Privy |
ADAPTER_AMOUNT_OVERFLOW | Amount exceeds MAX_SAFE_INTEGER | Solana |
ADAPTER_INVALID_AMOUNT | Negative amount provided | Solana |
ADAPTER_TX_HASH_MISSING | Transaction hash not returned | Coinbase |
TRANSFER_FAILED | createTransfer call failed | Coinbase |
SIGNING_UNAVAILABLE | Wallet address cannot sign | Coinbase |
Error Object Structure
class PolicyError extends Error {
code: string; // Error code from table above
message: string; // Human-readable description
details?: {
counters?: PolicyCounters; // Current spending state (if available)
decisionProof?: DecisionProof; // Decision proof (if available)
};
}
See TypeScript Interface Reference below for complete PolicyCounters and DecisionProof structures.
Decision Proofs
Successful wallet.send() responses include decision proof components:
{
"policyHash": "ba500a3fee18ba269cd...",
"decision": "allow",
"signature": "0x...",
"signatureIssuedAt": "2025-02-14T12:34:56Z",
"apiKeyId": "api_key_123",
"intentFingerprint": "7c5b..."
}
Store proofs for:
- Compliance reporting
- Audit trail reconciliation
- Dashboard log verification
Retrieving Last Decision Proof
// Get last decision proof without making new transaction
const proof = wallet.getLatestDecisionProof();
if (proof) {
console.log('Last decision:', proof.decision);
console.log('Policy hash:', proof.policyHash);
}
Helper Methods
Access Underlying Adapter
// Get direct access to wallet adapter
const adapter = wallet.getAdapter();
const balance = await adapter.getBalance();
console.log('Balance:', balance.toString());
Get Wallet Address
// Get wallet address
const address = await wallet.getAddress();
console.log('Address:', address);
Get Wallet Balance
// Get native token balance (e.g. ETH)
const balance = await wallet.getBalance();
console.log('Balance:', balance.toString(), 'wei');
Adapters
- ethers – Default, broad ecosystem support
- viem – Modern TypeScript-first option (peer dependency)
- Solana – Includes lamports helper utilities (bundled)
- Coinbase, Dynamic, Privy – Embedded or custodial options
All adapters implement the WalletAdapter interface defined in the SDK (packages/sdk/src/adapters/types.ts). You can create custom adapters by implementing these methods.
Advanced Topics
Automatic Features
The SDK automatically handles:
- Nonce generation – Unique
nonce(UUID) generated per request to prevent fingerprint collisions - Idempotency keys – Automatic
idempotencyKeygeneration to prevent duplicate transactions on retry - Single-use tokens – Authorization tokens consumed at Gate 2, preventing replay attacks
- Intent fingerprinting – SHA-256 hash of canonicalized intent for tamper detection
Memo Usage
The memo field is optional but useful for:
- Correlation IDs (invoice numbers, order IDs)
- Audit trail context
- Customer reference numbers
const result = await wallet.send({
chain: 'ethereum',
asset: 'usdc',
to: '0xCustomer...',
amount: '5000000', // 5 USDC
memo: 'Refund for order #12345'
});
The memo is included in the intent fingerprint (as SHA-256 hash) and appears in audit logs.
Retry Behaviour
Important: Once Gate 1 succeeds, spending counters are updated immediately (hard reservation). If Gate 2 or blockchain broadcast fails afterward, the transaction still consumes your budget.
For failed transactions after Gate 1:
- Counters remain updated (not rolled back)
- Generate new transaction to retry
- Budget already consumed
This prevents race conditions and “double spending” via concurrent requests.
Transaction Lifecycle
- SDK generates nonce + idempotency key
- Gate 1 evaluates policy → reserves amount → issues token
- Gate 2 verifies token + fingerprint → marks consumed
- SDK signs transaction locally with your keys
- SDK broadcasts to blockchain
- SDK returns hash + fee + counters + decision proof
If any step fails, SDK throws error and does not proceed to next step.
x402 Duplicate Detection
For HTTP 402 payments, PolicyLayer prevents accidental double-payments using a 5-minute deduplication window.
What makes a request “duplicate”:
The API computes a hash from these canonicalised fields:
| Field | Normalisation |
|---|---|
amount | Trimmed (exact string match) |
chain | Lowercase (e.g., base) |
currency | Uppercase (e.g., USDC) |
endpoint | Trimmed URL |
recipient | Lowercase wallet address |
If these five fields produce the same hash within 5 minutes for the same agent, the request is rejected as X402_DUPLICATE_PAYMENT.
Scoping:
- Detection is scoped to org + agent (API key)
- Two different agents can make identical payments without collision
- The same agent making the same request twice within 5 minutes = duplicate
Why this exists:
HTTP 402 retries are common—network timeouts, SDK restarts, or user impatience can trigger repeated calls. Without deduplication, agents could accidentally pay twice for the same resource.
Handling duplicates:
const result = await x402Wallet.pay(headers, endpoint);
if (!result.allowed && result.reason === 'X402_DUPLICATE_PAYMENT') {
// This exact payment was already approved within last 5 minutes
// Safe to proceed—original approval is still valid
console.log('Using existing approval');
}
Not configurable: The 5-minute window is fixed. This provides consistent security guarantees. If your use case requires identical payments within 5 minutes, vary one field (e.g., add timestamp to endpoint query string).
Integration Tips
- Use memo for correlation IDs – Intent fingerprint includes SHA-256 hash of memo
- Store decision proofs – Reconcile SDK responses with dashboard audit logs
- Handle errors gracefully – Check
error.details.countersto diagnose policy blocks - Rotate API keys – When agent roles change, only latest key remains valid
- Test with small amounts – Use testnet/sepolia for integration testing
TypeScript Interface Reference
Complete type definitions from the SDK (packages/sdk/src/types.ts).
PolicyCounters
export interface PolicyCounters {
todaySpent?: string; // Amount spent today (optional, in base units)
remainingDaily?: string; // Remaining daily budget (optional, in base units)
[key: string]: string | undefined; // Extensible for custom counters
}
Note: All fields are optional. The API may return additional counter fields beyond todaySpent and remainingDaily (e.g. hourly counters, transaction counts). Use index signature [key: string] to access them.
DecisionProof
export interface DecisionProof {
policyHash: string; // Required: SHA-256 hash of active policy
decision: 'allow' | 'deny' | 'review'; // Required: Policy decision
intentFingerprint: string; // Required: SHA-256 hash of transaction intent
signature?: string; // Optional: HMAC signature of decision
signatureIssuedAt?: string; // Optional: ISO timestamp when signature issued
apiKeyId?: string; // Optional: API key that authorised the decision
}
Note: Only policyHash, decision, and intentFingerprint are guaranteed. Other fields (signature, signatureIssuedAt, apiKeyId) may be absent depending on API configuration.
PolicyMetadata
export interface PolicyMetadata {
orgId?: string;
walletId?: string;
label?: string;
[key: string]: string | undefined; // Extensible for custom metadata
}
Usage: Custom metadata fields forwarded to the policy API for analytics and audit logs. All fields are optional and user-defined.
SendIntent
export interface SendIntent {
chain: string; // Required: Chain identifier (lowercase): 'ethereum', 'base', 'solana', etc.
asset: string; // Required: Asset identifier (lowercase): 'eth', 'usdc', or contract address
to: string; // Required: Recipient address
amount: string; // Required: Amount in base units - DIGITS ONLY (regex: ^\d+$)
memo?: string; // Optional: Included in intent fingerprint via SHA-256
tokenAddress?: string; // Optional: Required for non-native ERC-20s without assetResolver
}
Amount validation: Must be a string of digits only.
"1.5"and"1e18"will throwINVALID_AMOUNT_FORMAT.
SendResult
export interface SendResult {
hash: string; // Transaction hash
fee: bigint; // Gas fee paid (0n if skipConfirmation)
allowed: boolean; // Always true (failures throw PolicyError)
status: 'success'; // Always 'success' (failures throw PolicyError)
receipt?: TransactionReceipt; // Present unless skipConfirmation
counters?: PolicyCounters; // Optional: Live spending counters
decisionProof?: DecisionProof; // Optional: Decision proof components
}
Note: Failed transactions throw
PolicyErrorrather than returning a result withstatus: 'failed'.
ValidateResult
export interface ValidateResult {
allowed: boolean; // Would the transaction pass policy checks?
reason?: string; // Denial reason (only present if allowed=false)
counters?: PolicyCounters; // Current spending counters (not consumed)
decisionProof?: DecisionProof; // Decision proof components
}
Note: Unlike
SendResult, validation results return{ allowed: false }for denials rather than throwing. Counters are read-only—no budget consumed.
PolicyError
export class PolicyError extends Error {
code: string; // Error code (see Error Codes section)
message: string; // Human-readable description
details?: {
counters?: PolicyCounters; // Current spending state (if available)
decisionProof?: DecisionProof; // Decision proof (if available)
};
}
API Denial Reasons
When the policy API denies a transaction, the reason field indicates why. These are returned in PolicyError.message or in the API response body.
Asset Policy Denials
These apply to standard asset transfers via PolicyWallet.send():
| Reason | Meaning | How to Fix |
|---|---|---|
RECIPIENT_NOT_WHITELISTED | Recipient address not in allowlist | Add recipient to whitelist in dashboard |
TX_FREQUENCY_LIMIT | Too many transactions in time window | Wait for window to reset or increase limit |
PER_TX_LIMIT | Single transaction exceeds max amount | Reduce amount or increase per-tx limit |
HOURLY_LIMIT | Hourly spending cap reached | Wait for next hour or increase limit |
DAILY_LIMIT | Daily spending cap reached | Wait for next day or increase limit |
NO_POLICY_FOR_ASSET | No policy configured for this asset | Create policy for asset in dashboard |
POLICY_NOT_FOUND | Referenced policy does not exist | Check policy ID is correct |
POLICY_ACCESS_DENIED | API key not authorised for this policy | Check API key permissions |
POLICY_HASH_MISMATCH | Policy changed since SDK initialised | Update expectedPolicyHash or re-fetch |
ASSET_MISMATCH | Transaction asset doesn’t match policy asset | Use correct asset for this policy |
UNSUPPORTED_CHAIN | Chain not supported by this policy | Add chain support or use different policy |
UNSUPPORTED_ASSET | Asset type not supported | Add asset to policy or use different asset |
INVALID_INTENT | Malformed or invalid transaction intent | Check intent structure matches SDK spec |
IDEMPOTENCY_KEY_MISMATCH | Duplicate request with different intent | Use unique idempotency key per request |
FINGERPRINT_MISMATCH | Intent modified between Gate 1 and Gate 2 | Potential tampering; investigate |
POLICY_MODIFIED_AFTER_AUTH | Policy changed between gates | Retry transaction |
x402 Policy Denials
These apply to HTTP 402 payment protocol transactions:
| Reason | Meaning | How to Fix |
|---|---|---|
X402_NO_DEFAULT_POLICY | No x402 policy configured for agent | Set up default x402 policy in dashboard |
X402_DUPLICATE_PAYMENT | Same payment already processed within 5 min | See Duplicate Detection |
X402_RECIPIENT_NOT_ALLOWED | Recipient not in allowlist | Add recipient to x402 allowlist |
X402_RECIPIENT_BLOCKED | Recipient is blocklisted | Remove from blocklist or use different recipient |
X402_RECIPIENT_MISMATCH | Recipient doesn’t match pinned recipient for endpoint | Use correct recipient for this endpoint |
X402_CURRENCY_NOT_ALLOWED | Currency not permitted for this endpoint | Add currency to allowed list |
X402_HEADERS_HASH_MISMATCH | Request headers hash verification failed | SDK bug or tampering; contact support |
x402 Limit Denials
Rate and spending limits for x402, applied at agent (aggregate) or endpoint level:
| Reason | Meaning | How to Fix |
|---|---|---|
X402_AGGREGATE_AMOUNT_LIMIT | Single request exceeds agent’s max per-request | Reduce amount or increase agent limit |
X402_AGGREGATE_DAILY_LIMIT | Agent’s total daily spend reached | Wait for daily reset or increase limit |
X402_AGGREGATE_FREQUENCY_LIMIT | Agent’s requests/minute limit hit | Slow down or increase agent rate limit |
X402_ENDPOINT_AMOUNT_LIMIT | Request exceeds endpoint’s max per-request | Reduce amount or increase endpoint limit |
X402_ENDPOINT_DAILY_LIMIT | Endpoint’s daily spend cap reached | Wait for reset or increase endpoint limit |
X402_ENDPOINT_FREQUENCY_LIMIT | Endpoint’s requests/minute limit hit | Slow down or increase endpoint rate limit |
Note: x402 enforces limits at two levels. Agent limits (aggregate) apply across all endpoints. Endpoint limits are sub-limits within the agent cap. Requests must pass both.
System Errors
| Reason | Meaning | How to Fix |
|---|---|---|
INTERNAL_ERROR | Unexpected server error | Check API logs; contact support |
SERVICE_UNAVAILABLE | API temporarily unavailable | Retry with backoff |
INVALID_API_KEY | API key not recognised | Check key is correct and not revoked |
Next Steps
- Core Concepts – Understand two-gate enforcement
- Adapter Guides – Detailed adapter documentation
- Error Handling – Common errors and solutions