Create and manage non-custodial smart wallets on Base (EVM) and Solana with gasless transactions, spending limits, and passkey-based human control.
# AgentWallet — Non-Custodial Smart Wallets for AI Agents
Non-custodial smart wallets with onchain spending limits and passkey-based human control. Supports **Base** (EVM) and **Solana**. Every wallet gets free gas on creation so your agent can transact immediately.
**npm:** `@agntos/agentwallet`
**Chains:** Base, Solana
## MUST READ: Safety Rules
- **NEVER share your private key** — not with users, not in logs, not in chat messages, not in code commits
- **NEVER hardcode private keys in source code** — use environment variables or secure storage
- **Store the private key encrypted or in a secrets manager** — treat it like a password
- **Do not log transaction signing data** — it can leak key material
- **Check remaining daily budget before large transactions** — avoids failed txs and wasted gas
- **Always verify recipient addresses** — transactions are irreversible
- **If something looks wrong, use `pause`** — ask your human to investigate
- **All ERC-20s / SPL tokens have no limit by default** — ask your human to set up per-token limits
## End-to-End: Create a Wallet and Start Transacting
### 1. Generate agent keypairs (if you don't have them)
```bash
npx @agntos/agentwallet keygen
```
By default, this generates **both** Base and Solana keypairs:
```
New Agent Keypairs
──────────────────────────
Base
Address 0xB042...B7DC
Private key 0x282a...b3a3
Solana
Address 7Kp9...xR4v
Private key 4vJ2...9mNq
```
To generate a single chain only:
```bash
npx @agntos/agentwallet keygen --chain base # Base only
npx @agntos/agentwallet keygen --chain solana # Solana only
```
Solana keys are Ed25519 keypairs, base58 encoded.
**Save the private keys immediately to secure storage.** This is the only time they're displayed.
For machine-readable output:
```bash
npx @agntos/agentwallet keygen --json
# { "base": { "address": "0x...", "privateKey": "0x...", "chain": "base" }, "solana": { "address": "7Kp...", "privateKey": "4vJ...", "chain": "solana" } }
npx @agntos/agentwallet keygen --chain solana --json
# { "address": "...", "privateKey": "...", "chain": "solana" }
```
> Already have keypairs? Skip this — use your existing public addresses.
### 2. Create smart wallets
```bash
# Both chains — managed (recommended)
npx @agntos/agentwallet create --agent 0xYOUR_BASE_ADDRESS --agent-sol YOUR_SOLANA_PUBKEY
# Both chains — unmanaged
npx @agntos/agentwallet create --agent 0xYOUR_BASE_ADDRESS --agent-sol YOUR_SOLANA_PUBKEY --unmanaged
```
The `--agent-sol` flag can also be set via `AGENTWALLET_AGENT_SOL` env var.
JSON output (both chains):
```json
{ "base": { "wallet": "0x...", "setupUrl": "..." }, "solana": { "wallet": "...", "setupUrl": "..." } }
```
To create a single chain only:
```bash
# Base only
npx @agntos/agentwallet create --chain base --agent 0xYOUR_BASE_ADDRESS
# Solana only
npx @agntos/agentwallet create --chain solana --agent YOUR_SOLANA_PUBKEY
```
**Managed wallets** return a `setupUrl` — send it to your human. They set limits and register their passkey (FaceID/YubiKey). One-time setup.
**Unmanaged wallets** have no human owner. Fully autonomous.
Default limits: **$50/day, $25/tx**. **Gas is free** — every wallet is funded on creation.
### 3. Fund the wallet
- **Base:** Send ETH and/or USDC to the wallet address on Base (chain ID 8453)
- **Solana:** Send SOL and/or SPL tokens to the wallet PDA on Solana
### 4. Transact
#### Base (EVM)
Call the wallet contract directly with your agent's private key:
```typescript
import { Wallet, Contract, JsonRpcProvider, parseEther } from 'ethers'
const AGENT_KEY = process.env.AGENT_PRIVATE_KEY
const WALLET_ADDR = process.env.WALLET_ADDRESS
const provider = new JsonRpcProvider('https://base-rpc.publicnode.com')
const agent = new Wallet(AGENT_KEY, provider)
const wallet = new Contract(WALLET_ADDR, [
'function execute(address to, uint256 value, bytes data) external',
'function executeERC20(address token, address to, uint256 amount) external',
'function getSpentToday() external view returns (uint256)',
'function getRemainingDaily() external view returns (uint256)',
'function getPolicy() external view returns (uint256 dailyLimit, uint256 perTxLimit, bool paused)',
], agent)
// Send ETH
await wallet.execute('0xRecipient', parseEther('0.001'), '0x')
// Send USDC (6 decimals)
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
await wallet.executeERC20(USDC, '0xRecipient', 5_000_000n) // 5 USDC
// Call any contract (swap, mint, etc.)
await wallet.execute('0xContractAddr', parseEther('0.01'), '0xEncodedCalldata')
// Check remaining budget
const remaining = await wallet.getRemainingDaily() // USDC units (6 decimals)
const remainingUsd = Number(remaining) / 1e6
if (remainingUsd < amountNeeded) {
// Request a limit increase
}
```
#### Solana
Agents transact via the Anchor program directly:
```typescript
import { Program, AnchorProvider } from '@coral-xyz/anchor'
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
import BN from 'bn.js'
const connection = new Connection('https://api.devnet.solana.com')
const agentKeypair = Keypair.fromSecretKey(bs58.decode(AGENT_PRIVATE_KEY))
// Transfer SOL
await program.methods
.transferSol(new BN(amountUsdc), new BN(amountLamports))
.accounts({
wallet: walletPda,
agent: agentKeypair.publicKey,
recipient: recipientPubkey
})
.signers([agentKeypair])
.rpc()
// Transfer SPL token
await program.methods
.transferToken(new BN(tokenAmount), new BN(amountUsdc))
.accounts({
wallet: walletPda,
agent: agentKeypair.publicKey,
mint: mintPubkey,
walletTokenAccount,
recipientTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID
})
.signers([agentKeypair])
.rpc()
```
Solana wallet PDAs are derived with seeds `["wallet", owner, agent, index]`.
Transactions that exceed limits **revert instantly** onchain. Check remaining budget first.
### 5. Check wallet status
```bash
npx @agntos/agentwallet status 0xWALLET_ADDRESS # Base (auto-detected)
npx @agntos/agentwallet status SOLANA_WALLET_PDA # Solana (auto-detected)
npx @agntos/agentwallet status 0xWALLET_ADDRESS --json
```
The `status` command auto-detects chain by address format: `0x` prefix → Base, base58 → Solana.
### 6. Request higher limits
```bash
npx @agntos/agentwallet limits 0xWALLET --daily 200 --pertx 100 --reason "Trading requires higher limits"
```
Returns a URL. Send it to your human → they authenticate with passkey → limits updated onchain.
### 7. Set per-token limits (optional)
#### Base (ERC-20)
```bash
npx @agntos/agentwallet token-limit 0xWALLET --token 0xTOKEN --token-daily 1000 --token-pertx 300
```
#### Solana (SPL tokens)
Per-token limits are stored onchain in the wallet PDA. Up to 16 tokens can have individual daily/per-tx limits.
### 8. Emergency pause
```bash
npx @agntos/agentwallet pause 0xWALLET --reason "Suspicious activity detected"
```
Once approved, **all agent transactions revert** until unpaused.
## All Commands
```bash
npx @agntos/agentwallet keygen # generate BOTH Base + Solana keypairs
npx @agntos/agentwallet keygen --chain base # generate Base keypair only
npx @agntos/agentwallet keygen --chain solana # generate Solana keypair only
npx @agntos/agentwallet create --agent 0x... --agent-sol Sol... # managed wallets (both chains)
npx @agntos/agentwallet create --chain base --agent 0x... # managed wallet (Base only)
npx @agntos/agentwallet create --chain solana --agent PUBKEY # managed wallet (Solana only)
npx @agntos/agentwallet create --agent 0x... --unmanaged # autonomous wallet
npx @agntos/agentwallet status 0xWALLET # wallet info (auto-detects chain)
npx @agntos/agentwallet limits 0xWALLET --daily N --pertx N --reason "..."
npx @agntos/agentwallet token-limit 0xWALLET --token 0x... --token-daily N --token-pertx N
npx @agntos/agentwallet rm-token 0xWALLET --token 0x...
npx @agntos/agentwallet pause 0xWALLET --reason "..."
npx @agntos/agentwallet unpause 0xWALLET
npx @agntos/agentwallet stats
```
All commands support `--json` for machine-readable output.
## Limit Tracking
### Base
| Asset | Tracking | Limits |
|-------|----------|--------|
| **ETH** | → USD via Chainlink oracle | Shared USD daily + per-tx |
| **USDC** | 1:1 USD | Same shared pool as ETH |
| **Other ERC-20s** | Unlimited by default | Owner can set per-token limits |
ETH + USDC share an **aggregated USD daily limit**. Spending $30 in ETH and $15 in USDC = $45 against a $50 limit.
### Solana
| Asset | Tracking | Limits |
|-------|----------|--------|
| **SOL** | USD amount passed by agent | Shared USD daily + per-tx |
| **SPL tokens** | Per-token tracking | Up to 16 tokens with individual limits |
Limits are USD-denominated (6 decimals). Daily spending resets based on `unix_timestamp / 86400`.
## Contract Addresses
### Base Mainnet
| Contract | Address |
|----------|---------|
| Factory | `0x77c2a63BB08b090b46eb612235604dEB8150A4A1` |
| Implementation | `0xEF85c0F9D468632Ff97a36235FC73d70cc19BAbA` |
| Chainlink ETH/USD | `0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70` |
| USDC | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
### Solana Devnet
| Item | Address |
|------|---------|
| Program | `4XHYgv4fczfAtkKB792yrP57iakR9extKtkigsXCJm5e` |
| IDL Account | `6tEPFHmaaDMH2rth1jPWyvDDxh6GcZhkAEj9kKTCY9k6` |
## Security Model
- **Non-custodial**: your private key never leaves your machine
- **Onchain enforcement**: limits are in the smart contract / Solana program, not the API
- **Gas-sponsored**: free gas on creation, transact immediately
- **Passkey ownership**: human's key in device secure enclave
- Base: verified on-chain via RIP-7212 precompile
- Solana: verified via secp256r1 precompile (P-256)
- **Irrevocable handoff**: after passkey registration, admin loses control
- Base: owner set to zero address
- Solana: owner transferred to dead address `11111111111111111111111111111112`
- **Chainlink oracle** (Base): decentralized price feed, 1-hour staleness check
- **Emergency controls**: owner can pause, withdraw, blacklist at any time
- **Direct contract access**: you can bypass the API entirely and call contracts directly
don't have the plugin yet? install it then click "run inline in claude" again.
by @clawhub
added explicit decision points for managed vs unmanaged wallets, RPC/auth edge cases, Solana ATA requirements, and Chainlink staleness; structured procedure into 8 numbered steps with clear inputs/outputs; moved contract addresses to output contract section; preserved all original safety rules and commands.
build non-custodial smart wallets for AI agents on Base (EVM) and Solana that enforce onchain spending limits, support passkey-based human override, and come pre-funded with gas. use this when your agent needs to transact autonomously without custody risk, with humans able to pause, adjust limits, or reclaim funds at any time. works for both managed wallets (with human owner) and fully autonomous agents.
external connections:
https://base-rpc.publicnode.com, override via BASE_RPC_URL env varhttps://api.devnet.solana.com, override via SOLANA_RPC_URL env var0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70, used for limit tracking (1-hour staleness tolerance)required context:
AGENT_PRIVATE_KEY env var. never log or commit this.AGENT_PRIVATE_KEY_SOL env var["wallet", owner, agent, index]setupUrloptional context:
edge cases and limits:
status command returns zero balances if wallet not yet funded; transactions revert (not fail silently) if limit exceededinputs: optional --chain flag (base, solana, or omit for both)
run the keygen command to create one or both keypairs:
npx @agntos/agentwallet keygen
# outputs both Base (0x-prefixed) and Solana (base58) keypairs, one-time display only
npx @agntos/agentwallet keygen --chain base
# outputs Base keypair only
npx @agntos/agentwallet keygen --chain solana
# outputs Solana keypair only
npx @agntos/agentwallet keygen --json
# machine-readable output: { "base": { "address": "0x...", "privateKey": "0x...", "chain": "base" }, "solana": { "address": "7Kp...", "privateKey": "4vJ...", "chain": "solana" } }
outputs: agent address (for Base: 0x-prefixed, 42 chars; for Solana: base58, 44 chars) and private key (hex for Base, base58 for Solana).
critical: save private keys to secure storage (secrets manager, encrypted file, env var) immediately. this is the only time they display.
inputs: agent public address (Base or Solana or both), optional --unmanaged flag, optional --chain flag
create wallets for your agent:
# both chains, managed (human can set limits and passkey)
npx @agntos/agentwallet create --agent 0xYOUR_BASE_ADDRESS --agent-sol YOUR_SOLANA_PUBKEY
# Base only, managed
npx @agntos/agentwallet create --chain base --agent 0xYOUR_BASE_ADDRESS
# Solana only, managed
npx @agntos/agentwallet create --chain solana --agent YOUR_SOLANA_PUBKEY
# both chains, autonomous (no human control)
npx @agntos/agentwallet create --agent 0xYOUR_BASE_ADDRESS --agent-sol YOUR_SOLANA_PUBKEY --unmanaged
# JSON output for parsing
npx @agntos/agentwallet create --agent 0x... --agent-sol ... --json
env var override: --agent-sol can also be set via AGENTWALLET_AGENT_SOL.
outputs: wallet address (Base: 0x-prefixed; Solana: base58 PDA), and setupUrl for managed wallets.
default limits: $50/day, $25/per-tx. ETH and USDC share an aggregated USD pool. ERC-20s and SPL tokens unlimited by default.
free gas: every wallet is pre-funded on creation; agent can transact immediately.
inputs: wallet address, amount in native asset or ERC-20/SPL token
send funds to the created wallet:
outputs: none (confirmed via block explorer or status command)
inputs: wallet address, optional --json flag
npx @agntos/agentwallet status 0xWALLET_ADDRESS
# auto-detects chain by address format (0x = Base, base58 = Solana)
npx @agntos/agentwallet status 0xWALLET_ADDRESS --json
# machine-readable: { "wallet": "0x...", "chain": "base", "dailyLimit": "50000000", "perTxLimit": "25000000", "spent": "15000000", "remaining": "35000000", "paused": false, ... }
outputs: current daily/per-tx limits, amount spent today (in USD, 6 decimals), amount remaining, pause status, list of per-token limits (if any).
critical: always check remaining budget before attempting a large transaction. transactions exceeding limits revert onchain and waste gas (even though gas is free, execution time is not).
inputs: agent private key, wallet contract address, recipient address, amount or encoded calldata
use ethers.js (or equivalent) to call the wallet contract:
import { Wallet, Contract, JsonRpcProvider, parseEther } from 'ethers'
const AGENT_KEY = process.env.AGENT_PRIVATE_KEY
const WALLET_ADDR = process.env.WALLET_ADDRESS
const provider = new JsonRpcProvider('https://base-rpc.publicnode.com')
const agent = new Wallet(AGENT_KEY, provider)
const wallet = new Contract(WALLET_ADDR, [
'function execute(address to, uint256 value, bytes data) external',
'function executeERC20(address token, address to, uint256 amount) external',
'function getRemainingDaily() external view returns (uint256)',
'function getPolicy() external view returns (uint256 dailyLimit, uint256 perTxLimit, bool paused)',
], agent)
// send native ETH
await wallet.execute('0xRecipient', parseEther('0.001'), '0x')
// send USDC (6 decimals)
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
await wallet.executeERC20(USDC, '0xRecipient', 5_000_000n) // 5 USDC
// call any contract (swap, mint, LP, etc.)
await wallet.execute('0xDexAddr', parseEther('0.01'), '0xSwapCalldata')
// query remaining budget before large tx
const remaining = await wallet.getRemainingDaily()
const remainingUsd = Number(remaining) / 1e6
if (remainingUsd < 10) {
console.error('insufficient budget, request limit increase')
}
outputs: transaction hash, confirmed onchain. if limit exceeded, transaction reverts with no state change.
edge case: if agent is not the wallet's admin, calls will revert. verify agent address matches wallet creation input.
inputs: agent keypair (Ed25519), wallet PDA, recipient public key, amount in lamports/smallest token unit, and USD amount spent
use Anchor to call the wallet program:
import { Program, AnchorProvider } from '@coral-xyz/anchor'
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
import BN from 'bn.js'
import bs58 from 'bs58'
const connection = new Connection('https://api.devnet.solana.com')
const agentKeypair = Keypair.fromSecretKey(bs58.decode(process.env.AGENT_PRIVATE_KEY_SOL))
const walletPda = new PublicKey('...')
const recipientPubkey = new PublicKey('...')
// transfer SOL
await program.methods
.transferSol(new BN(usdAmount), new BN(lamports))
.accounts({
wallet: walletPda,
agent: agentKeypair.publicKey,
recipient: recipientPubkey
})
.signers([agentKeypair])
.rpc()
// transfer SPL token
const mint = new PublicKey('...')
const walletTokenAccount = await getAssociatedTokenAddress(mint, walletPda, true)
const recipientTokenAccount = await getAssociatedTokenAddress(mint, recipientPubkey)
await program.methods
.transferToken(new BN(tokenAmount), new BN(usdAmount))
.accounts({
wallet: walletPda,
agent: agentKeypair.publicKey,
mint,
walletTokenAccount,
recipientTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID
})
.signers([agentKeypair])
.rpc()
outputs: transaction signature, confirmed onchain. if limit exceeded, instruction fails atomically.
edge case: SPL token accounts (ATA) must exist before transfer; transferToken does not create them. agent must have created or funded ATA in advance.
inputs: wallet address, new daily and/or per-tx limit amounts, optional reason string
request a limit increase (managed wallets only):
npx @agntos/agentwallet limits 0xWALLET --daily 200 --pertx 100 --reason "high-volume trading"
outputs: approval URL. send to human owner, they authenticate with passkey, limits update onchain.
edge case: unmanaged wallets cannot request limits; owner-initiated pause/withdrawal is the only override.
inputs: wallet address, token contract address (Base) or mint (Solana), daily and per-tx caps in smallest units
set individual caps for specific tokens:
# Base ERC-20
npx @agntos/agentwallet token-limit 0xWALLET --token 0xTOKEN_ADDR --token-daily 1000 --token-pertx 300
# Solana SPL
npx @agntos/agentwallet token-limit WALLET_PDA --token MINT_PUBKEY --token-daily 1000 --token-pertx 300
outputs: confirmation, limit stored onchain.
constraints: Base allows unlimited tokens; Solana supports up to 16 tokens with individual limits.
inputs: wallet address, optional reason string
pause all agent transactions immediately:
npx @agntos/agentwallet pause 0xWALLET --reason "suspicious activity"
outputs: approval URL (managed wallets) or immediate pause (unmanaged). once paused, all agent-signed transactions revert.
unpause with:
npx @agntos/agentwallet unpause 0xWALLET
if creating a managed wallet (human control):
setupUrl. send to human to register passkey and set limits. once passkey is registered, admin (you) loses control permanently.else if creating unmanaged wallet (fully autonomous):
setupUrl returned. wallet operates under default limits and only emergency pause (admin-initiated) is available.if remaining daily budget is zero or near zero:
if Solana SPL token transfer and recipient ATA does not exist:
getAssociatedTokenAddress(..., false) and fund it, or use a wallet that auto-creates ATAs.if Chainlink oracle (Base) is stale (>1 hour):
if RPC returns 429 (rate limited):
if wallet address format unrecognized in status command:
--chain base or --chain solana.if human loses device (passkey gone):
keygen:
{ "base": { "address": "0x...", "privateKey": "0x...", "chain": "base" }, "solana": { ... } }create:
{ "base": { "wallet": "0x...", "setupUrl": "..." }, "solana": { "wallet": "...", "setupUrl": "..." } }status:
{ "wallet": "0x...", "chain": "base", "dailyLimit": "50000000", "perTxLimit": "25000000", "spent": "15000000", "remaining": "35000000", "paused": false, "tokenLimits": [ ... ] }limits / pause / token-limit:
{ "status": "pending" | "approved", "approvalUrl": "..." }Base contract addresses (mainnet):
0x77c2a63BB08b090b46eb612235604dEB8150A4A10xEF85c0F9D468632Ff97a36235FC73d70cc19BAbA0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb700x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913Solana program addresses (devnet):
4XHYgv4fczfAtkKB792yrP57iakR9extKtkigsXCJm5e6tEPFHmaaDMH2rth1jPWyvDDxh6GcZhkAEj9kKTCY9k6keygen: user has private keys stored in secure location (env var, secrets manager, encrypted file) and can reference them for wallet creation.
create: wallet address appears on Base (Etherscan) or Solana (Solscan), shows zero balance before funding. setup URL (if managed) can be opened by human to register passkey.
fund: wallet balance increases on block explorer after transaction confirms. agent can query via status command.
transact (Base): transaction hash appears on Etherscan, state changes (recipient balance increases or contract action executed). getRemainingDaily() decreases by transaction amount.
transact (Solana): transaction signature appears on Solscan, recipient account balance increases. wallet PDA state updates to reflect new daily spend.
status: command returns non-zero "remaining" value, "paused" is false, and transaction can proceed. if paused or remaining is zero, no further transactions execute.
limits request: human receives email or in-app notification with approval URL. after passkey auth, status command shows new limits onchain.
pause: agent-signed transactions immediately revert with error code. manual inspection shows wallet paused flag is true.
unpause: agent-signed transactions resume execution.
npm package: @agntos/agentwallet
chains: Base (EVM), Solana (both devnet and mainnet)
security model: non-custodial (your private key never leaves your machine), onchain enforcement (limits are in contract/program, not API), passkey ownership (human's key in device secure enclave, verified onchain via RIP-7212 on Base or secp256r1 on Solana), irrevocable handoff (after passkey registration, admin loses control permanently), Chainlink oracle (Base mainnet only, 1-hour staleness tolerance), emergency controls (admin can pause/withdraw at any time).