clanker-wallet 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,93 +1,178 @@
1
1
  # clanker-wallet
2
2
 
3
- TypeScript SDK for AI agents to request human-approved Ethereum transactions over an E2E encrypted relay.
3
+ Human-approved blockchain transactions for AI agents. Agent proposes a tx, human reviews and signs it from the [web app](https://clanker-wallet.xyz). All communication is end-to-end encrypted.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install clanker-wallet
8
+ npm install -g clanker-wallet
9
9
  ```
10
10
 
11
- ## Quickstart
11
+ Or run without installing:
12
+ ```bash
13
+ npx clanker-wallet
14
+ bunx clanker-wallet
15
+ ```
12
16
 
13
- ```typescript
14
- import { ClankerWallet } from 'clanker-wallet'
17
+ ## Quick Start
15
18
 
16
- const wallet = new ClankerWallet({ agentName: 'my-trading-bot' })
19
+ ```bash
20
+ # 1. Human goes to https://clanker-wallet.xyz, connects wallet, clicks "copy prompt for agent",
21
+ # pastes the prompt to you. The prompt contains a JSON pairing code.
22
+ # 2. Pair (one-time) — extract the JSON from the prompt
23
+ clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
17
24
 
18
- // Pair with human's web app (scan QR code to get pairing JSON)
19
- wallet.pair(pairingJson)
25
+ # 3. Send a transaction (blocks until human approves or rejects)
26
+ clanker-wallet tx --chain 1 --to 0xRecipient --value 1000000000000000000 --reason "swap ETH for USDC"
20
27
 
21
- // Request a transaction — blocks until human approves or rejects
22
- const txHash = await wallet.requestTx({
23
- chain_id: 1,
24
- transaction: { to: '0x...', value: '1000000000000000000', data: '0x' },
25
- context: { reason: 'Buy tokens', urgency: 'medium' },
26
- })
28
+ # 4. Check status anytime
29
+ clanker-wallet status
30
+ ```
31
+
32
+ ## Commands
27
33
 
28
- console.log('tx hash:', txHash)
34
+ ### `pair`
35
+
36
+ Pair with a human's web app. Run once — identity and pairing are saved to `~/.clanker-wallet/`.
37
+
38
+ ```bash
39
+ clanker-wallet pair '<pairing JSON from web app>'
40
+ # → {"agent_id":"agent_abc123","status":"paired","wallet":"0x...","relay":"wss://..."}
29
41
  ```
30
42
 
31
- ## How It Works
43
+ ### `tx`
32
44
 
33
- - **E2E encrypted** — agent and human derive an X25519 shared secret. The relay only sees ciphertext.
34
- - **Relay transport** — lightweight WebSocket relay forwards encrypted messages between agent and human. No private keys touch the server.
35
- - **Human approval** — the human reviews transaction details in the Clanker Wallet web app and signs with their own wallet (MetaMask, WalletConnect, etc).
45
+ Send a transaction request. Blocks until the human approves or rejects.
36
46
 
37
- ## API
47
+ ```bash
48
+ clanker-wallet tx \
49
+ --chain 1 \
50
+ --to 0xRecipient \
51
+ --value 1000000000000000000 \
52
+ --data 0x \
53
+ --reason "swap ETH for USDC" \
54
+ --urgency medium \
55
+ --timeout 120000
56
+ # approved → {"tx_hash":"0x...","status":"approved"}
57
+ # rejected → {"status":"rejected","error":"..."} (exit code 1)
58
+ # timeout → {"status":"timeout","error":"..."} (exit code 2)
59
+ ```
38
60
 
39
- ### `new ClankerWallet(options?)`
61
+ **Flags:**
62
+ | Flag | Required | Default | Description |
63
+ |------|----------|---------|-------------|
64
+ | `--chain <id>` | yes | — | Chain ID (1=Ethereum, 137=Polygon, 8453=Base) |
65
+ | `--to <address>` | yes | — | Recipient address |
66
+ | `--value <wei>` | no | `0` | Value in wei (1 ETH = `1000000000000000000`) |
67
+ | `--data <hex>` | no | `0x` | Calldata hex |
68
+ | `--reason <text>` | no | — | Shown to human |
69
+ | `--urgency <level>` | no | — | `low`, `medium`, or `high` |
70
+ | `--expected-outcome <text>` | no | — | What will happen |
71
+ | `--timeout <ms>` | no | `3600000` | How long to wait |
40
72
 
41
- | Option | Type | Description |
42
- |--------|------|-------------|
43
- | `agentName` | `string` | Display name shown to human (optional) |
44
- | `secretKey` | `Uint8Array` | 32-byte secret key to restore identity. Omit to generate new keypair. |
73
+ ### `status`
45
74
 
46
- ### Properties
75
+ Check current pairing status.
47
76
 
48
- | Property | Type | Description |
49
- |----------|------|-------------|
50
- | `publicKey` | `Uint8Array` | Agent's X25519 public key |
51
- | `secretKey` | `Uint8Array` | Agent's X25519 secret key (save this to persist identity) |
52
- | `agentId` | `string` | Derived agent identifier (`agent_<pubkey_prefix>`) |
53
- | `paired` | `boolean` | Whether `pair()` has been called |
77
+ ```bash
78
+ clanker-wallet status
79
+ # → {"agent_id":"agent_abc123","paired":true,"wallet":"0x...","relay":"wss://..."}
80
+ ```
81
+
82
+ ### `whoami`
54
83
 
55
- ### `wallet.pair(qrData: string): PairingData`
84
+ Show agent identity. `fingerprint` (first 16 hex chars of the public key) is what the web app shows for this agent; the human compares it before trusting the agent.
56
85
 
57
- Parse pairing JSON (from the web app's QR code) and store the human's public key and relay URL. Returns the parsed `PairingData`.
86
+ ```bash
87
+ clanker-wallet whoami
88
+ # → {"agent_id":"agent_abc123","public_key":"ab12cd34ef567890...","fingerprint":"ab12 cd34 ef56 7890"}
89
+ ```
58
90
 
59
- ### `wallet.requestTx(options): Promise<string>`
91
+ ## Exit Codes
60
92
 
61
- Send a transaction request and wait for human response. Resolves with the transaction hash on approval. Throws on rejection, timeout, or connection error.
93
+ | Code | Meaning |
94
+ |------|---------|
95
+ | 0 | Success |
96
+ | 1 | Rejected by human |
97
+ | 2 | Timeout |
98
+ | 3 | Connection/relay error |
99
+ | 4 | Not paired |
100
+ | 5 | Invalid arguments |
62
101
 
63
- | Option | Type | Description |
64
- |--------|------|-------------|
65
- | `chain_id` | `number` | Target chain (1 = mainnet, 137 = polygon, etc) |
66
- | `transaction` | `{ to, value, data }` | Transaction parameters (`value` in wei) |
67
- | `context` | `{ reason?, urgency?, expected_outcome? }` | Optional context shown to human |
68
- | `timeout_ms` | `number` | Timeout in ms (default: 1 hour) |
102
+ ## State
69
103
 
70
- ## Persisting Agent Identity
104
+ Identity and pairing are persisted in `~/.clanker-wallet/`:
71
105
 
72
- Save `wallet.secretKey` between sessions to keep the same agent identity:
106
+ ```
107
+ ~/.clanker-wallet/
108
+ identity.json # agent keypair (auto-generated on first use)
109
+ pairing.json # human's public key + relay URL
110
+ ```
111
+
112
+ ## Common Chain IDs
113
+
114
+ | Chain | ID |
115
+ |-------|----|
116
+ | Ethereum | 1 |
117
+ | Polygon | 137 |
118
+ | Arbitrum | 42161 |
119
+ | Base | 8453 |
120
+ | Optimism | 10 |
121
+ | Sepolia (testnet) | 11155111 |
122
+
123
+ ---
124
+
125
+ ## Library Usage (Advanced)
126
+
127
+ The CLI wraps a TypeScript library. For programmatic use:
128
+
129
+ ```bash
130
+ npm install clanker-wallet
131
+ ```
73
132
 
74
133
  ```typescript
75
- import { writeFileSync, readFileSync, existsSync } from 'fs'
134
+ import { ClankerWallet } from 'clanker-wallet'
135
+
136
+ const wallet = new ClankerWallet({ agentName: 'my-bot' })
137
+ wallet.pair(pairingJson)
138
+
139
+ const txHash = await wallet.requestTx({
140
+ chain_id: 1,
141
+ transaction: { to: '0x...', value: '1000000000000000000', data: '0x' },
142
+ context: { reason: 'Buy tokens', urgency: 'medium' },
143
+ })
144
+ ```
76
145
 
77
- // Restore or create
78
- const secretKey = existsSync('.agent-key')
79
- ? new Uint8Array(readFileSync('.agent-key'))
80
- : undefined
146
+ See full API: [`src/`](./src/)
81
147
 
82
- const wallet = new ClankerWallet({ secretKey })
148
+ ## Python SDK
83
149
 
84
- // Save for next time
85
- if (!secretKey) writeFileSync('.agent-key', wallet.secretKey)
150
+ ```bash
151
+ pip install clanker-wallet
86
152
  ```
87
153
 
88
- ## Web App
154
+ ```python
155
+ from clanker_wallet import ClankerWallet, Transaction, RequestTxOptions, TxContext
156
+
157
+ agent = ClankerWallet()
158
+ agent.pair(pairing_json)
159
+
160
+ tx_hash = await agent.request_tx(RequestTxOptions(
161
+ chain_id=1,
162
+ transaction=Transaction(to="0x...", value="1000000000000000000", data="0x"),
163
+ context=TxContext(reason="Buy tokens"),
164
+ ))
165
+ ```
166
+
167
+ ## How It Works
168
+
169
+ 1. Human opens [clanker-wallet.xyz](https://clanker-wallet.xyz), connects wallet, clicks "copy prompt for agent" — gets a paste-ready prompt that bundles intent, the setup guide URL, and the pairing JSON
170
+ 2. Human pastes the prompt to their AI agent. Agent extracts the JSON and runs `clanker-wallet pair` — establishes E2E encrypted channel
171
+ 3. Agent runs `clanker-wallet tx` — encrypts tx with nacl.box, sends via WebSocket relay
172
+ 4. Human reviews tx details, simulation, risk flags in the web app. A new agent shows up as unverified with its fingerprint; auto-approve rules only apply after the human trusts it. Contract rules (address + methods) don't limit token amounts or recipients
173
+ 5. Human approves → wallet signs + broadcasts → agent gets the tx hash
89
174
 
90
- The human-facing web app is at [clanker-wallet.vercel.app](https://clanker-wallet.vercel.app). Connect a wallet, scan the QR code from your agent, and approve transactions.
175
+ Request and response payloads are encrypted end-to-end; the relay only forwards them. Exception: when paired in managed mode, the agent also sends the transaction fields (from, to, value, data, chainId) to the relay for simulation.
91
176
 
92
177
  ## License
93
178
 
package/SKILL.md ADDED
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: clanker-wallet
3
+ description: "Send blockchain transactions through a human-approved wallet. You propose tx, human reviews and signs. No private keys needed."
4
+ homepage: https://clanker-wallet.xyz
5
+ ---
6
+
7
+ # Clanker Wallet — Human-Approved Transactions
8
+
9
+ You are an AI agent that needs to send blockchain transactions. You do NOT have a private key. Instead, you propose transactions and a human approves them from their own wallet.
10
+
11
+ ## Setup
12
+
13
+ ```bash
14
+ npm install -g clanker-wallet
15
+ ```
16
+
17
+ Or use without installing: `npx clanker-wallet` / `bunx clanker-wallet`
18
+
19
+ ## Pairing (one-time)
20
+
21
+ The human will paste a prompt block from https://clanker-wallet.xyz that looks like this:
22
+
23
+ > read the setup guide first: https://clanker-wallet.xyz/llms.txt
24
+ >
25
+ > then pair using this code:
26
+ > `{"version":1,"pubkey":"ab12cd...","relay":"wss://...","wallet":"0x..."}`
27
+
28
+ Extract the JSON object (the line starting with `{`) and pair:
29
+
30
+ ```bash
31
+ clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
32
+ ```
33
+
34
+ If the human only pastes raw JSON (no prompt prose), pair with it directly. If they only paste the URL, ask them to click "copy prompt for agent" on the pairing screen.
35
+
36
+ This saves identity and pairing to `~/.clanker-wallet/`. You only need to do this once.
37
+
38
+ ### Trust (first request)
39
+
40
+ The web app lists a new agent as unverified and shows its fingerprint (e.g. `ab12 cd34 ef56 7890`). Until the human compares it with the `fingerprint` from `clanker-wallet whoami` and clicks "trust this agent", none of their auto-approve rules apply: every tx waits for manual approval. Contract rules (contract address + allowed methods) do not limit token amounts or recipients: a rule allowing `transfer` on a token auto-approves any amount to any recipient. If asked, run `clanker-wallet whoami` and share the fingerprint.
41
+
42
+ ### Transaction notifications
43
+
44
+ After pairing, ask the human:
45
+
46
+ > "Would you like me to notify you here when I need a transaction approved? I'll send you a message with a direct link to approve it."
47
+
48
+ If they agree, **every time you send a `tx` command**, immediately send a message through your current communication channel with:
49
+
50
+ 1. A short description of what the tx does (from the `--reason`)
51
+ 2. A direct link to the web app: **https://clanker-wallet.xyz**
52
+ 3. The urgency level
53
+
54
+ Example notification:
55
+ > Transaction awaiting your approval
56
+ > I'm requesting to send 0.001 ETH on Sepolia to a test address.
57
+ > Urgency: normal
58
+ > [Approve or reject here](https://clanker-wallet.xyz)
59
+
60
+ ## Sending a Transaction
61
+
62
+ ```bash
63
+ clanker-wallet tx \
64
+ --chain 1 \
65
+ --to 0xRecipientAddress \
66
+ --value 1000000000000000 \
67
+ --reason "Send 0.001 ETH to recipient" \
68
+ --urgency medium \
69
+ --timeout 120000
70
+ ```
71
+
72
+ The command **blocks until the human approves or rejects**. Output is JSON:
73
+ - Approved: `{"tx_hash":"0x...","status":"approved"}` (exit code 0)
74
+ - Rejected: `{"status":"rejected","error":"..."}` (exit code 1)
75
+ - Timeout: `{"status":"timeout","error":"..."}` (exit code 2)
76
+
77
+ ## Critical Details
78
+
79
+ - **value is in WEI, not ETH.** 1 ETH = `1000000000000000000`. Use string, not number.
80
+ - **data is hex-encoded calldata.** Use `0x` for simple ETH transfers. For contract calls, encode the function call.
81
+ - **The human must be online to approve.** Set a reasonable `--timeout`. Always notify the human through your communication channel when you send a request (if they opted in).
82
+
83
+ ## tx Flags
84
+
85
+ | Flag | Required | Default | Description |
86
+ |------|----------|---------|-------------|
87
+ | `--chain <id>` | yes | — | Chain ID (1=Ethereum, 137=Polygon, 8453=Base) |
88
+ | `--to <address>` | yes | — | Recipient address |
89
+ | `--value <wei>` | no | `0` | Value in wei |
90
+ | `--data <hex>` | no | `0x` | Calldata hex |
91
+ | `--reason <text>` | no | — | Shown to human |
92
+ | `--urgency <level>` | no | — | `low`, `medium`, or `high` |
93
+ | `--expected-outcome <text>` | no | — | What will happen |
94
+ | `--timeout <ms>` | no | `3600000` | How long to wait |
95
+
96
+ ## Common Chain IDs
97
+
98
+ | Chain | ID |
99
+ |-------|----|
100
+ | Ethereum | 1 |
101
+ | Polygon | 137 |
102
+ | Arbitrum | 42161 |
103
+ | Base | 8453 |
104
+ | Optimism | 10 |
105
+ | Sepolia (testnet) | 11155111 |
106
+
107
+ ## Common Patterns
108
+
109
+ ### Simple ETH transfer
110
+ ```bash
111
+ clanker-wallet tx --chain 1 --to 0xRecipientAddress --value 1000000000000000 --reason "Send 0.001 ETH"
112
+ ```
113
+
114
+ ### ERC-20 token transfer
115
+ ```bash
116
+ # transfer(address to, uint256 amount) — selector 0xa9059cbb
117
+ # encode to + amount as 32-byte padded hex
118
+ clanker-wallet tx \
119
+ --chain 1 \
120
+ --to 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
121
+ --data 0xa9059cbb000000000000000000000000RecipientAddr00000000000000000000000000000000000000000000000000000000000f4240 \
122
+ --reason "Transfer 1 USDC"
123
+ ```
124
+
125
+ ### ERC-20 approval
126
+ ```bash
127
+ # approve(address spender, uint256 amount) — selector 0x095ea7b3
128
+ clanker-wallet tx \
129
+ --chain 1 \
130
+ --to 0xTokenContractAddress \
131
+ --data 0x095ea7b3000000000000000000000000SpenderAddr0000000000000000000000000000000000000000000000000000000000000f4240 \
132
+ --reason "Approve 1 USDC spend for DeFi protocol"
133
+ ```
134
+
135
+ ## Other Commands
136
+
137
+ ```bash
138
+ # Check pairing status
139
+ clanker-wallet status
140
+ # → {"agent_id":"agent_abc123","paired":true,"wallet":"0x...","relay":"wss://..."}
141
+
142
+ # Show agent identity
143
+ clanker-wallet whoami
144
+ # → {"agent_id":"agent_abc123","public_key":"ab12cd34ef567890...","fingerprint":"ab12 cd34 ef56 7890"}
145
+ ```
146
+
147
+ ## Troubleshooting
148
+
149
+ - **exit code 4 / "not paired"** — run `clanker-wallet pair '<JSON>'` first
150
+ - **exit code 2 / timeout** — human didn't respond in time, increase `--timeout` or ask them to check the web app
151
+ - **exit code 1 / "rejected"** — human clicked reject. Explain why the tx is needed and try again if appropriate.
152
+ - **exit code 3 / connection error** — relay may be down, try again in a few seconds
package/bin/cli.js ADDED
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+ import { ClankerWallet } from '../dist/index.js'
3
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs'
4
+ import { join } from 'path'
5
+ import { homedir } from 'os'
6
+
7
+ // ── constants ────────────────────────────────────────────────────────
8
+ const STATE_DIR = join(homedir(), '.clanker-wallet')
9
+ const IDENTITY_FILE = join(STATE_DIR, 'identity.json')
10
+ const PAIRING_FILE = join(STATE_DIR, 'pairing.json')
11
+
12
+ const EXIT = { OK: 0, REJECTED: 1, TIMEOUT: 2, CONNECTION: 3, NOT_PAIRED: 4, BAD_ARGS: 5 }
13
+
14
+ // ── helpers ──────────────────────────────────────────────────────────
15
+ const hex = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
16
+ const fromHexStr = (h) => new Uint8Array(h.match(/.{2}/g).map(b => parseInt(b, 16)))
17
+ /** short id the human compares in the web app: first 16 hex (64 bits) of the x25519 pubkey, "ab12 cd34 ef56 7890" */
18
+ const fingerprint = (pubkeyHex) => pubkeyHex.slice(0, 16).toLowerCase().match(/.{1,4}/g).join(' ')
19
+
20
+ function die(msg, code) {
21
+ process.stderr.write(msg + '\n')
22
+ process.exit(code)
23
+ }
24
+
25
+ function out(obj) {
26
+ process.stdout.write(JSON.stringify(obj) + '\n')
27
+ }
28
+
29
+ // owner-only; an existing dir from an older version is tightened too
30
+ function ensureDir() {
31
+ if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 })
32
+ else chmodSync(STATE_DIR, 0o700)
33
+ }
34
+
35
+ // identity.json holds the agent secret key: owner-only, tightened if an older version left it readable
36
+ function loadIdentity() {
37
+ if (!existsSync(IDENTITY_FILE)) return null
38
+ ensureDir()
39
+ chmodSync(IDENTITY_FILE, 0o600)
40
+ try { return JSON.parse(readFileSync(IDENTITY_FILE, 'utf8')) }
41
+ catch { return null }
42
+ }
43
+
44
+ function saveIdentity(data) {
45
+ ensureDir()
46
+ writeFileSync(IDENTITY_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 })
47
+ chmodSync(IDENTITY_FILE, 0o600)
48
+ }
49
+
50
+ function loadPairing() {
51
+ if (!existsSync(PAIRING_FILE)) return null
52
+ try { return JSON.parse(readFileSync(PAIRING_FILE, 'utf8')) }
53
+ catch { return null }
54
+ }
55
+
56
+ function savePairing(data) {
57
+ ensureDir()
58
+ writeFileSync(PAIRING_FILE, JSON.stringify(data, null, 2) + '\n')
59
+ }
60
+
61
+ /** restore or create a ClankerWallet with persisted identity */
62
+ function getWallet() {
63
+ const identity = loadIdentity()
64
+ const secretKey = identity ? fromHexStr(identity.secret_key) : undefined
65
+ const wallet = new ClankerWallet({ secretKey })
66
+ if (!identity) {
67
+ saveIdentity({ secret_key: hex(wallet.secretKey), agent_id: wallet.agentId })
68
+ }
69
+ return wallet
70
+ }
71
+
72
+ // ── arg parsing ──────────────────────────────────────────────────────
73
+ function parseArgs(argv) {
74
+ const args = argv.slice(2)
75
+ const cmd = args[0]
76
+ const positional = []
77
+ const flags = {}
78
+
79
+ for (let i = 1; i < args.length; i++) {
80
+ const a = args[i]
81
+ if (a.startsWith('--')) {
82
+ const key = a.slice(2)
83
+ const next = args[i + 1]
84
+ if (next && !next.startsWith('--')) {
85
+ flags[key] = next
86
+ i++
87
+ } else {
88
+ flags[key] = true
89
+ }
90
+ } else {
91
+ positional.push(a)
92
+ }
93
+ }
94
+
95
+ return { cmd, positional, flags }
96
+ }
97
+
98
+ // ── commands ─────────────────────────────────────────────────────────
99
+ function cmdPair(positional) {
100
+ const jsonStr = positional[0]
101
+ if (!jsonStr) {
102
+ die('usage: clanker-wallet pair \'<pairing JSON from web app>\'', EXIT.BAD_ARGS)
103
+ }
104
+
105
+ const wallet = getWallet()
106
+
107
+ let pairingData
108
+ try {
109
+ pairingData = wallet.pair(jsonStr)
110
+ } catch (err) {
111
+ die(`pair failed: ${err.message}`, EXIT.BAD_ARGS)
112
+ }
113
+
114
+ savePairing({
115
+ pubkey: pairingData.pubkey,
116
+ relay: pairingData.relay,
117
+ wallet: pairingData.wallet || null,
118
+ mode: pairingData.mode || null,
119
+ })
120
+
121
+ out({
122
+ agent_id: wallet.agentId,
123
+ status: 'paired',
124
+ wallet: pairingData.wallet || null,
125
+ relay: pairingData.relay,
126
+ })
127
+ }
128
+
129
+ async function cmdTx(flags) {
130
+ const pairing = loadPairing()
131
+ if (!pairing) die('not paired — run: clanker-wallet pair \'<JSON>\'', EXIT.NOT_PAIRED)
132
+
133
+ const chain = flags.chain
134
+ const to = flags.to
135
+ if (!chain) die('missing --chain', EXIT.BAD_ARGS)
136
+ if (!to) die('missing --to', EXIT.BAD_ARGS)
137
+
138
+ const chainId = parseInt(chain, 10)
139
+ if (isNaN(chainId)) die('--chain must be a number', EXIT.BAD_ARGS)
140
+
141
+ const value = flags.value || '0'
142
+ const data = flags.data || '0x'
143
+ const timeout = flags.timeout ? parseInt(flags.timeout, 10) : 3_600_000
144
+
145
+ const context = {}
146
+ if (flags.reason) context.reason = flags.reason
147
+ if (flags.urgency) context.urgency = flags.urgency
148
+ if (flags['expected-outcome']) context.expected_outcome = flags['expected-outcome']
149
+
150
+ const wallet = getWallet()
151
+ const pairingJson = JSON.stringify({
152
+ version: 1,
153
+ pubkey: pairing.pubkey,
154
+ relay: pairing.relay,
155
+ wallet: pairing.wallet,
156
+ mode: pairing.mode || undefined,
157
+ })
158
+ wallet.pair(pairingJson)
159
+
160
+ try {
161
+ const txHash = await wallet.requestTx({
162
+ chain_id: chainId,
163
+ transaction: { to, value, data },
164
+ context: Object.keys(context).length > 0 ? context : undefined,
165
+ timeout_ms: timeout,
166
+ })
167
+ out({ tx_hash: txHash, status: 'approved' })
168
+ process.exit(EXIT.OK)
169
+ } catch (err) {
170
+ const msg = err.message || String(err)
171
+ if (msg.includes('rejected')) {
172
+ out({ status: 'rejected', error: msg })
173
+ process.exit(EXIT.REJECTED)
174
+ } else if (msg.includes('timed out')) {
175
+ out({ status: 'timeout', error: msg })
176
+ process.exit(EXIT.TIMEOUT)
177
+ } else {
178
+ out({ status: 'error', error: msg })
179
+ process.exit(EXIT.CONNECTION)
180
+ }
181
+ }
182
+ }
183
+
184
+ function cmdStatus() {
185
+ const identity = loadIdentity()
186
+ const pairing = loadPairing()
187
+
188
+ out({
189
+ agent_id: identity?.agent_id || null,
190
+ paired: !!pairing,
191
+ wallet: pairing?.wallet || null,
192
+ relay: pairing?.relay || null,
193
+ })
194
+ }
195
+
196
+ function cmdWhoami() {
197
+ const wallet = getWallet()
198
+ out({
199
+ agent_id: wallet.agentId,
200
+ public_key: hex(wallet.publicKey),
201
+ fingerprint: fingerprint(hex(wallet.publicKey)),
202
+ })
203
+ }
204
+
205
+ function showHelp() {
206
+ process.stderr.write(`clanker-wallet — human-approved transactions for AI agents
207
+
208
+ commands:
209
+ pair '<JSON>' pair with human's web app (one-time setup)
210
+ tx send a transaction (blocks until approved/rejected)
211
+ status check pairing status
212
+ whoami show agent identity
213
+
214
+ tx flags:
215
+ --chain <id> chain ID (required) — 1, 137, 8453, etc.
216
+ --to <address> recipient (required)
217
+ --value <wei> value in wei (default: 0)
218
+ --data <hex> calldata (default: 0x)
219
+ --reason <text> shown to human
220
+ --urgency <low|med|high> priority hint
221
+ --expected-outcome <text> what will happen
222
+ --timeout <ms> wait time (default: 3600000)
223
+
224
+ exit codes:
225
+ 0 success
226
+ 1 rejected by human
227
+ 2 timeout
228
+ 3 connection error
229
+ 4 not paired
230
+ 5 invalid arguments
231
+
232
+ examples:
233
+ clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
234
+ clanker-wallet tx --chain 1 --to 0xRecipient --value 1000000000000000000 --reason "swap ETH"
235
+ clanker-wallet status
236
+ clanker-wallet whoami
237
+
238
+ docs: https://github.com/almogdepaz/clanker-wallet
239
+ web: https://clanker-wallet.xyz
240
+ `)
241
+ }
242
+
243
+ // ── main ─────────────────────────────────────────────────────────────
244
+ const { cmd, positional, flags } = parseArgs(process.argv)
245
+
246
+ switch (cmd) {
247
+ case 'pair':
248
+ cmdPair(positional)
249
+ break
250
+ case 'tx':
251
+ await cmdTx(flags)
252
+ break
253
+ case 'status':
254
+ cmdStatus()
255
+ break
256
+ case 'whoami':
257
+ cmdWhoami()
258
+ break
259
+ case 'help':
260
+ case '--help':
261
+ case '-h':
262
+ showHelp()
263
+ break
264
+ default:
265
+ showHelp()
266
+ break
267
+ }
package/dist/client.d.ts CHANGED
@@ -13,11 +13,9 @@ export declare class ClankerWallet extends TypedEmitter {
13
13
  /** Ed25519 public key for signing TxRequests */
14
14
  readonly signingPublicKey: Uint8Array;
15
15
  private readonly signingSecretKey;
16
- private humanPubkey;
17
- private humanPubkeyHex;
18
- private relayUrl;
19
- private humanWallet;
20
- private mode;
16
+ private readonly agentPubkeyHex;
17
+ private readonly signingPubkeyHex;
18
+ private pairing;
21
19
  constructor(options?: ClankerWalletOptions);
22
20
  /** whether this agent has been paired with a human */
23
21
  get paired(): boolean;
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { encryptMessage, decryptMessage, toHex, fromHex, generateKeypair, keypairFromSecret, signingKeypairFromSeed, signMessage } from './crypto.js';
1
+ import { decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex } from './crypto.js';
2
2
  import { TypedEmitter } from './events.js';
3
3
  const DEFAULT_TIMEOUT_MS = 3_600_000; // 1 hour
4
4
  export class ClankerWallet extends TypedEmitter {
@@ -8,11 +8,9 @@ export class ClankerWallet extends TypedEmitter {
8
8
  /** Ed25519 public key for signing TxRequests */
9
9
  signingPublicKey;
10
10
  signingSecretKey;
11
- humanPubkey = null;
12
- humanPubkeyHex = null;
13
- relayUrl = null;
14
- humanWallet = null;
15
- mode = 'encrypted';
11
+ agentPubkeyHex;
12
+ signingPubkeyHex;
13
+ pairing = null;
16
14
  constructor(options = {}) {
17
15
  super();
18
16
  const kp = options.secretKey
@@ -21,14 +19,15 @@ export class ClankerWallet extends TypedEmitter {
21
19
  this.publicKey = kp.publicKey;
22
20
  this.secretKey = kp.secretKey;
23
21
  this.agentId = `agent_${toHex(kp.publicKey).slice(0, 12)}`;
24
- // derive Ed25519 signing keypair from the x25519 secret key (first 32 bytes = seed)
22
+ this.agentPubkeyHex = toHex(kp.publicKey);
25
23
  const signingKp = signingKeypairFromSeed(kp.secretKey);
26
24
  this.signingPublicKey = signingKp.publicKey;
27
25
  this.signingSecretKey = signingKp.secretKey;
26
+ this.signingPubkeyHex = toHex(signingKp.publicKey);
28
27
  }
29
28
  /** whether this agent has been paired with a human */
30
29
  get paired() {
31
- return this.humanPubkey !== null;
30
+ return this.pairing !== null;
32
31
  }
33
32
  /** parse QR/pairing data and store the human's pubkey + relay url */
34
33
  pair(qrData) {
@@ -39,11 +38,13 @@ export class ClankerWallet extends TypedEmitter {
39
38
  if (!data.pubkey || !data.relay) {
40
39
  throw new Error('invalid pairing data: missing pubkey or relay');
41
40
  }
42
- this.humanPubkey = fromHex(data.pubkey);
43
- this.humanPubkeyHex = data.pubkey;
44
- this.relayUrl = data.relay;
45
- this.humanWallet = data.wallet || null;
46
- this.mode = data.mode || 'encrypted';
41
+ this.pairing = {
42
+ humanPubkey: fromHex(data.pubkey),
43
+ humanPubkeyHex: data.pubkey,
44
+ relayUrl: data.relay,
45
+ humanWallet: data.wallet || null,
46
+ mode: data.mode || 'encrypted',
47
+ };
47
48
  this.emit('paired', { relay: data.relay, wallet: data.wallet || '' });
48
49
  return data;
49
50
  }
@@ -53,9 +54,10 @@ export class ClankerWallet extends TypedEmitter {
53
54
  * throws on rejection, timeout, or connection error.
54
55
  */
55
56
  async requestTx(options) {
56
- if (!this.humanPubkey || !this.humanPubkeyHex || !this.relayUrl) {
57
+ if (!this.pairing) {
57
58
  throw new Error('not paired — call pair() first');
58
59
  }
60
+ const { humanPubkey, humanPubkeyHex, relayUrl, humanWallet, mode } = this.pairing;
59
61
  const timeoutMs = options.timeout_ms ?? DEFAULT_TIMEOUT_MS;
60
62
  const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
61
63
  const txRequest = {
@@ -67,17 +69,7 @@ export class ClankerWallet extends TypedEmitter {
67
69
  context: options.context,
68
70
  signature: '', // set below after signing
69
71
  };
70
- // sign the request (signature field empty during signing, then filled)
71
- const sigPayload = JSON.stringify(txRequest);
72
- txRequest.signature = signMessage(sigPayload, this.signingSecretKey);
73
- const humanPubkey = this.humanPubkey;
74
- const humanPubkeyHex = this.humanPubkeyHex;
75
- const relayUrl = this.relayUrl;
76
- const secretKey = this.secretKey;
77
- const agentPubkeyHex = toHex(this.publicKey);
78
- const signingPubkeyHex = toHex(this.signingPublicKey);
79
- const mode = this.mode;
80
- const humanWallet = this.humanWallet;
72
+ txRequest.signature = signMessage(JSON.stringify(txRequest), this.signingSecretKey);
81
73
  return new Promise((resolve, reject) => {
82
74
  let ws;
83
75
  let timer;
@@ -89,11 +81,14 @@ export class ClankerWallet extends TypedEmitter {
89
81
  ws.close();
90
82
  }
91
83
  };
84
+ const fail = (message) => {
85
+ cleanup();
86
+ this.emit('error', { message });
87
+ reject(new Error(message));
88
+ };
92
89
  timer = setTimeout(() => {
93
90
  if (!settled) {
94
- cleanup();
95
- this.emit('error', { message: `request ${requestId} timed out after ${timeoutMs}ms` });
96
- reject(new Error(`request ${requestId} timed out after ${timeoutMs}ms`));
91
+ fail(`request ${requestId} timed out after ${timeoutMs}ms`);
97
92
  }
98
93
  }, timeoutMs);
99
94
  this.emit('connecting', { relay: relayUrl });
@@ -101,17 +96,12 @@ export class ClankerWallet extends TypedEmitter {
101
96
  ws = new WebSocket(relayUrl);
102
97
  }
103
98
  catch (err) {
104
- cleanup();
105
- this.emit('error', { message: `failed to connect to relay: ${err}` });
106
- reject(new Error(`failed to connect to relay: ${err}`));
99
+ fail(`failed to connect to relay: ${err}`);
107
100
  return;
108
101
  }
109
102
  ws.addEventListener('error', () => {
110
- if (!settled) {
111
- cleanup();
112
- this.emit('error', { message: 'relay connection error' });
113
- reject(new Error('relay connection error'));
114
- }
103
+ if (!settled)
104
+ fail('relay connection error');
115
105
  });
116
106
  ws.addEventListener('close', () => {
117
107
  this.emit('disconnected', {});
@@ -121,7 +111,6 @@ export class ClankerWallet extends TypedEmitter {
121
111
  }
122
112
  });
123
113
  ws.addEventListener('open', () => {
124
- // join the human's room
125
114
  ws.send(JSON.stringify({ type: 'join', room: humanPubkeyHex }));
126
115
  });
127
116
  ws.addEventListener('message', (event) => {
@@ -132,23 +121,22 @@ export class ClankerWallet extends TypedEmitter {
132
121
  msg = JSON.parse(event.data);
133
122
  }
134
123
  catch {
135
- return; // ignore non-JSON
124
+ return;
136
125
  }
137
126
  if (msg.type === 'joined') {
138
127
  this.emit('connected', { room: humanPubkeyHex });
139
- // room joined — send the encrypted tx request
140
- const plaintext = JSON.stringify(txRequest);
141
- const ciphertext = encryptMessage(plaintext, humanPubkey, secretKey);
128
+ const ciphertext = encryptMessage(JSON.stringify(txRequest), humanPubkey, this.secretKey);
142
129
  const envelope = {
143
- sender_pubkey: agentPubkeyHex,
130
+ sender_pubkey: this.agentPubkeyHex,
144
131
  ciphertext,
145
- signing_pubkey: signingPubkeyHex,
132
+ signing_pubkey: this.signingPubkeyHex,
146
133
  };
147
134
  const wsMsg = {
148
135
  type: 'message',
149
136
  room: humanPubkeyHex,
150
137
  payload: JSON.stringify(envelope),
151
138
  };
139
+ // managed mode: relay gets plaintext tx params so it can simulate
152
140
  if (mode === 'managed') {
153
141
  wsMsg.simulate = {
154
142
  from: humanWallet || '',
@@ -163,15 +151,17 @@ export class ClankerWallet extends TypedEmitter {
163
151
  return;
164
152
  }
165
153
  if (msg.type === 'message') {
166
- // try to decrypt as TxResponse
167
154
  try {
168
155
  const envelope = JSON.parse(msg.payload);
169
- const plaintext = decryptMessage(envelope.ciphertext, fromHex(envelope.sender_pubkey), secretKey);
156
+ // only the paired human may answer
157
+ if (envelope.sender_pubkey?.toLowerCase() !== humanPubkeyHex.toLowerCase())
158
+ return;
159
+ const plaintext = decryptMessage(envelope.ciphertext, humanPubkey, this.secretKey);
170
160
  if (!plaintext)
171
- return; // not for us or decryption failed
161
+ return;
172
162
  const response = JSON.parse(plaintext);
173
163
  if (response.request_id !== requestId)
174
- return; // not our response
164
+ return;
175
165
  this.emit('response', {
176
166
  request_id: response.request_id,
177
167
  status: response.status,
@@ -194,11 +184,10 @@ export class ClankerWallet extends TypedEmitter {
194
184
  catch {
195
185
  // ignore messages that aren't valid envelopes
196
186
  }
187
+ return;
197
188
  }
198
189
  if (msg.type === 'error') {
199
- cleanup();
200
- this.emit('error', { message: `relay error: ${msg.message}` });
201
- reject(new Error(`relay error: ${msg.message}`));
190
+ fail(`relay error: ${msg.message}`);
202
191
  }
203
192
  });
204
193
  });
package/dist/crypto.d.ts CHANGED
@@ -17,4 +17,7 @@ export declare function signingKeypairFromSeed(seed: Uint8Array): nacl.SignKeyPa
17
17
  export declare function signMessage(message: string, signingSecretKey: Uint8Array): string;
18
18
  /** verify a detached Ed25519 signature. signature is base64-encoded. */
19
19
  export declare function verifySignature(message: string, base64Signature: string, signingPublicKey: Uint8Array): boolean;
20
+ /** base64 string → Uint8Array */
21
+ export declare function base64ToBytes(b64: string): Uint8Array;
22
+ /** Uint8Array → base64 string */
20
23
  export declare function bytesToBase64(bytes: Uint8Array): string;
package/dist/crypto.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import nacl from 'tweetnacl';
2
+ const encoder = new TextEncoder();
3
+ const decoder = new TextDecoder();
2
4
  /** generate a random x25519 keypair for the agent's identity */
3
5
  export function generateKeypair() {
4
6
  return nacl.box.keyPair();
@@ -10,8 +12,7 @@ export function keypairFromSecret(secretKey) {
10
12
  /** encrypt a message using nacl.box. returns base64(nonce + ciphertext). */
11
13
  export function encryptMessage(message, recipientPublicKey, senderSecretKey) {
12
14
  const nonce = nacl.randomBytes(nacl.box.nonceLength);
13
- const encoded = new TextEncoder().encode(message);
14
- const ciphertext = nacl.box(encoded, nonce, recipientPublicKey, senderSecretKey);
15
+ const ciphertext = nacl.box(encoder.encode(message), nonce, recipientPublicKey, senderSecretKey);
15
16
  const combined = new Uint8Array(nonce.length + ciphertext.length);
16
17
  combined.set(nonce);
17
18
  combined.set(ciphertext, nonce.length);
@@ -19,7 +20,13 @@ export function encryptMessage(message, recipientPublicKey, senderSecretKey) {
19
20
  }
20
21
  /** decrypt a nacl.box message. payload = base64(nonce + ciphertext). */
21
22
  export function decryptMessage(base64Payload, senderPublicKey, recipientSecretKey) {
22
- const raw = base64ToBytes(base64Payload);
23
+ let raw;
24
+ try {
25
+ raw = base64ToBytes(base64Payload);
26
+ }
27
+ catch {
28
+ return null;
29
+ }
23
30
  if (raw.length <= nacl.box.nonceLength)
24
31
  return null;
25
32
  const nonce = raw.slice(0, nacl.box.nonceLength);
@@ -27,7 +34,7 @@ export function decryptMessage(base64Payload, senderPublicKey, recipientSecretKe
27
34
  const plaintext = nacl.box.open(ciphertext, nonce, senderPublicKey, recipientSecretKey);
28
35
  if (!plaintext)
29
36
  return null;
30
- return new TextDecoder().decode(plaintext);
37
+ return decoder.decode(plaintext);
31
38
  }
32
39
  /** hex-encode bytes (no 0x prefix) */
33
40
  export function toHex(bytes) {
@@ -37,7 +44,13 @@ export function toHex(bytes) {
37
44
  }
38
45
  /** hex string → Uint8Array (strips 0x prefix if present) */
39
46
  export function fromHex(hex) {
40
- const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
47
+ const clean = hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
48
+ if (clean.length % 2 !== 0) {
49
+ throw new Error('invalid hex string: length must be even');
50
+ }
51
+ if (!/^[0-9a-fA-F]*$/.test(clean)) {
52
+ throw new Error('invalid hex string: contains non-hex characters');
53
+ }
41
54
  const bytes = new Uint8Array(clean.length / 2);
42
55
  for (let i = 0; i < bytes.length; i++) {
43
56
  bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
@@ -46,21 +59,24 @@ export function fromHex(hex) {
46
59
  }
47
60
  /** derive an Ed25519 signing keypair from a 32-byte seed */
48
61
  export function signingKeypairFromSeed(seed) {
49
- return nacl.sign.keyPair.fromSeed(seed);
62
+ if (seed.length !== nacl.sign.seedLength) {
63
+ throw new Error(`invalid seed length: expected ${nacl.sign.seedLength} bytes, got ${seed.length}`);
64
+ }
65
+ // copy to keep derivation independent of caller-owned mutable buffers
66
+ return nacl.sign.keyPair.fromSeed(new Uint8Array(seed));
50
67
  }
51
68
  /** sign a message string with an Ed25519 secret key. returns base64(signature). */
52
69
  export function signMessage(message, signingSecretKey) {
53
- const encoded = new TextEncoder().encode(message);
54
- const signature = nacl.sign.detached(encoded, signingSecretKey);
70
+ const signature = nacl.sign.detached(encoder.encode(message), signingSecretKey);
55
71
  return bytesToBase64(signature);
56
72
  }
57
73
  /** verify a detached Ed25519 signature. signature is base64-encoded. */
58
74
  export function verifySignature(message, base64Signature, signingPublicKey) {
59
- const encoded = new TextEncoder().encode(message);
60
75
  const signature = base64ToBytes(base64Signature);
61
- return nacl.sign.detached.verify(encoded, signature, signingPublicKey);
76
+ return nacl.sign.detached.verify(encoder.encode(message), signature, signingPublicKey);
62
77
  }
63
- function base64ToBytes(b64) {
78
+ /** base64 string → Uint8Array */
79
+ export function base64ToBytes(b64) {
64
80
  const binary = atob(b64);
65
81
  const bytes = new Uint8Array(binary.length);
66
82
  for (let i = 0; i < binary.length; i++) {
@@ -68,6 +84,7 @@ function base64ToBytes(b64) {
68
84
  }
69
85
  return bytes;
70
86
  }
87
+ /** Uint8Array → base64 string */
71
88
  export function bytesToBase64(bytes) {
72
89
  let binary = '';
73
90
  for (let i = 0; i < bytes.length; i++) {
package/dist/events.js CHANGED
@@ -1,12 +1,13 @@
1
1
  export class TypedEmitter {
2
2
  _listeners = new Map();
3
3
  on(event, handler) {
4
- let set = this._listeners.get(event);
5
- if (!set) {
6
- set = new Set();
7
- this._listeners.set(event, set);
4
+ const existing = this._listeners.get(event);
5
+ if (existing) {
6
+ existing.add(handler);
7
+ }
8
+ else {
9
+ this._listeners.set(event, new Set([handler]));
8
10
  }
9
- set.add(handler);
10
11
  return this;
11
12
  }
12
13
  off(event, handler) {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { ClankerWallet } from './client.js';
2
2
  export type { ClankerWalletOptions } from './client.js';
3
+ export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
3
4
  export { TypedEmitter } from './events.js';
4
5
  export type { EventMap } from './events.js';
5
- export { generateKeypair, keypairFromSecret, encryptMessage, decryptMessage, toHex, fromHex, signingKeypairFromSeed, signMessage, verifySignature, bytesToBase64 } from './crypto.js';
6
- export type { Transaction, TxContext, TxRequest, TxResponse, AgentEnvelope, PairingData, RequestTxOptions, } from './types.js';
6
+ export type { AgentEnvelope, PairingData, RequestTxOptions, Transaction, TxContext, TxRequest, TxResponse, } from './types.js';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { ClankerWallet } from './client.js';
2
+ export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
2
3
  export { TypedEmitter } from './events.js';
3
- export { generateKeypair, keypairFromSecret, encryptMessage, decryptMessage, toHex, fromHex, signingKeypairFromSeed, signMessage, verifySignature, bytesToBase64 } from './crypto.js';
package/dist/types.d.ts CHANGED
@@ -40,7 +40,7 @@ export interface PairingData {
40
40
  pubkey: string;
41
41
  relay: string;
42
42
  wallet: string;
43
- /** 'managed' = relay simulates server-side; 'encrypted' = user provides own keys */
43
+ /** "encrypted" (default): relay sees only ciphertext. "managed": relay also receives tx params for simulation. */
44
44
  mode?: 'encrypted' | 'managed';
45
45
  }
46
46
  /** options for requestTx */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clanker-wallet",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,16 @@
10
10
  "types": "./dist/index.d.ts"
11
11
  }
12
12
  },
13
- "files": ["dist", "README.md", "LICENSE"],
13
+ "bin": {
14
+ "clanker-wallet": "bin/cli.js"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "bin",
19
+ "SKILL.md",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
14
23
  "license": "MIT",
15
24
  "author": "Clanker Wallet Contributors",
16
25
  "repository": {
@@ -18,7 +27,27 @@
18
27
  "url": "git+https://github.com/almogdepaz/clanker-wallet.git"
19
28
  },
20
29
  "homepage": "https://github.com/almogdepaz/clanker-wallet#readme",
21
- "keywords": ["wallet", "ai-agent", "ethereum", "e2e-encrypted", "transaction-approval"],
30
+ "keywords": [
31
+ "wallet",
32
+ "ai-agent",
33
+ "ethereum",
34
+ "e2e-encrypted",
35
+ "transaction-approval",
36
+ "blockchain",
37
+ "web3",
38
+ "langchain",
39
+ "human-in-the-loop",
40
+ "evm",
41
+ "agent-wallet",
42
+ "llm",
43
+ "typescript",
44
+ "base",
45
+ "polygon",
46
+ "transaction",
47
+ "approval",
48
+ "defi",
49
+ "vercel-ai-sdk"
50
+ ],
22
51
  "scripts": {
23
52
  "build": "tsc -p tsconfig.build.json",
24
53
  "dev": "tsc --watch",
@@ -31,9 +60,7 @@
31
60
  },
32
61
  "devDependencies": {
33
62
  "@types/bun": "^1.3.8",
34
- "@types/ws": "^8.18.1",
35
63
  "typescript": "^5.4.0",
36
- "vitest": "^2.1.0",
37
- "ws": "^8.19.0"
64
+ "vitest": "^2.1.0"
38
65
  }
39
66
  }