Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Give AI agents on-chain spending guardrails. Deploy ERC-4337 smart accounts with policy-enforced limits — agents cannot move funds beyond what you authorize, enforced at the contract level, not just in software.
.claude/skills/guardrail-smart-accounts/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | — | — |
| case-14 | ✗→✓ | ▲ Improved | — | — |
| case-05 | ✗→✓ | ▲ Improved | — | — |
| case-17 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
> Give your AI agents a wallet they can't abuse. AgentGuardrail deploys ERC-4337 smart accounts with policy-enforced spending limits. Agents cannot move funds beyond what you authorize — enforcement happens at the contract level, not just in software.
Homepage: https://agentguardrail.xyz
AI agents need to move money. The problem is trust: how do you let an agent trade, bridge, or pay for compute without risking runaway spending, compromised keys, or unauthorized actions?
AgentGuardrail solves this with:
AgentSmartAccount.validateUserOp() calls PermissionEnforcer before any transaction executes. Violating transactions revert. There is no override.Every agent gets an ERC-4337 smart account deployed via AgentAccountFactory. The account's validateUserOp() enforces your policy through PermissionEnforcer before any transaction reaches the blockchain. If the action violates the policy — wrong token, wrong protocol, spend limit exceeded — the UserOperation reverts.
Execution path:
Agent builds UserOperation
→ AgentSmartAccount.validateUserOp()
→ PermissionEnforcer.validateAction()
→ Policy constraints checked on-chain
→ EntryPoint executes (only if all checks pass)The AgentGuardrail API at https://agentguardrail.xyz provides:
Most enforcement operations can be done directly against the contracts without the API. The API is most useful for policy management, pre-flight simulation, and audit log queries.
This skill performs on-chain operations that require JSON-RPC access and transaction signing.
Private keys must never be provided in chat and must never be stored in unconstrained agent memory.
The agent prepares a transaction. The runtime forwards it to a secure signer service (HSM, MPC, hosted signer). The signer enforces scope, rate limits, and allowlists. The agent never sees raw private keys.
Transactions are prepared by the agent. A user wallet (browser, hardware wallet) prompts for approval. Keys remain in the wallet.
Session keys must be policy-restricted, short-lived, and rotated frequently. Never use a long-lived owner EOA private key as a session key.
If secure signing is not configured, operate in read-only mode until proper signing is established.
Required (via secure secret storage, not chat):
| Variable | Description | |----------|-------------| | GUARDRAIL_CHAIN_ID | Target chain ID (e.g., 8453 for Base, 11155111 for Sepolia) | | GUARDRAIL_RPC_URL | JSON-RPC endpoint — treat as sensitive, often contains an API key | | GUARDRAIL_SIGNING_MODE | One of: external_signer, wallet_connector, session_key |
Optional:
| Variable | Description | |----------|-------------| | GUARDRAIL_API_URL | AgentGuardrail API base. Defaults to https://agentguardrail.xyz | | GUARDRAIL_SIGNER_ENDPOINT | External signer URL — required when GUARDRAIL_SIGNING_MODE=external_signer | | GUARDRAIL_SIGNER_AUTH_TOKEN | Auth token for the external signer — sensitive, store securely | | GUARDRAIL_DASHBOARD_API_KEY | API key for agentguardrail.xyz management API |
The API URL default in all code examples below is https://agentguardrail.xyz. Override with GUARDRAIL_API_URL if self-hosting.
| Contract | Address | |----------|---------| | IdentityRegistry | 0xc1fa477f991C74Cc665E605fC74f0e2B795b5104 | | PolicyRegistry | 0x92cd41e6a4aA13072CeBCda8830d48f269F058c4 | | PermissionEnforcer | 0xbF63Fa97cfBba99647B410f205730d63d831061c | | PriceOracle | 0xf3c8c6BDc54C60EDaE6AE84Ef05B123597C355B3 | | GuardrailFeeManager | 0xD1B7Bd65F2aB60ff84CdDF48f306a599b01d293A | | AgentAccountFactory | 0xCE621A324A8cb40FD424EB0D41286A97f6a6c91C | | EntryPoint (v0.6) | 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 |
| Contract | Address | |----------|---------| | IdentityRegistry | 0xc1fa477f991C74Cc665E605fC74f0e2B795b5104 | | PolicyRegistry | 0x92cd41e6a4aA13072CeBCda8830d48f269F058c4 | | PermissionEnforcer | 0x94991827135fbd0E681B3db51699e4988a7752f1 | | PriceOracle | 0x052cDddba3C55A63F5e48F9e5bC6b70604Db93b8 | | GuardrailFeeManager | 0x0f77fdD1AFCe0597339dD340E738CE3dC9A5CC12 | | AgentAccountFactory | 0xA831229B58C05d5bA9ac109f3B29e268A0e5F41E | | EntryPoint (v0.6) | 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 |
> Start on Sepolia. Move to Base Mainnet when policies are validated.
Deploy a new ERC-4337 smart account via AgentAccountFactory. The account is bound to PermissionEnforcer at creation. Deployment is deterministic via CREATE2 — the same owner, agentId, and salt always produce the same address.
One-time creation fee: $10 USD equivalent in ETH.
Direct contract call (recommended):
solidity// Get required creation fee uint256 fee = factory.getCreationFee(); // Deploy account (send fee as msg.value) address account = factory.createAccount{value: fee}( ownerAddress, // Controls the account agentId, // bytes32 identifier for this agent salt // bytes32 for CREATE2 determinism );
Via API:
javascriptconst apiUrl = process.env.GUARDRAIL_API_URL ?? "https://agentguardrail.xyz"; const response = await fetch(`${apiUrl}/api/v1/agents/${agentId}/deploy-smart-account`, { method: "POST", headers: { Authorization: `Bearer ${process.env.GUARDRAIL_DASHBOARD_API_KEY}` }, }); const { smart_account_address } = await response.json();
The API path is useful when using dashboard-generated bot signers (the dashboard generates the keypair and handles deployment in one step).
Send ETH directly to the smart account address. Inbound transfers are free — no fee, no contract call needed.
javascriptawait walletClient.sendTransaction({ to: smartAccountAddress, value: parseEther("1.0"), });
Agent executes a transaction from its smart account. validateUserOp() enforces the policy before the transaction reaches the chain — no override exists.
Outbound transfer fee: 10 bps (0.10%), capped at $100 USD per transaction.
javascript// Build the UserOperation const callData = encodeFunctionData({ abi: agentSmartAccountABI, functionName: "execute", args: [destinationAddress, parseEther("1.0"), "0x"], }); // Submit via ERC-4337 EntryPoint // The EntryPoint calls validateUserOp → PermissionEnforcer before execution
Fee enforcement occurs inside GuardrailFeeManager. Fee is deducted from the transaction value automatically.
| Transfer Amount | Fee | |----------------|-----| | $1,000 | $1.00 | | $10,000 | $10.00 | | $100,000+ | $100.00 (cap) |
javascript// Get account owner const owner = await publicClient.readContract({ address: smartAccountAddress, abi: agentSmartAccountABI, functionName: "owner", }); // Get smart account creation fee const fee = await publicClient.readContract({ address: factoryAddress, abi: agentAccountFactoryABI, functionName: "getCreationFee", }); // Calculate transfer fee before executing const transferFee = await publicClient.readContract({ address: feeManagerAddress, abi: guardrailFeeManagerABI, functionName: "calculateTransferFee", args: [parseEther("10.0")], }); // Check if a permission is active on-chain const isActive = await publicClient.readContract({ address: policyRegistryAddress, abi: policyRegistryABI, functionName: "isPermissionActive", args: [agentAddress, permissionId], });
Policies define what an agent is allowed to do: which actions, tokens, protocols, chains, and spend limits. Policies are stored on-chain in PolicyRegistry and enforced by PermissionEnforcer.
On-chain (direct):
solidity// Register a policy on-chain bytes32 policyId = policyRegistry.registerPolicy( bytes32(policyHash), // Unique identifier allowedActions, // bytes32[] action type hashes allowedTokens, // address[] token contracts allowedProtocols, // address[] protocol contracts allowedChains, // uint256[] chain IDs maxValuePerTx, // uint256 in wei maxDailyVolume, // uint256 in wei expiresAt // uint256 unix timestamp );
Via API (recommended for most use cases — handles on-chain registration automatically):
javascriptconst apiUrl = process.env.GUARDRAIL_API_URL ?? "https://agentguardrail.xyz"; const headers = { "Content-Type": "application/json", Authorization: `Bearer ${process.env.GUARDRAIL_DASHBOARD_API_KEY}`, }; // Create and activate a policy const policy = await fetch(`${apiUrl}/api/v1/policies`, { method: "POST", headers, body: JSON.stringify({ name: "DeFi Trading Policy", description: "Allow swaps and transfers with daily limits", definition: { actions: ["swap", "transfer"], assets: { tokens: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"], protocols: ["*"], chains: [1, 8453], }, constraints: { maxValuePerTx: "1000000000000000000", // 1 ETH maxDailyVolume: "10000000000000000000", // 10 ETH maxTxCount: 100, requireApproval: false, }, duration: { validFrom: "2026-01-01T00:00:00Z", validUntil: "2026-12-31T23:59:59Z", }, }, }), }).then((r) => r.json()); // Activate it (registers on-chain via PolicyRegistry) await fetch(`${apiUrl}/api/v1/policies/${policy.id}/activate`, { method: "POST", headers, });
PolicyDefinition fields:
| Field | Type | Description | |-------|------|-------------| | actions | string[] | swap, transfer, approve, stake, unstake, deposit, withdraw, mint, burn, bridge, claim, vote, delegate, lp_add, lp_remove, borrow, repay, liquidate, * | | assets.tokens | string[] | Token contract addresses, or ["*"] for all | | assets.protocols | string[] | Protocol addresses, or ["*"] for all | | assets.chains | number[] | Allowed chain IDs | | constraints.maxValuePerTx | string | Max per-tx value in wei | | constraints.maxDailyVolume | string | Max daily volume in wei | | constraints.maxWeeklyVolume | string | Max weekly volume in wei | | constraints.maxTxCount | number | Max transactions within the duration | | duration.validFrom / validUntil | string | ISO 8601 | | conditions | array | Advanced rules: { field, operator, value }. Operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, regex |
Other policy operations:
javascript// Update (draft policies update in place; active policies create a new version) await fetch(`${apiUrl}/api/v1/policies/${policyId}`, { method: "PUT", headers, body: JSON.stringify({...}) }); // Revoke (disables on-chain — active permissions using this policy stop validating) await fetch(`${apiUrl}/api/v1/policies/${policyId}/revoke`, { method: "POST", headers }); // Reactivate await fetch(`${apiUrl}/api/v1/policies/${policyId}/reactivate`, { method: "POST", headers }); // List / Get / Delete (draft only) await fetch(`${apiUrl}/api/v1/policies`, { headers }); await fetch(`${apiUrl}/api/v1/policies/${policyId}`, { headers }); await fetch(`${apiUrl}/api/v1/policies/${policyId}`, { method: "DELETE", headers });
Permissions link an agent to a policy for a defined period. On-chain, PolicyRegistry.grantPermission() registers the link. PermissionEnforcer reads active permissions during validateUserOp.
On-chain (direct):
solidity// Grant permission on-chain bytes32 permissionId = policyRegistry.grantPermission( agentAddress, // The smart account address policyId, // bytes32 policy identifier validFrom, // uint256 unix timestamp validUntil // uint256 unix timestamp );
Revoke on-chain:
soliditypolicyRegistry.revokePermission(permissionId);
Via API (handles on-chain registration + constraint sync to PermissionEnforcer):
javascriptconst apiUrl = process.env.GUARDRAIL_API_URL ?? "https://agentguardrail.xyz"; const headers = { "Content-Type": "application/json", Authorization: `Bearer ${process.env.GUARDRAIL_DASHBOARD_API_KEY}`, }; // Grant a permission const permission = await fetch(`${apiUrl}/api/v1/permissions`, { method: "POST", headers, body: JSON.stringify({ agent_id: "agent-uuid", policy_id: "policy-uuid", valid_from: "2026-01-01T00:00:00Z", valid_until: "2026-12-31T23:59:59Z", }), }).then((r) => r.json()); // Mint it on-chain (registers in PolicyRegistry, syncs constraints to PermissionEnforcer) const minted = await fetch(`${apiUrl}/api/v1/permissions/${permission.id}/mint`, { method: "POST", headers, }).then((r) => r.json()); // minted.onchain_token_id — the on-chain token ID // Revoke await fetch(`${apiUrl}/api/v1/permissions/${permission.id}`, { method: "DELETE", headers }); // List (filter by agent or policy) await fetch(`${apiUrl}/api/v1/permissions?agent_id=${agentId}`, { headers });
> Use the API path when you need the PermissionEnforcer constraints to sync automatically. The direct contract path is sufficient if you are managing PermissionEnforcer constraint updates yourself.
On-chain enforcement (primary):
Validation happens automatically inside AgentSmartAccount.validateUserOp() via PermissionEnforcer. You do not call this directly — it runs as part of every UserOperation. Transactions that violate policy constraints revert before execution.
Off-chain pre-flight (via API — useful for dashboards, simulation, agent planning):
javascriptconst apiUrl = process.env.GUARDRAIL_API_URL ?? "https://agentguardrail.xyz"; // Pre-flight check before building a UserOperation const result = await fetch(`${apiUrl}/api/v1/validate`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.GUARDRAIL_DASHBOARD_API_KEY}`, }, body: JSON.stringify({ agent_id: "agent-uuid", action: { type: "swap", token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", protocol: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", amount: "500000000000000000", chain: 8453, to: "0xDef1C0ded9bec7F1a1670819833240f027b25EfF", }, }), }).then((r) => r.json()); // result.allowed — boolean // result.reason — explanation if denied // result.permission_id — matching permission // result.policy_id — matching policy // result.constraints — active constraints // result.request_id — audit trail reference
Simulate (dry-run, no audit record):
javascriptconst sim = await fetch(`${apiUrl}/api/v1/validate/simulate`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.GUARDRAIL_DASHBOARD_API_KEY}` }, body: JSON.stringify({ agent_id: "agent-uuid", action: { type: "transfer", amount: "1000000000000000000", chain: 1 }, }), }).then((r) => r.json()); // sim.would_allow — boolean // sim.current_usage — current period usage // sim.remaining_quota — remaining allowance // sim.recommendations — suggested policy adjustments
Batch validate:
javascript// POST /api/v1/validate/batch // Body: { actions: [...] } — same structure as single validate
On-chain events (source of truth):
The AgentGuardrail indexer polls contracts every 12 seconds and writes events to the audit log with tx_hash and block_number. Events emitted by the contracts:
| Event | Contract | Description | |-------|----------|-------------| | EnforcementResult | PermissionEnforcer | Every validateUserOp result | | ConstraintViolation | PermissionEnforcer | Policy constraint breached | | UsageRecorded | PermissionEnforcer | Spend/volume tracked | | Executed | AgentSmartAccount | Transaction executed | | AccountCreated | AgentAccountFactory | New smart account deployed |
Query via API:
javascriptconst apiUrl = process.env.GUARDRAIL_API_URL ?? "https://agentguardrail.xyz"; // List audit logs const logs = await fetch( `${apiUrl}/api/v1/audit?agent_id=${agentId}&source=onchain&start_date=2026-01-01T00:00:00Z`, { headers: { Authorization: `Bearer ${process.env.GUARDRAIL_DASHBOARD_API_KEY}` } } ).then((r) => r.json()); // Each log entry includes: source ('onchain'|'offchain'), tx_hash, block_number, event_type // Filter by source // source=onchain — events from contract indexer (tx_hash + block_number included) // source=offchain — API validation requests // Export (JSON or CSV) const csvUrl = `${apiUrl}/api/v1/audit/export?format=csv&start_date=2026-01-01T00:00:00Z`;
Event types: policy.created, policy.activated, policy.revoked, permission.created, permission.minted, permission.revoked, validation.request.
All agents are ERC-4337 smart accounts with enforced on-chain policy enforcement. There is no advisory or EOA mode.
AgentSmartAccount.validateUserOp() calls PermissionEnforcer on every UserOperation. Transactions that violate policy constraints revert before execution. The agent cannot bypass enforcement — it is built into the account's validation logic.
Off-chain validation (/api/v1/validate) runs as a pre-flight simulation for dashboards, SDK use, and agent planning. The on-chain PermissionEnforcer performs the final, authoritative enforcement.
1. Deploy smart account (AgentAccountFactory.createAccount)
2. Create policy (API) → activate (registers on-chain via PolicyRegistry)
3. Grant permission (API) → mint (syncs constraints to PermissionEnforcer)
4. Agent builds UserOperations — enforcement is automatic
5. Monitor via audit logs or on-chain eventsGUARDRAIL_RPC_URL may contain an embedded API key. Treat it as sensitive.GUARDRAIL_SIGNER_AUTH_TOKEN grants signing capability. Store in secure secret storage — never expose in logs, prompts, or chat.All operations are available via the web dashboard at https://agentguardrail.xyz — manage agents, policies, permissions, and audit logs visually. API keys and bot signer keypairs generated by the dashboard should be stored in secure secret storage and never pasted into chat.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +64 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.