celo-agent-sdk
v0.1.1
Published
SDK for AI agents to interact with JAW smart wallets via the celo-agent CLI
Readme
Install
npm install celo-agent-sdkPrerequisite: The celo-agent CLI must be installed and available in your PATH.
npm install -g celo-agent-cliThe SDK has zero crypto dependencies. Every method spawns
celo-agentas a subprocess — all signing and key material stays inside the CLI process, never in your agent's memory.
Setup (one-time, human)
Before your agent can transact, a human must run the interactive setup once. This generates a session key, opens the browser for passkey approval, and optionally sets up identity + registry:
celo-agent initThis walks you through 4 steps:
| Step | What it does | Required? | |------|-------------|-----------| | 1. Wallet Setup | Generate session keypair, approve with passkey, set spending limits | Yes | | 2. Self.xyz | ZK passport proof — proves a real human is behind this agent | Optional | | 3. ERC-8004 + ENS | On-chain agent registry on Celo + free ENS subname | Optional | | 4. Scaffold | Generate a starter project from a template | Optional |
After this, the agent can operate headlessly — no browser, no passkey, no human in the loop.
Quick Start
import { createWalletClient } from 'celo-agent-sdk'
const wallet = createWalletClient()
// Send 10 USDC
const tx = await wallet.send({ to: '0xRecipient', amount: '10', token: 'USDC' })
console.log(tx.txHash)
// Check balance
const bal = await wallet.balance()
console.log(bal.balances)
// Pay for an API automatically via x402
const fetchWithPay = wallet.x402Fetch({ maxAmount: '1000000' })
const res = await fetchWithPay('https://paid-api.com/data')
const data = await res.json()API Reference
- createWalletClient
- Transactions — send | call
- Read-Only — balance | status
- Signing — sign | signMessage | signTyped
- x402 Payments — x402Fetch | createX402Fetch
- Self.xyz Identity — selfRegister | selfStatus
- ERC-8004 Registry — erc8004Status | erc8004Update | erc8004Verify | erc8004ReputationScore | erc8004Feedback
createWalletClient
Creates the SDK client. All methods are available on the returned object.
import { createWalletClient } from 'celo-agent-sdk'
const wallet = createWalletClient({
cliPath?: string // path to celo-agent binary (default: "celo-agent" from PATH)
env?: Record<string, string> // extra env vars passed to every CLI call
})Config:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| cliPath | string | "celo-agent" | Override the CLI binary path |
| env | Record<string, string> | {} | Extra environment variables for the CLI subprocess |
// Example: point to a custom CLI binary + override chain
const wallet = createWalletClient({
cliPath: '/usr/local/bin/celo-agent',
env: { JAW_CHAIN_ID: '42220' },
})Transactions
wallet.send(params)
Send tokens. Broadcasts immediately (no dry run).
const result = await wallet.send({
to: '0xe08224b2cfaf4f27e2dc7cb3f6b99acc68cf06c0',
amount: '10',
token: 'USDC',
})Params:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| to | string | Yes | Recipient address |
| amount | string | Yes | Human-readable amount (e.g. "10", "0.5") |
| token | string | Yes | Token symbol: CELO, USDC, USDT, DAI |
Returns: SendResult
{
txHash: '0x...',
status: 'confirmed'
}wallet.call(params)
Execute an arbitrary smart contract call. Broadcasts immediately.
// Simple call
const result = await wallet.call({
to: '0xContractAddress',
data: '0xa9059cbb000000000000000000000000...',
})
// With ETH value
const result2 = await wallet.call({
to: '0xContractAddress',
data: '0x...',
value: '0.1',
})
// Via PermissionsManager (uses stored permissionId)
const result3 = await wallet.call({
to: '0xContractAddress',
data: '0x...',
permission: true,
})
// Via PermissionsManager (explicit permissionId)
const result4 = await wallet.call({
to: '0xContractAddress',
data: '0x...',
permission: '0xYourPermissionId',
})Params:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| to | string | Yes | Target contract address |
| data | string | Yes | ABI-encoded calldata (0x-prefixed hex) |
| value | string | No | ETH/native value to send (default: "0") |
| permission | boolean \| string | No | true = use stored permissionId, or pass a specific 0x... ID |
Returns: CallResult
{
txHash: '0x...',
status: 'confirmed'
}Read-Only
wallet.balance()
Check wallet balances for native + ERC-20 tokens. No session key needed.
const balance = await wallet.balance()
console.log(balance.walletAddress)
balance.balances.forEach(b => {
console.log(`${b.symbol}: ${b.balance}`)
})Returns: BalanceResult
{
walletAddress: '0x...',
chainId: 11142220,
balances: [
{ symbol: 'CELO', balance: '1.5' },
{ symbol: 'USDC', balance: '100.0', address: '0x01C5...' }
]
}wallet.status()
Check if the agent is ready, view permissions, and see expiry info.
const status = await wallet.status()
if (!status.ready) {
console.error('Run `celo-agent setup` first')
process.exit(1)
}
console.log(`Agent: ${status.agentName}`)
console.log(`Wallet: ${status.walletAddress}`)
console.log(`Expires: ${status.expiresIn}`)Returns: StatusResult
{
ready: true,
agentName: 'my-agent',
walletAddress: '0x...',
permissionId: '0x...',
chainId: 11142220,
expiry: '2025-04-01T00:00:00.000Z',
expiresIn: '6 days',
permission: { /* on-chain permission data */ }
}Signing
Three signing modes. sign uses the raw session key EOA. signMessage and signTyped sign via the smart account.
wallet.sign({ hash })
Raw secp256k1 signature of a 32-byte hash. No message prefix. Uses the session key EOA directly.
const { signature } = await wallet.sign({
hash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'
})
// signature: '0x...' (65 bytes, r + s + v)| Field | Type | Required | Description |
|-------|------|----------|-------------|
| hash | string | Yes | 0x-prefixed 32-byte hex hash |
wallet.signMessage({ message })
EIP-191 personal sign. Prepends "\x19Ethereum Signed Message:\n" prefix. Signs via the smart account (EIP-1271 compatible).
const { signature } = await wallet.signMessage({
message: 'Hello from my agent'
})| Field | Type | Required | Description |
|-------|------|----------|-------------|
| message | string | Yes | Plain text message |
wallet.signTyped({ data })
EIP-712 typed data signature via the smart account.
const { signature } = await wallet.signTyped({
data: {
domain: {
name: 'MyProtocol',
version: '1',
chainId: 11142220,
verifyingContract: '0xContractAddress',
},
types: {
Order: [
{ name: 'maker', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
},
primaryType: 'Order',
message: {
maker: '0xAgentWallet',
amount: '1000000',
},
},
})| Field | Type | Required | Description |
|-------|------|----------|-------------|
| data | object \| string | Yes | EIP-712 typed data (object or JSON string) with domain, types, primaryType, message |
All three return: SignResult
{
signature: '0x...'
}x402 Payments
Pay for APIs automatically. When a server returns HTTP 402, the SDK signs an EIP-3009 USDC payment and retries with the payment header. No API keys needed — the signature IS the payment.
wallet.x402Fetch(options?)
Returns a fetch-compatible function that auto-handles 402 responses.
const fetchWithPay = wallet.x402Fetch({
maxAmount: '1000000', // safety cap: max 1 USDC per request
onPaymentRequired: ({ amount, payTo, asset }) => {
console.log(`Paying ${amount} to ${payTo}`)
return true // return false to reject
},
})
// Use like normal fetch — 402s are handled automatically
const res = await fetchWithPay('https://paid-api.com/premium-data')
const data = await res.json()
// Works with any fetch options
const res2 = await fetchWithPay('https://paid-api.com/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: 'something' }),
})Options:
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| maxAmount | string | No | Maximum payment in smallest unit (e.g. "1000000" = 1 USDC). Throws if price exceeds this. |
| onPaymentRequired | (info) => boolean \| Promise<boolean> | No | Callback before each payment. Return false to reject. Receives { amount, payTo, asset }. |
How it works internally:
- Makes the initial request
- If response is 402 → parses payment requirements (V1 body or V2
PAYMENT-REQUIREDheader) - Checks
maxAmountguard and callsonPaymentRequiredcallback - Shells out to
celo-agent x402-signto sign the EIP-3009 payment - Auto-tops up the session key EOA from the human's wallet if USDC balance is insufficient
- Retries the request with the payment header (
X-PAYMENTorPAYMENT-SIGNATURE) - Returns the paid response
createX402Fetch(options?)
Standalone import — same functionality without creating a wallet client first.
import { createX402Fetch } from 'celo-agent-sdk'
const fetchWithPay = createX402Fetch({
maxAmount: '100000', // max 0.1 USDC per request
cliPath: '/usr/local/bin/celo-agent', // optional CLI path override
env: { JAW_CHAIN_ID: '11142220' }, // optional extra env vars
onPaymentRequired: ({ amount, payTo }) => {
console.log(`Payment: ${amount} → ${payTo}`)
return true
},
})
const res = await fetchWithPay('https://paid-api.com/data')Options: Same as x402Fetch plus:
| Option | Type | Description |
|--------|------|-------------|
| cliPath | string | Override CLI binary path |
| env | Record<string, string> | Extra env vars for CLI subprocess |
Self.xyz Identity
wallet.selfRegister(params?)
Register the agent with Self.xyz for on-chain identity verification. Opens a browser for the human to complete a ZK proof.
const result = await wallet.selfRegister({
network: 'testnet', // 'testnet' or 'mainnet'
minAge: '18', // '0', '18', or '21'
ofac: true, // request OFAC compliance check
})Params:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| network | string | 'testnet' | Self.xyz network |
| minAge | string | '0' | Minimum age disclosure |
| ofac | boolean | false | Request OFAC compliance |
Returns: SelfRegisterResult
{
status: 'registered',
selfAgentId: 42,
selfAgentAddress: '0x...',
network: 'testnet'
}wallet.selfStatus()
Check on-chain Self.xyz registration status.
const status = await wallet.selfStatus()
if (status.isVerified) {
console.log(`Verified! Strength: ${status.strengthLabel}`)
}Returns: SelfStatusResult
{
registered: true,
selfAgentId: 42,
agentAddress: '0x...',
isVerified: true,
verificationStrength: 3,
strengthLabel: 'strong',
network: 'testnet'
}ERC-8004 Registry
On-chain agent identity registry on Celo. Register your agent, update metadata, verify endpoints, and manage reputation.
wallet.erc8004Status(params?)
Read on-chain registration status and the Agent Card.
const status = await wallet.erc8004Status({
network: 'testnet', // 'testnet' or 'mainnet'
agentId: '53', // optional: query a specific agent
})Params:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| network | string | 'testnet' | Celo network |
| agentId | string | from config | Override agent ID to query |
Returns: ERC8004StatusResult
{
registered: true,
agentId: 53,
owner: '0x...',
walletAddress: '0x...',
agentURI: 'ipfs://Qm...',
registrationFile: { /* full Agent Card JSON */ },
network: 'testnet',
scan: 'https://8004scan.com/agent/53'
}wallet.erc8004Update(params)
Update your agent's on-chain metadata. Deep-merges your overrides into the existing Agent Card and publishes to IPFS + on-chain.
const result = await wallet.erc8004Update({
network: 'testnet',
name: 'My Agent',
description: 'An AI agent that manages DeFi positions',
url: 'https://myagent.com',
a2a: 'https://myagent.com/.well-known/agent.json',
mcp: 'https://myagent.com/mcp',
did: 'did:key:z6Mk...',
email: '[email protected]',
image: 'https://myagent.com/avatar.png',
x402: true,
tags: ['ai-agent', 'defi', 'wallet'],
categories: ['finance', 'payments'],
protocols: ['ERC-4337', 'x402', 'A2A'],
})Params:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| network | string | No | 'testnet' or 'mainnet' |
| agentId | string | No | Override agent ID |
| name | string | No | Display name |
| description | string | No | Agent description |
| url | string | No | Web endpoint URL |
| a2a | string | No | A2A agent card endpoint |
| mcp | string | No | MCP server endpoint |
| did | string | No | Decentralized identifier |
| email | string | No | Contact email |
| image | string | No | Avatar/icon URL |
| x402 | boolean | No | Advertise x402 support |
| tags | string[] | No | Tags (e.g. ['ai-agent', 'defi']) |
| categories | string[] | No | Categories |
| protocols | string[] | No | Supported protocols |
Returns: ERC8004UpdateResult
{
status: 'updated',
agentId: '53',
txHash: '0x...',
network: 'testnet',
scan: 'https://8004scan.com/agent/53',
registrationFile: { /* updated Agent Card */ }
}wallet.erc8004Verify(params?)
Generate the .well-known/agent-registration.json content needed for endpoint verification on 8004scan.
const verify = await wallet.erc8004Verify({
domain: 'myagent.com',
network: 'testnet',
})
// Host the content at: https://myagent.com/.well-known/agent-registration.json
console.log(JSON.stringify(verify.content, null, 2))Params:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| network | string | from config | Celo network |
| agentId | string | from config | Override agent ID |
| domain | string | from Agent Card | Domain to verify |
Returns: ERC8004VerifyResult
{
path: '.well-known/agent-registration.json',
url: 'https://myagent.com/.well-known/agent-registration.json',
domain: 'myagent.com',
agentId: 53,
network: 'testnet',
content: { /* JSON to host at the path */ }
}wallet.erc8004ReputationScore(params?)
Read an agent's on-chain reputation score from the ERC-8004 Reputation Registry.
const score = await wallet.erc8004ReputationScore({
network: 'testnet',
agentId: '53',
})
console.log(`Score: ${score.reputationScore}/100`)
console.log(`Based on ${score.totalFeedback} reviews`)Params:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| network | string | from config | Celo network |
| agentId | string | from config | Agent to query |
Returns: ERC8004ReputationScoreResult
{
agentId: '53',
network: 'testnet',
reputationRegistry: '0x...',
totalFeedback: '12',
averageScore: '85',
reputationScore: '85',
scan: 'https://8004scan.com/agent/53'
}wallet.erc8004Feedback(params?)
Submit on-chain feedback for an agent to the Reputation Registry.
const feedback = await wallet.erc8004Feedback({
network: 'testnet',
agentId: '53',
score: 90, // 1–100
tag: 'payment', // category tag
content: 'Fast and reliable', // description
})Params:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| network | string | No | Celo network |
| agentId | string | No | Target agent |
| score | number | Yes (for feedback) | Score from 1 to 100 |
| tag | string | No | Feedback category (e.g. 'wallet', 'payment', 'defi') |
| content | string | No | Feedback description text |
Returns: ERC8004FeedbackResult
{
status: 'submitted',
feedbackId: '7',
txHash: '0x...',
agentId: '53',
score: 90,
tag: 'payment',
network: 'testnet'
}Types
All types are exported for use in your TypeScript projects:
import type {
WalletClientConfig,
SendParams,
SendResult,
CallParams,
CallResult,
BalanceResult,
StatusResult,
SignResult,
X402FetchOptions,
} from 'celo-agent-sdk'Error Handling
All methods throw on failure. Errors come directly from the CLI as structured messages.
try {
const result = await wallet.send({
to: '0xRecipient',
amount: '10',
token: 'USDC',
})
} catch (err) {
console.error('Transaction failed:', (err as Error).message)
// e.g. "Session key expired", "Insufficient balance", "Permission denied"
}x402-specific errors:
try {
const res = await fetchWithPay('https://expensive-api.com/data')
} catch (err) {
// "x402 payment rejected: price 5000000 exceeds max 1000000"
// "x402 payment rejected by onPaymentRequired callback."
}Environment Variables
The SDK inherits environment variables from the parent process. You can also pass them via the env option on createWalletClient.
| Variable | Default | Description |
|----------|---------|-------------|
| JAW_CHAIN_ID | 11142220 (Celo Sepolia) | Target chain |
| JAW_API_KEY | built-in | JAW API key |
| JAW_RPC_URL | built-in | RPC endpoint |
| JAW_PAYMASTER_URL | built-in | Pimlico paymaster |
| JAW_SESSION_KEY | — | Raw private key for hosted/Docker mode |
All variables have hardcoded defaults for Celo Sepolia testnet — no .env file required to get started.
How It Works
Your Agent (Node.js)
│
│ wallet.send({ to, amount, token })
│
▼
celo-agent-sdk
│
│ child_process.execFile('celo-agent', ['send', '--to', ...])
│
▼
celo-agent CLI (separate OS process)
│
├── Decrypts session key from ~/.jaw-agent/keystore.json
├── Builds ERC-4337 UserOp
├── Signs with session key
├── Submits to Bundler → Paymaster → Chain
└── Outputs JSON to stdout: { txHash, status }
│
▼
SDK parses JSON → returns typed result to your agentThe agent never sees the private key. The key is decrypted, used for signing, and discarded — all inside the CLI process.
Full Example
import { createWalletClient } from 'celo-agent-sdk'
async function main() {
const wallet = createWalletClient()
// 1. Check if agent is ready
const status = await wallet.status()
if (!status.ready) {
console.error('Run `celo-agent setup` first')
process.exit(1)
}
console.log(`Agent "${status.agentName}" ready on chain ${status.chainId}`)
// 2. Check balance
const balance = await wallet.balance()
for (const b of balance.balances) {
console.log(` ${b.symbol}: ${b.balance}`)
}
// 3. Send USDC
const tx = await wallet.send({
to: '0xe08224b2cfaf4f27e2dc7cb3f6b99acc68cf06c0',
amount: '0.01',
token: 'USDC',
})
console.log('Sent:', tx.txHash)
// 4. Sign a message
const { signature } = await wallet.signMessage({
message: 'Proof that this agent is alive',
})
console.log('Signature:', signature)
// 5. Pay for an API with x402
const fetchWithPay = wallet.x402Fetch({ maxAmount: '1000000' })
try {
const res = await fetchWithPay('https://paid-api.com/data')
if (res.ok) {
console.log('Paid API data:', await res.json())
}
} catch (err) {
console.log('x402 error:', (err as Error).message)
}
// 6. Check on-chain reputation
const rep = await wallet.erc8004ReputationScore({ network: 'testnet' })
console.log(`Reputation: ${rep.reputationScore}/100 (${rep.totalFeedback} reviews)`)
}
main().catch(console.error)