clawmoney 0.16.0 → 0.17.1

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.
@@ -3,8 +3,9 @@ import chalk from "chalk";
3
3
  import ora from "ora";
4
4
  import { requireConfig } from "../utils/config.js";
5
5
  import { apiGet, apiPost } from "../utils/api.js";
6
- import { awalExec } from "../utils/awal.js";
7
6
  import { uploadFile } from "../hub/media.js";
7
+ import { CdpProvider } from "../wallet/cdp-provider.js";
8
+ import { x402Fetch } from "../wallet/x402-client.js";
8
9
  export async function gigCreateCommand(options) {
9
10
  const config = requireConfig();
10
11
  const budget = parseFloat(options.budget);
@@ -43,17 +44,19 @@ export async function gigCreateCommand(options) {
43
44
  // Auto-fund: check wallet balance and pay
44
45
  spinner.start("Checking wallet balance...");
45
46
  try {
46
- const balanceResult = await awalExec(["balance"]);
47
- const baseBalances = balanceResult.data.base;
48
- const usdcBalance = baseBalances?.balances
49
- ? baseBalances.balances.USDC?.formatted
50
- : undefined;
51
- const balance = parseFloat(String(usdcBalance || "0"));
47
+ const wallet = new CdpProvider(config.api_key);
48
+ const bal = await wallet.getBalance("usdc");
49
+ const atomic = BigInt(bal.amount);
50
+ const divisor = 10n ** BigInt(bal.decimals || 6);
51
+ const balance = Number(atomic / divisor) + Number(atomic % divisor) / Number(divisor);
52
52
  if (balance >= budget) {
53
53
  // Enough USDC — pay via x402
54
54
  spinner.text = `Funding $${budget.toFixed(2)} USDC via x402...`;
55
55
  try {
56
- await awalExec(["x402", "pay", `https://pay.clawmoney.ai/market/escrow/${task.id}?price=${budget}`]);
56
+ const res = await x402Fetch(wallet, `https://pay.clawmoney.ai/market/escrow/${task.id}?price=${budget}`, { method: "POST" });
57
+ if (!res.ok) {
58
+ throw new Error(`Payment endpoint returned ${res.status}`);
59
+ }
57
60
  spinner.succeed(chalk.green(`Funded! $${budget.toFixed(2)} USDC`));
58
61
  console.log(chalk.dim(" Task is now live and accepting submissions."));
59
62
  }
@@ -82,7 +85,7 @@ export async function gigCreateCommand(options) {
82
85
  }
83
86
  else {
84
87
  spinner.warn(chalk.yellow("Stripe checkout not available."));
85
- console.log(chalk.dim(` Fund via x402: npx awal x402 pay "https://pay.clawmoney.ai/market/escrow/${task.id}?price=${budget}"`));
88
+ console.log(chalk.dim(` Fund via x402 in another client, or use Stripe checkout above.`));
86
89
  }
87
90
  }
88
91
  }
@@ -5,8 +5,9 @@ import chalk from "chalk";
5
5
  import ora from "ora";
6
6
  import { requireConfig } from "../utils/config.js";
7
7
  import { apiGet, apiPost } from "../utils/api.js";
8
- import { awalExec } from "../utils/awal.js";
9
8
  import { readPid, isPidAlive, removePid } from "../hub/provider.js";
9
+ import { CdpProvider } from "../wallet/cdp-provider.js";
10
+ import { x402Fetch } from "../wallet/x402-client.js";
10
11
  const LOG_FILE = join(homedir(), ".clawmoney", "provider.log");
11
12
  // ── hub start ──
12
13
  export async function hubStartCommand(options) {
@@ -219,7 +220,11 @@ export async function hubCallCommand(options) {
219
220
  if (options.pay) {
220
221
  spinner.text = `Funding task $${budget} USDC via x402...`;
221
222
  try {
222
- await awalExec(["x402", "pay", `https://pay.clawmoney.ai/market/escrow/${taskId}?price=${budget}`]);
223
+ const wallet = new CdpProvider(config.api_key);
224
+ const res = await x402Fetch(wallet, `https://pay.clawmoney.ai/market/escrow/${taskId}?price=${budget}`, { method: "POST" });
225
+ if (!res.ok) {
226
+ throw new Error(`Payment endpoint returned ${res.status}`);
227
+ }
223
228
  }
224
229
  catch (err) {
225
230
  spinner.fail(chalk.red(`Funding failed: ${err.message}`));
@@ -239,25 +244,28 @@ export async function hubCallCommand(options) {
239
244
  if (options.pay) {
240
245
  // x402 payment flow via pay.clawmoney.ai Worker
241
246
  const skillPrice = skillInfo?.price ?? 0.01;
242
- // Step 2: Pay via awal x402 → pay.clawmoney.ai Worker
247
+ // Step 2: Pay via x402 → pay.clawmoney.ai Worker
243
248
  spinner.text = `Paying $${skillPrice} USDC for ${options.agent}/${options.skill}...`;
244
249
  const payUrl = `https://pay.clawmoney.ai/market/${encodeURIComponent(options.agent)}/${encodeURIComponent(options.skill)}?price=${skillPrice}`;
245
- let payResult;
250
+ let payData;
246
251
  try {
247
- payResult = await awalExec(["x402", "pay", payUrl]);
252
+ const wallet = new CdpProvider(config.api_key);
253
+ const res = await x402Fetch(wallet, payUrl, { method: "POST" });
254
+ if (!res.ok) {
255
+ throw new Error(`Payment endpoint returned ${res.status}`);
256
+ }
257
+ payData = (await res.json());
248
258
  }
249
259
  catch (err) {
250
260
  spinner.fail(chalk.red(`Payment failed: ${err.message}`));
251
261
  process.exit(1);
252
262
  }
253
263
  // Extract payment_token from Worker response
254
- // awal returns {status, statusText, data: {payment_token, ...}, headers}
255
- const payData = payResult.data;
256
264
  const innerData = payData.data ?? payData;
257
265
  const paymentToken = innerData.payment_token ?? payData.payment_token;
258
266
  if (!paymentToken) {
259
267
  spinner.fail(chalk.red("Payment succeeded but no payment_token returned"));
260
- console.error(chalk.dim(` Raw response: ${JSON.stringify(payResult.data).slice(0, 200)}`));
268
+ console.error(chalk.dim(` Raw response: ${JSON.stringify(payData).slice(0, 200)}`));
261
269
  process.exit(1);
262
270
  }
263
271
  // Step 3: Invoke with payment_token
@@ -1,8 +1,9 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { apiGet, apiPost } from '../utils/api.js';
4
- import { awalExec } from '../utils/awal.js';
5
4
  import { requireConfig } from '../utils/config.js';
5
+ import { CdpProvider } from '../wallet/cdp-provider.js';
6
+ import { x402PayJson } from '../wallet/x402-client.js';
6
7
  export async function promoteSubmitCommand(taskId, options) {
7
8
  const config = requireConfig();
8
9
  // 自动检测平台(从 task 获取)
@@ -82,9 +83,8 @@ export async function promoteVerifyCommand(submissionId, options) {
82
83
  const witnessSpinner = ora('Fetching witness proof via x402 ($0.01)...').start();
83
84
  let witnessData;
84
85
  try {
85
- witnessData = await awalExec([
86
- 'x402', 'pay', `https://witness.bnbot.ai/x/${tweetId}`,
87
- ]);
86
+ const wallet = new CdpProvider(config.api_key);
87
+ witnessData = await x402PayJson(wallet, `https://witness.bnbot.ai/x/${tweetId}`);
88
88
  witnessSpinner.succeed('Witness proof obtained');
89
89
  }
90
90
  catch (err) {
@@ -92,8 +92,9 @@ export async function promoteVerifyCommand(submissionId, options) {
92
92
  console.error(chalk.red(err.message));
93
93
  return;
94
94
  }
95
- // Parse witness response — awalExec wraps: { success, data: { status, data: { code, data: {...}, proof: {...} } } }
96
- const proof = witnessData?.data?.data?.proof || witnessData?.data?.proof || witnessData?.proof;
95
+ // Parse witness response — x402PayJson returns the response body directly.
96
+ const wdInner = witnessData?.data;
97
+ const proof = (wdInner?.proof ?? witnessData?.proof);
97
98
  if (!proof) {
98
99
  console.error(chalk.red(' No proof in witness response'));
99
100
  console.log(chalk.dim(` Raw: ${JSON.stringify(witnessData).slice(0, 200)}`));
@@ -1,252 +1,211 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { awalExec } from '../utils/awal.js';
4
3
  import { apiGet, apiPost } from '../utils/api.js';
5
4
  import { loadConfig, saveConfig, getConfigPath } from '../utils/config.js';
6
5
  import { prompt } from '../utils/prompt.js';
7
- import { execSync } from 'node:child_process';
6
+ const CLAIM_POLL_INTERVAL_MS = 4_000;
7
+ const CLAIM_POLL_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
8
+ async function pollForClaim(apiKey, onTick) {
9
+ const start = Date.now();
10
+ while (Date.now() - start < CLAIM_POLL_TIMEOUT_MS) {
11
+ try {
12
+ const resp = await apiGet('/api/v1/claw-agents/me', apiKey);
13
+ // Claim completion is detected via wallet_address being populated.
14
+ // Agent.status stays "unclaimed" by design until X account verification
15
+ // (see crud.claim_claw_agent), so we can't gate on status here — the
16
+ // wallet address is only set by complete_claim_with_key, so its
17
+ // presence is a reliable claim-completed signal.
18
+ if (resp.ok && resp.data && resp.data.wallet_address) {
19
+ return resp.data;
20
+ }
21
+ }
22
+ catch {
23
+ // Network blip — keep polling.
24
+ }
25
+ onTick?.();
26
+ await new Promise((r) => setTimeout(r, CLAIM_POLL_INTERVAL_MS));
27
+ }
28
+ return null;
29
+ }
8
30
  export async function setupCommand() {
9
31
  console.log(chalk.bold('\n ClawMoney Setup\n'));
10
- // Non-interactive mode: if no TTY (e.g. AI agent running the command),
11
- // install the skill and instruct the AI to proceed with onboarding.
32
+ // Non-interactive mode: skill-install path.
12
33
  if (!process.stdin.isTTY) {
13
34
  console.log('ClawMoney skill installed successfully.');
14
35
  console.log('');
15
36
  console.log('NEXT STEP: Run /clawmoney now to complete onboarding.');
16
- console.log('The skill will walk you through wallet setup, agent registration, and earning.');
37
+ console.log('The skill will walk you through agent registration and earning.');
17
38
  console.log('');
18
39
  return;
19
40
  }
20
- // Step 1: Check and install dependencies
21
- const depSpinner = ora('Checking dependencies...').start();
22
- try {
23
- // Check if awal is available
24
- try {
25
- execSync('npx awal --version', { stdio: 'pipe' });
26
- depSpinner.succeed('awal is available');
27
- }
28
- catch {
29
- depSpinner.text = 'Installing awal...';
30
- try {
31
- execSync('npm install -g awal', { stdio: 'pipe' });
32
- depSpinner.succeed('awal installed');
33
- }
34
- catch {
35
- depSpinner.warn('Could not install awal globally. Will use npx.');
36
- }
37
- }
38
- }
39
- catch (err) {
40
- depSpinner.fail('Failed to check dependencies');
41
- console.error(chalk.red(err.message));
41
+ // Step 1: Ask for email.
42
+ const email = await prompt(chalk.cyan('? ') + 'Enter your email: ');
43
+ if (!email || !email.includes('@')) {
44
+ console.log(chalk.red('Invalid email address.'));
42
45
  return;
43
46
  }
44
- // Step 2: Check wallet status
45
- //
46
- // We try to get the wallet address directly — if awal returns one, the
47
- // wallet is signed in, end of story. This avoids the awal `status` command
48
- // returning an unrecognized shape (observed field-name drift: some versions
49
- // return `authenticated`, others `signedIn`/`account`/nested objects)
50
- // which would otherwise force us into the login flow even when the user
51
- // is already signed in, and awal would then refuse with "already signed in".
52
- const walletSpinner = ora('Checking wallet status...').start();
53
- let walletAddress = '';
54
- let needsLogin = false;
47
+ // Step 2: Check if this email already has an agent.
48
+ const agentSpinner = ora('Checking agent status...').start();
49
+ let existingStatus = '';
55
50
  try {
56
- const addrResult = await awalExec(['address']);
57
- const addrData = addrResult.data;
58
- const addr = addrData.address || '';
59
- if (addr) {
60
- walletAddress = addr;
61
- walletSpinner.succeed(`Wallet connected: ${walletAddress}`);
62
- }
63
- else {
64
- // Fall back to legacy `status` shape in case some awal version only
65
- // exposes authentication through that command.
66
- const status = await awalExec(['status']);
67
- const statusData = status.data;
68
- if (statusData.authenticated || statusData.loggedIn || statusData.address) {
69
- walletAddress = statusData.address || '';
70
- walletSpinner.succeed(`Wallet connected${walletAddress ? `: ${walletAddress}` : ''}`);
71
- }
72
- else {
73
- needsLogin = true;
74
- walletSpinner.info('Wallet not authenticated');
75
- }
51
+ const checkResp = await apiGet(`/api/v1/claw-agents/check-email?email=${encodeURIComponent(email)}`);
52
+ if (checkResp.ok) {
53
+ const info = checkResp.data.agent ?? {};
54
+ existingStatus = (info.status ?? checkResp.data.status ?? '').toUpperCase();
76
55
  }
77
56
  }
78
57
  catch {
79
- needsLogin = true;
80
- walletSpinner.info('Wallet not authenticated');
81
- }
82
- // Step 3: Ask for email
83
- const email = await prompt(chalk.cyan('? ') + 'Enter your email: ');
84
- if (!email || !email.includes('@')) {
85
- console.log(chalk.red('Invalid email address.'));
86
- return;
58
+ // Fall through — if the check fails, we still try registering.
87
59
  }
88
- // Step 4: Login wallet if needed
89
- if (needsLogin) {
90
- const loginSpinner = ora('Logging in to wallet...').start();
60
+ // Existing ACTIVE agent → login path: send OTP, verify, rotate key.
61
+ // This is the "I already registered on another machine / lost my api_key"
62
+ // flow no new claim link needed.
63
+ if (existingStatus === 'ACTIVE') {
64
+ agentSpinner.info('Existing agent found. Sending login code to your email...');
91
65
  try {
92
- const loginResult = await awalExec(['auth', 'login', email]);
93
- const loginData = loginResult.data;
94
- const flowId = loginData.flowId || loginData.flow_id;
95
- if (!flowId) {
96
- loginSpinner.fail('Login failed: no flow ID returned');
97
- console.log(chalk.dim('Response:'), loginResult.raw);
98
- return;
99
- }
100
- loginSpinner.info('OTP sent to your email');
101
- const otp = await prompt(chalk.cyan('? ') + 'Enter OTP from email: ');
102
- if (!otp) {
103
- console.log(chalk.red('OTP is required.'));
66
+ const loginResp = await apiPost('/api/v1/claw-agents/login', { email });
67
+ if (!loginResp.ok) {
68
+ console.error(chalk.red(JSON.stringify(loginResp.data)));
104
69
  return;
105
70
  }
106
- const verifySpinner = ora('Verifying OTP...').start();
107
- try {
108
- const verifyResult = await awalExec(['auth', 'verify', String(flowId), otp]);
109
- verifySpinner.succeed('Wallet authenticated');
110
- // Get wallet address
111
- const addrResult = await awalExec(['address']);
112
- const addrData = addrResult.data;
113
- walletAddress = addrData.address || '';
114
- if (walletAddress) {
115
- console.log(chalk.dim(` Wallet: ${walletAddress}`));
116
- }
117
- }
118
- catch (err) {
119
- verifySpinner.fail('OTP verification failed');
120
- console.error(chalk.red(err.message));
71
+ }
72
+ catch (err) {
73
+ console.error(chalk.red(err.message));
74
+ return;
75
+ }
76
+ const otp = await prompt(chalk.cyan('? ') + 'Enter the 6-digit code from your email: ');
77
+ if (!otp || !/^\d{4,8}$/.test(otp.trim())) {
78
+ console.log(chalk.red('Invalid code.'));
79
+ return;
80
+ }
81
+ const verifySpinner = ora('Verifying code...').start();
82
+ let loginData;
83
+ try {
84
+ const verifyResp = await apiPost('/api/v1/claw-agents/login/verify', { email, otp: otp.trim() });
85
+ if (!verifyResp.ok) {
86
+ verifySpinner.fail('Verification failed');
87
+ console.error(chalk.red(JSON.stringify(verifyResp.data)));
121
88
  return;
122
89
  }
90
+ loginData = verifyResp.data;
123
91
  }
124
92
  catch (err) {
125
- // If awal reports "already signed in" we're actually in the happy path
126
- // — the wallet is authenticated, the address check at the top just
127
- // failed to detect it. Try once more to fetch the address directly.
128
- const msg = err.message || '';
129
- if (/already\s*signed\s*in/i.test(msg)) {
130
- loginSpinner.info('Wallet already signed in');
131
- try {
132
- const addrResult = await awalExec(['address']);
133
- const addrData = addrResult.data;
134
- walletAddress = addrData.address || '';
135
- if (walletAddress) {
136
- console.log(chalk.dim(` Wallet: ${walletAddress}`));
137
- }
93
+ verifySpinner.fail('Verification failed');
94
+ console.error(chalk.red(err.message));
95
+ return;
96
+ }
97
+ verifySpinner.succeed('Logged in');
98
+ // Persist api_key immediately so nothing is lost if wallet provision hiccups.
99
+ saveConfig({
100
+ api_key: loginData.api_key,
101
+ agent_id: loginData.agent_id,
102
+ agent_slug: loginData.agent_slug,
103
+ email,
104
+ wallet_address: loginData.agent?.wallet_address ?? undefined,
105
+ });
106
+ // Ensure a CDP wallet exists for this agent. Older ACTIVE agents
107
+ // predating the CDP flow have wallet_address=null; hitting the
108
+ // balance endpoint triggers _ensure_agent_wallet on the backend.
109
+ let walletAddress = loginData.agent?.wallet_address ?? '';
110
+ if (!walletAddress) {
111
+ const walletSpinner = ora('Provisioning CDP wallet...').start();
112
+ try {
113
+ const balResp = await apiGet('/api/v1/claw-agents/me/wallet/balance?asset=usdc', loginData.api_key);
114
+ if (balResp.ok && balResp.data?.address) {
115
+ walletAddress = balResp.data.address;
116
+ saveConfig({ wallet_address: walletAddress });
117
+ walletSpinner.succeed(`Wallet ready: ${walletAddress}`);
138
118
  }
139
- catch {
140
- // Address fetch still failed continue anyway; the agent
141
- // register/login flow below doesn't strictly require a wallet.
119
+ else {
120
+ walletSpinner.warn('Wallet not yet availablewill be created on first use.');
142
121
  }
143
122
  }
144
- else {
145
- loginSpinner.fail('Wallet login failed');
146
- console.error(chalk.red(msg));
147
- return;
123
+ catch (err) {
124
+ walletSpinner.warn(`Wallet provisioning deferred: ${err.message}`);
148
125
  }
149
126
  }
150
- }
151
- // If we still don't have address, try fetching it
152
- if (!walletAddress) {
153
- try {
154
- const addrResult = await awalExec(['address']);
155
- const addrData = addrResult.data;
156
- walletAddress = addrData.address || '';
157
- }
158
- catch {
159
- // continue without address
127
+ // Summary.
128
+ console.log('');
129
+ console.log(chalk.green.bold(' Setup complete!'));
130
+ console.log('');
131
+ console.log(chalk.dim(` Agent ID: ${loginData.agent_id}`));
132
+ console.log(chalk.dim(` Agent Slug: ${loginData.agent_slug}`));
133
+ if (walletAddress) {
134
+ console.log(chalk.dim(` Wallet: ${walletAddress}`));
135
+ console.log(chalk.dim(` Custody: Coinbase CDP Server Wallet`));
160
136
  }
137
+ console.log(chalk.dim(` Config: ${getConfigPath()}`));
138
+ console.log('');
139
+ console.log(` Next steps:`);
140
+ console.log(` ${chalk.cyan('clawmoney wallet balance')} Check your wallet balance`);
141
+ console.log(` ${chalk.cyan('clawmoney browse')} Browse available tasks`);
142
+ console.log('');
143
+ return;
161
144
  }
162
- // Step 5: Check agent status
163
- const agentSpinner = ora('Checking agent status...').start();
145
+ // Step 3: Register agent (or re-send claim link for an UNCLAIMED agent).
146
+ // The backend generates the anonymous slug from email hash; we never send a name.
147
+ agentSpinner.text = 'Registering agent...';
148
+ let regData;
164
149
  try {
165
- const checkResp = await apiGet(`/api/v1/claw-agents/check-email?email=${encodeURIComponent(email)}`);
166
- if (!checkResp.ok) {
167
- agentSpinner.fail(`API error: ${checkResp.status}`);
168
- return;
169
- }
170
- const checkData = checkResp.data;
171
- // Backend returns agent details nested under `agent` now; fall back
172
- // to legacy top-level fields so an older backend still works.
173
- // Status is normalized to uppercase so case differences between
174
- // backend builds (active vs ACTIVE) don't trip the branch select.
175
- const agentInfo = checkData.agent ?? {};
176
- const agentStatus = (agentInfo.status ?? checkData.status ?? '').toUpperCase();
177
- const agentSlug = agentInfo.slug ?? checkData.slug;
178
- const agentIdFromCheck = agentInfo.id ?? checkData.agent_id;
179
- if (!checkData.exists || agentStatus === 'UNCLAIMED') {
180
- // Step 6: Register new agent
181
- agentSpinner.text = 'Registering agent...';
182
- // Backend generates the anonymous provider slug from email hash
183
- // — we deliberately do NOT send a name here so users can't pick
184
- // something that leaks PII.
185
- const registerBody = { email };
186
- if (walletAddress) {
187
- registerBody.wallet_address = walletAddress;
188
- }
189
- const regResp = await apiPost('/api/v1/claw-agents/register', registerBody);
190
- if (!regResp.ok) {
191
- agentSpinner.fail('Registration failed');
192
- console.error(chalk.red(JSON.stringify(regResp.data)));
193
- return;
194
- }
195
- const regData = regResp.data;
196
- agentSpinner.succeed(`Agent registered: ${regData.slug}`);
197
- saveConfig({
198
- api_key: regData.api_key,
199
- agent_id: regData.agent_id,
200
- agent_slug: regData.slug,
201
- email,
202
- wallet_address: walletAddress || undefined,
203
- });
204
- }
205
- else if (agentStatus === 'ACTIVE') {
206
- // Step 7: Login existing agent via OTP
207
- agentSpinner.info(`Agent found: ${agentSlug || agentIdFromCheck}`);
208
- const loginSpinner2 = ora('Sending login OTP...').start();
209
- const loginResp = await apiPost('/api/v1/claw-agents/login', { email });
210
- if (!loginResp.ok) {
211
- loginSpinner2.fail('Agent login failed');
212
- console.error(chalk.red(JSON.stringify(loginResp.data)));
213
- return;
214
- }
215
- loginSpinner2.info('OTP sent to your email');
216
- const agentOtp = await prompt(chalk.cyan('? ') + 'Enter agent login OTP: ');
217
- const verifySpinner2 = ora('Verifying...').start();
218
- const verifyResp = await apiPost('/api/v1/claw-agents/login/verify', {
219
- email,
220
- otp: agentOtp,
221
- flow_id: loginResp.data.flow_id,
222
- });
223
- if (!verifyResp.ok) {
224
- verifySpinner2.fail('Agent login verification failed');
225
- console.error(chalk.red(JSON.stringify(verifyResp.data)));
226
- return;
227
- }
228
- const loginData = verifyResp.data;
229
- verifySpinner2.succeed('Agent authenticated');
230
- saveConfig({
231
- api_key: loginData.api_key,
232
- agent_id: loginData.agent_id,
233
- agent_slug: loginData.slug,
234
- email,
235
- wallet_address: walletAddress || undefined,
236
- });
237
- }
238
- else {
239
- agentSpinner.warn(`Agent status: ${agentStatus || '(unknown)'}`);
240
- console.log(chalk.yellow('Please contact support if you need help.'));
150
+ const regResp = await apiPost('/api/v1/claw-agents/register', { email });
151
+ if (!regResp.ok) {
152
+ agentSpinner.fail('Registration failed');
153
+ console.error(chalk.red(JSON.stringify(regResp.data)));
241
154
  return;
242
155
  }
156
+ regData = regResp.data;
243
157
  }
244
158
  catch (err) {
245
- agentSpinner.fail('Failed to check agent status');
159
+ agentSpinner.fail('Registration failed');
246
160
  console.error(chalk.red(err.message));
247
161
  return;
248
162
  }
249
- // Step 8: Print summary
163
+ agentSpinner.succeed(`Agent registered: ${regData.agent.slug}`);
164
+ // Persist the api_key and agent_id immediately — the key only activates
165
+ // after the claim link is clicked, but we save it now so nothing is lost
166
+ // if the user ctrl-C's during the claim step.
167
+ saveConfig({
168
+ api_key: regData.api_key,
169
+ agent_id: regData.agent.id,
170
+ agent_slug: regData.agent.slug,
171
+ email,
172
+ });
173
+ // Step 4: Instruct the user to click the claim link in their email.
174
+ console.log('');
175
+ console.log(chalk.bold(' Check your email'));
176
+ console.log('');
177
+ console.log(chalk.dim(' We sent a claim link to'), chalk.cyan(email));
178
+ console.log(chalk.dim(' Click the link to complete setup. Your CDP wallet will be'));
179
+ console.log(chalk.dim(' provisioned automatically and this CLI will unlock.'));
180
+ console.log('');
181
+ // Step 5: Poll for claim completion.
182
+ const pollSpinner = ora('Waiting for claim link to be clicked...').start();
183
+ let tickCount = 0;
184
+ const completed = await pollForClaim(regData.api_key, () => {
185
+ tickCount++;
186
+ if (tickCount % 5 === 0) {
187
+ pollSpinner.text = `Waiting for claim link... (${Math.floor((tickCount * CLAIM_POLL_INTERVAL_MS) / 1000)}s)`;
188
+ }
189
+ });
190
+ if (!completed) {
191
+ pollSpinner.warn('Claim not completed within 15 minutes.');
192
+ console.log('');
193
+ console.log(chalk.yellow(' You can re-run'), chalk.cyan('clawmoney setup'), chalk.yellow('later to resume.'));
194
+ console.log(chalk.dim(' Your API key is already saved and will activate once you click the claim link.'));
195
+ console.log('');
196
+ return;
197
+ }
198
+ const walletAddress = completed.wallet_address ?? '';
199
+ pollSpinner.succeed('Agent claimed');
200
+ // Step 6: Update config with the now-known wallet address.
201
+ saveConfig({
202
+ api_key: regData.api_key,
203
+ agent_id: completed.id,
204
+ agent_slug: completed.slug,
205
+ email,
206
+ wallet_address: walletAddress || undefined,
207
+ });
208
+ // Step 7: Summary.
250
209
  const config = loadConfig();
251
210
  console.log('');
252
211
  console.log(chalk.green.bold(' Setup complete!'));
@@ -256,6 +215,7 @@ export async function setupCommand() {
256
215
  console.log(chalk.dim(` Agent Slug: ${config.agent_slug}`));
257
216
  if (walletAddress) {
258
217
  console.log(chalk.dim(` Wallet: ${walletAddress}`));
218
+ console.log(chalk.dim(` Custody: Coinbase CDP Server Wallet`));
259
219
  }
260
220
  console.log(chalk.dim(` Config: ${getConfigPath()}`));
261
221
  }
@@ -1,12 +1,10 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { awalExec, awalExecSafe } from '../utils/awal.js';
4
3
  import { apiGet } from '../utils/api.js';
5
- import { loadConfig, saveConfig } from '../utils/config.js';
6
- // Base mainnet USDC contract + balanceOf(address) ABI selector.
7
- // Keeping on-chain reads as a first-class path lets `wallet balance`
8
- // skip the awal Electron bridge entirely, which is notorious for
9
- // cold-starting slowly or hanging if the daemon isn't warm.
4
+ import { loadConfig, requireConfig, saveConfig } from '../utils/config.js';
5
+ import { CdpProvider } from '../wallet/cdp-provider.js';
6
+ // On-chain balance is read directly over public RPC to avoid a hot-path
7
+ // round-trip through the backend for a plain USDC balance query.
10
8
  const BASE_RPC_URL = 'https://mainnet.base.org';
11
9
  const BASE_USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
12
10
  const BALANCE_OF_SELECTOR = '0x70a08231';
@@ -43,14 +41,14 @@ async function readBaseUsdcBalance(walletAddress, timeoutMs = 8000) {
43
41
  export async function walletStatusCommand() {
44
42
  const spinner = ora('Getting wallet status...').start();
45
43
  try {
46
- // Read-only, safe to auto-retry after killing a wedged awal.
47
- const result = await awalExecSafe(['status'], { timeoutMs: 8_000 });
44
+ const config = requireConfig();
45
+ const wallet = new CdpProvider(config.api_key);
46
+ const address = await wallet.getAddress();
48
47
  spinner.succeed('Wallet Status');
49
48
  console.log('');
50
- const data = result.data;
51
- for (const [key, value] of Object.entries(data)) {
52
- console.log(` ${chalk.dim(key + ':')} ${value}`);
53
- }
49
+ console.log(` ${chalk.dim('Address:')} ${chalk.cyan(address)}`);
50
+ console.log(` ${chalk.dim('Custody:')} Coinbase CDP Server Wallet`);
51
+ console.log(` ${chalk.dim('Network:')} Base`);
54
52
  console.log('');
55
53
  }
56
54
  catch (err) {
@@ -58,53 +56,19 @@ export async function walletStatusCommand() {
58
56
  console.error(chalk.red(err.message));
59
57
  }
60
58
  }
61
- // Wrap a promise in a hard timeout so a hung awal process can't
62
- // swallow the whole command. On timeout we surface a specific
63
- // error string the caller can tell apart from generic spawn errors.
64
- function withTimeout(p, ms, label) {
65
- return new Promise((resolve, reject) => {
66
- const timer = setTimeout(() => {
67
- reject(new Error(`${label} timed out after ${ms}ms`));
68
- }, ms);
69
- p.then((v) => {
70
- clearTimeout(timer);
71
- resolve(v);
72
- }, (e) => {
73
- clearTimeout(timer);
74
- reject(e);
75
- });
76
- });
77
- }
78
59
  export async function walletBalanceCommand() {
79
60
  const spinner = ora('Getting wallet balance...').start();
80
- // Source of truth for on-chain balance: direct JSON-RPC to Base
81
- // mainnet USDC, not `awal balance`. Reasons:
82
- // - awal is an Electron app; cold-starting it via `npx` takes 3-10s
83
- // and occasionally wedges under load (see GH issues around
84
- // DEP0190 / pipe buffers).
85
- // - We already store the wallet address in ~/.clawmoney/config.yaml
86
- // at setup time, so we don't need awal to look it up.
87
- // - RPC reads are idempotent, cacheable, and cost nothing.
88
- // Relay earnings are fetched in parallel from the clawmoney backend.
89
- // Either half is allowed to fail — we print "(unavailable)" for the
90
- // broken section and keep going.
61
+ // Source of truth for on-chain balance: direct JSON-RPC to Base mainnet
62
+ // USDC. The wallet address comes from config.yaml (set at setup time) or
63
+ // the backend /me endpoint. Relay earnings are fetched in parallel.
91
64
  const config = loadConfig();
92
- // Wallet address lookup order:
93
- // 1. ~/.clawmoney/config.yaml cache (instant)
94
- // 2. Backend /api/v1/claw-agents/me (authoritative, ~200ms)
95
- // 3. awal address (Electron cold-start, last resort, 5s cap)
96
- // After a successful #2 we write the result back to the config so
97
- // every future `wallet balance` hits path #1.
98
65
  let walletAddress = config?.wallet_address ?? null;
99
- let addressSource = 'config';
100
66
  let addressError = null;
101
67
  if (!walletAddress && config?.api_key) {
102
68
  try {
103
69
  const resp = await apiGet('/api/v1/claw-agents/me', config.api_key);
104
70
  if (resp.ok && typeof resp.data?.wallet_address === 'string' && resp.data.wallet_address) {
105
71
  walletAddress = resp.data.wallet_address;
106
- addressSource = 'api';
107
- // Cache it so the next run is instant.
108
72
  try {
109
73
  saveConfig({ wallet_address: walletAddress });
110
74
  }
@@ -117,22 +81,6 @@ export async function walletBalanceCommand() {
117
81
  addressError = err.message;
118
82
  }
119
83
  }
120
- if (!walletAddress) {
121
- // Last resort: cold-start awal. Uses awalExecSafe so a wedged
122
- // Electron is automatically killed + retried before surfacing
123
- // the error. Capped at 6s per attempt.
124
- try {
125
- const awalResult = await awalExecSafe(['address'], { timeoutMs: 6_000 });
126
- const data = awalResult.data;
127
- if (typeof data?.address === 'string' && data.address) {
128
- walletAddress = data.address;
129
- addressSource = 'awal';
130
- }
131
- }
132
- catch (err) {
133
- addressError = err.message;
134
- }
135
- }
136
84
  const relayPromise = config?.api_key
137
85
  ? apiGet("/api/v1/relay/providers/me", config.api_key)
138
86
  .then((resp) => (resp.ok && Array.isArray(resp.data) ? resp.data : null))
@@ -151,7 +99,6 @@ export async function walletBalanceCommand() {
151
99
  else {
152
100
  onchainError = addressError ?? 'no wallet address in config';
153
101
  }
154
- void addressSource; // reserved for future debug display
155
102
  const relayRows = await relayPromise;
156
103
  spinner.stop();
157
104
  console.log('');
@@ -170,12 +117,6 @@ export async function walletBalanceCommand() {
170
117
  console.log('');
171
118
  console.log(chalk.bold(' Pending payout (Relay)'));
172
119
  if (relayRows && relayRows.length > 0) {
173
- // "Earned lifetime" is vanity — the only thing that matters for
174
- // the wallet view is: how much is already in the on-chain wallet
175
- // (shown above as USDC), and how much is still owed to the user
176
- // (pending = earned - withdrawn). Those two numbers together
177
- // tell the full story; showing "earned" separately would double-
178
- // count the wallet USDC that came from relay payouts.
179
120
  const earned = relayRows.reduce((s, p) => s + (p.total_earned_usd ?? 0), 0);
180
121
  const withdrawn = relayRows.reduce((s, p) => s + (p.total_withdrawn_usd ?? 0), 0);
181
122
  const pending = Math.max(0, earned - withdrawn);
@@ -194,13 +135,27 @@ export async function walletBalanceCommand() {
194
135
  export async function walletAddressCommand() {
195
136
  const spinner = ora('Getting wallet address...').start();
196
137
  try {
197
- // Read-only, safe to auto-retry on awal wedge.
198
- const result = await awalExecSafe(['address'], { timeoutMs: 8_000 });
138
+ const config = requireConfig();
139
+ // Fast path: config.yaml cache (written at setup).
140
+ if (config.wallet_address) {
141
+ spinner.succeed('Wallet Address');
142
+ console.log('');
143
+ console.log(` ${chalk.cyan(config.wallet_address)}`);
144
+ console.log('');
145
+ return;
146
+ }
147
+ const wallet = new CdpProvider(config.api_key);
148
+ const address = await wallet.getAddress();
149
+ // Cache back to config.
150
+ try {
151
+ saveConfig({ wallet_address: address });
152
+ }
153
+ catch {
154
+ // Non-fatal.
155
+ }
199
156
  spinner.succeed('Wallet Address');
200
157
  console.log('');
201
- const data = result.data;
202
- const address = data.address || result.raw.trim();
203
- console.log(` ${chalk.cyan(String(address))}`);
158
+ console.log(` ${chalk.cyan(address)}`);
204
159
  console.log('');
205
160
  }
206
161
  catch (err) {
@@ -210,22 +165,26 @@ export async function walletAddressCommand() {
210
165
  }
211
166
  export async function walletSendCommand(amount, to) {
212
167
  console.log('');
213
- console.log(chalk.bold(' Send Transaction'));
168
+ console.log(chalk.bold(' Send USDC'));
214
169
  console.log(chalk.dim(` Amount: ${amount}`));
215
170
  console.log(chalk.dim(` To: ${to}`));
216
171
  console.log('');
217
172
  const spinner = ora('Sending...').start();
218
173
  try {
219
- const result = await awalExec(['send', amount, to]);
174
+ const config = requireConfig();
175
+ const wallet = new CdpProvider(config.api_key);
176
+ // `amount` is the user-facing decimal (e.g. "1.5"); convert to atomic (6dp for USDC).
177
+ const [whole, frac = ''] = amount.split('.');
178
+ const paddedFrac = (frac + '000000').slice(0, 6);
179
+ const atomic = BigInt(whole || '0') * 1000000n + BigInt(paddedFrac || '0');
180
+ const result = await wallet.send(to, atomic.toString(), 'usdc');
220
181
  spinner.succeed('Transaction sent');
221
182
  console.log('');
222
- const data = result.data;
223
- if (data.txHash || data.hash || data.transactionHash) {
224
- const hash = data.txHash || data.hash || data.transactionHash;
225
- console.log(` ${chalk.dim('TX Hash:')} ${chalk.cyan(String(hash))}`);
183
+ if (result.transaction_hash) {
184
+ console.log(` ${chalk.dim('TX Hash:')} ${chalk.cyan(result.transaction_hash)}`);
226
185
  }
227
- else {
228
- console.log(` ${result.raw}`);
186
+ if (result.transaction_link) {
187
+ console.log(` ${chalk.dim('Link:')} ${chalk.cyan(result.transaction_link)}`);
229
188
  }
230
189
  console.log('');
231
190
  }
@@ -1,6 +1,7 @@
1
1
  import { apiGet, apiPost } from "../utils/api.js";
2
- import { awalExec, awalExecSafe } from "../utils/awal.js";
3
2
  import { requireConfig } from "../utils/config.js";
3
+ import { CdpProvider } from "../wallet/cdp-provider.js";
4
+ import { x402PayJson } from "../wallet/x402-client.js";
4
5
  const POLL_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
5
6
  const MAX_PER_CYCLE = 3;
6
7
  const MIN_BALANCE_USD = 0.05;
@@ -9,15 +10,16 @@ function log(msg) {
9
10
  const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
10
11
  console.log(`[${ts}] ${msg}`);
11
12
  }
12
- async function getUsdcBalance() {
13
+ async function getUsdcBalance(apiKey) {
13
14
  try {
14
- // Read-only balance check inside a long-running daemon loop —
15
- // must auto-recover if awal wedges during the day.
16
- const result = await awalExecSafe(["balance"], { timeoutMs: 10_000 });
17
- const base = result.data.base;
18
- const balances = base?.balances;
19
- const usdc = balances?.USDC;
20
- return parseFloat(String(usdc?.formatted || "0"));
15
+ const wallet = new CdpProvider(apiKey);
16
+ const bal = await wallet.getBalance("usdc");
17
+ // `amount` is in atomic units; USDC has 6 decimals.
18
+ const atomic = BigInt(bal.amount);
19
+ const divisor = 10n ** BigInt(bal.decimals || 6);
20
+ const whole = Number(atomic / divisor);
21
+ const frac = Number(atomic % divisor) / Number(divisor);
22
+ return whole + frac;
21
23
  }
22
24
  catch {
23
25
  return 0;
@@ -65,19 +67,15 @@ async function verifySubmission(task, submission, apiKey) {
65
67
  const tweetId = tweetMatch[1];
66
68
  let witnessData;
67
69
  try {
68
- witnessData = await awalExec([
69
- "x402",
70
- "pay",
71
- `https://witness.bnbot.ai/x/${tweetId}`,
72
- ]);
70
+ const wallet = new CdpProvider(apiKey);
71
+ witnessData = await x402PayJson(wallet, `https://witness.bnbot.ai/x/${tweetId}`);
73
72
  }
74
73
  catch (err) {
75
74
  log(` Witness failed: ${err.message}`);
76
75
  return false;
77
76
  }
78
- const wd = witnessData.data;
79
- const wdInner = wd?.data;
80
- const proof = (wdInner?.proof ?? wd?.proof ?? null);
77
+ const wdInner = witnessData?.data;
78
+ const proof = (wdInner?.proof ?? witnessData?.proof ?? null);
81
79
  if (!proof) {
82
80
  log(` No proof in witness response`);
83
81
  return false;
@@ -108,7 +106,7 @@ async function verifySubmission(task, submission, apiKey) {
108
106
  }
109
107
  async function runCycle(apiKey) {
110
108
  // Check balance
111
- const balance = await getUsdcBalance();
109
+ const balance = await getUsdcBalance(apiKey);
112
110
  if (balance < MIN_BALANCE_USD) {
113
111
  log(`Balance $${balance.toFixed(3)} below minimum $${MIN_BALANCE_USD}. Pausing.`);
114
112
  return;
@@ -0,0 +1,15 @@
1
+ import type { Asset, Balance, Eip712TypedData, SendResult, SignedTypedData, WalletProvider } from './provider.js';
2
+ /**
3
+ * CdpProvider: routes all wallet operations through bnbot-api's
4
+ * /api/v1/claw-agents/me/wallet/* endpoints. The underlying keys live
5
+ * in Coinbase CDP (Server Wallet v2) — this CLI never sees them.
6
+ */
7
+ export declare class CdpProvider implements WalletProvider {
8
+ private readonly apiKey;
9
+ constructor(apiKey: string);
10
+ getAddress(): Promise<string>;
11
+ getBalance(asset?: Asset): Promise<Balance>;
12
+ signTypedData(typed: Eip712TypedData, idempotencyKey?: string): Promise<SignedTypedData>;
13
+ send(to: string, amount: string, asset?: Asset, network?: string): Promise<SendResult>;
14
+ getOnrampUrl(amountUsd?: number, network?: string): Promise<string>;
15
+ }
@@ -0,0 +1,52 @@
1
+ import { apiGet, apiPost } from '../utils/api.js';
2
+ /**
3
+ * CdpProvider: routes all wallet operations through bnbot-api's
4
+ * /api/v1/claw-agents/me/wallet/* endpoints. The underlying keys live
5
+ * in Coinbase CDP (Server Wallet v2) — this CLI never sees them.
6
+ */
7
+ export class CdpProvider {
8
+ apiKey;
9
+ constructor(apiKey) {
10
+ this.apiKey = apiKey;
11
+ }
12
+ async getAddress() {
13
+ const res = await apiGet('/api/v1/claw-agents/me', this.apiKey);
14
+ if (!res.ok || !res.data?.wallet_address) {
15
+ throw new Error(res.ok
16
+ ? 'Agent has no wallet yet; claim your agent via the link sent to your email.'
17
+ : `Failed to fetch agent: ${res.status}`);
18
+ }
19
+ return res.data.wallet_address;
20
+ }
21
+ async getBalance(asset = 'usdc') {
22
+ const res = await apiGet(`/api/v1/claw-agents/me/wallet/balance?asset=${asset}`, this.apiKey);
23
+ if (!res.ok) {
24
+ throw new Error(`Failed to get balance: ${res.status}`);
25
+ }
26
+ return res.data;
27
+ }
28
+ async signTypedData(typed, idempotencyKey) {
29
+ const res = await apiPost('/api/v1/claw-agents/me/wallet/sign-typed-data', { ...typed, idempotency_key: idempotencyKey }, this.apiKey);
30
+ if (!res.ok) {
31
+ const err = res.data?.detail ?? `HTTP ${res.status}`;
32
+ throw new Error(`signTypedData failed: ${err}`);
33
+ }
34
+ return res.data;
35
+ }
36
+ async send(to, amount, asset = 'usdc', network) {
37
+ const res = await apiPost('/api/v1/claw-agents/me/wallet/send', { to, amount, asset, network }, this.apiKey);
38
+ if (!res.ok) {
39
+ const err = res.data?.detail ?? `HTTP ${res.status}`;
40
+ throw new Error(`send failed: ${err}`);
41
+ }
42
+ return res.data;
43
+ }
44
+ async getOnrampUrl(amountUsd = 5, network = 'base') {
45
+ const res = await apiPost('/api/v1/claw-agents/me/wallet/onramp-url', { amount_usd: amountUsd, network }, this.apiKey);
46
+ if (!res.ok) {
47
+ const err = res.data?.detail ?? `HTTP ${res.status}`;
48
+ throw new Error(`onramp url failed: ${err}`);
49
+ }
50
+ return res.data.url;
51
+ }
52
+ }
@@ -0,0 +1,28 @@
1
+ export interface Balance {
2
+ asset: string;
3
+ amount: string;
4
+ decimals: number;
5
+ }
6
+ export interface SendResult {
7
+ transaction_hash: string;
8
+ transaction_link?: string;
9
+ network?: string;
10
+ }
11
+ export interface SignedTypedData {
12
+ signature: string;
13
+ address: string;
14
+ }
15
+ export type Eip712TypedData = {
16
+ domain: Record<string, unknown>;
17
+ types: Record<string, unknown>;
18
+ primary_type: string;
19
+ message: Record<string, unknown>;
20
+ };
21
+ export type Asset = 'usdc' | 'eth';
22
+ export interface WalletProvider {
23
+ getAddress(): Promise<string>;
24
+ getBalance(asset?: Asset): Promise<Balance>;
25
+ signTypedData(typed: Eip712TypedData, idempotencyKey?: string): Promise<SignedTypedData>;
26
+ send(to: string, amount: string, asset?: Asset, network?: string): Promise<SendResult>;
27
+ getOnrampUrl(amountUsd?: number, network?: string): Promise<string>;
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import type { WalletProvider } from './provider.js';
2
+ export interface X402PaymentRequirement {
3
+ scheme: string;
4
+ network: string;
5
+ maxAmountRequired: string;
6
+ resource: string;
7
+ description?: string;
8
+ mimeType?: string;
9
+ payTo: string;
10
+ maxTimeoutSeconds: number;
11
+ asset: string;
12
+ extra?: {
13
+ name?: string;
14
+ version?: string;
15
+ };
16
+ }
17
+ export interface X402Challenge {
18
+ x402Version: number;
19
+ accepts: X402PaymentRequirement[];
20
+ error?: string;
21
+ }
22
+ export interface X402PayOptions {
23
+ method?: string;
24
+ body?: unknown;
25
+ headers?: Record<string, string>;
26
+ maxAmountAtomic?: string;
27
+ }
28
+ /**
29
+ * Execute an x402-protected HTTP request, handling the 402 challenge
30
+ * by signing a EIP-3009 TransferWithAuthorization via the wallet
31
+ * provider and retrying with an X-Payment header.
32
+ *
33
+ * Returns the final (paid) Response. If the server does not challenge
34
+ * with 402, returns the original response unchanged.
35
+ */
36
+ export declare function x402Fetch(wallet: WalletProvider, url: string, options?: X402PayOptions): Promise<Response>;
37
+ /**
38
+ * Convenience: run x402Fetch and return parsed JSON body.
39
+ * Throws on non-2xx after payment.
40
+ */
41
+ export declare function x402PayJson<T = unknown>(wallet: WalletProvider, url: string, options?: X402PayOptions): Promise<T>;
@@ -0,0 +1,145 @@
1
+ import { bytesToHex, keccak256, stringToBytes } from 'viem';
2
+ const CHAIN_IDS = {
3
+ base: 8453,
4
+ 'base-sepolia': 84532,
5
+ ethereum: 1,
6
+ 'ethereum-sepolia': 11155111,
7
+ polygon: 137,
8
+ arbitrum: 42161,
9
+ optimism: 10,
10
+ avalanche: 43114,
11
+ };
12
+ const EIP3009_TYPES = {
13
+ EIP712Domain: [
14
+ { name: 'name', type: 'string' },
15
+ { name: 'version', type: 'string' },
16
+ { name: 'chainId', type: 'uint256' },
17
+ { name: 'verifyingContract', type: 'address' },
18
+ ],
19
+ TransferWithAuthorization: [
20
+ { name: 'from', type: 'address' },
21
+ { name: 'to', type: 'address' },
22
+ { name: 'value', type: 'uint256' },
23
+ { name: 'validAfter', type: 'uint256' },
24
+ { name: 'validBefore', type: 'uint256' },
25
+ { name: 'nonce', type: 'bytes32' },
26
+ ],
27
+ };
28
+ function randomNonce32() {
29
+ const bytes = new Uint8Array(32);
30
+ crypto.getRandomValues(bytes);
31
+ return bytesToHex(bytes);
32
+ }
33
+ function pickAccepts(challenge) {
34
+ const exact = challenge.accepts.filter((a) => a.scheme === 'exact');
35
+ if (exact.length === 0) {
36
+ throw new Error(`No supported x402 scheme. Server accepts: ${challenge.accepts.map((a) => a.scheme).join(', ')}`);
37
+ }
38
+ // Prefer Base mainnet; otherwise first match.
39
+ return exact.find((a) => a.network === 'base') ?? exact[0];
40
+ }
41
+ async function signPaymentAuthorization(wallet, req, fromAddress) {
42
+ const chainId = CHAIN_IDS[req.network];
43
+ if (!chainId) {
44
+ throw new Error(`Unsupported x402 network: ${req.network}`);
45
+ }
46
+ const now = Math.floor(Date.now() / 1000);
47
+ const validAfter = 0;
48
+ const validBefore = now + req.maxTimeoutSeconds;
49
+ const nonce = randomNonce32();
50
+ const authorization = {
51
+ from: fromAddress,
52
+ to: req.payTo,
53
+ value: req.maxAmountRequired,
54
+ validAfter,
55
+ validBefore,
56
+ nonce,
57
+ };
58
+ const typed = {
59
+ domain: {
60
+ name: req.extra?.name ?? 'USD Coin',
61
+ version: req.extra?.version ?? '2',
62
+ chainId,
63
+ verifyingContract: req.asset,
64
+ },
65
+ types: {
66
+ TransferWithAuthorization: EIP3009_TYPES.TransferWithAuthorization,
67
+ },
68
+ primary_type: 'TransferWithAuthorization',
69
+ message: authorization,
70
+ };
71
+ // Idempotency key: deterministic hash of the authorization so a retry
72
+ // producing the same nonce won't double-sign.
73
+ const idempotencyKey = keccak256(stringToBytes(JSON.stringify(authorization)));
74
+ const signed = await wallet.signTypedData(typed, idempotencyKey);
75
+ return { signature: signed.signature, authorization };
76
+ }
77
+ function encodePaymentHeader(params) {
78
+ const payload = {
79
+ x402Version: 1,
80
+ scheme: params.scheme,
81
+ network: params.network,
82
+ payload: {
83
+ signature: params.signature,
84
+ authorization: params.authorization,
85
+ },
86
+ };
87
+ return Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64');
88
+ }
89
+ /**
90
+ * Execute an x402-protected HTTP request, handling the 402 challenge
91
+ * by signing a EIP-3009 TransferWithAuthorization via the wallet
92
+ * provider and retrying with an X-Payment header.
93
+ *
94
+ * Returns the final (paid) Response. If the server does not challenge
95
+ * with 402, returns the original response unchanged.
96
+ */
97
+ export async function x402Fetch(wallet, url, options = {}) {
98
+ const method = options.method ?? 'GET';
99
+ const headers = { ...(options.headers ?? {}) };
100
+ const body = options.body === undefined ? undefined
101
+ : typeof options.body === 'string' ? options.body
102
+ : JSON.stringify(options.body);
103
+ if (body && !headers['Content-Type']) {
104
+ headers['Content-Type'] = 'application/json';
105
+ }
106
+ const initialRes = await fetch(url, { method, headers, body });
107
+ if (initialRes.status !== 402) {
108
+ return initialRes;
109
+ }
110
+ const challenge = (await initialRes.json());
111
+ const requirement = pickAccepts(challenge);
112
+ if (options.maxAmountAtomic) {
113
+ const cap = BigInt(options.maxAmountAtomic);
114
+ const asked = BigInt(requirement.maxAmountRequired);
115
+ if (asked > cap) {
116
+ throw new Error(`x402 payment ${asked} exceeds --max-amount cap ${cap} (network=${requirement.network})`);
117
+ }
118
+ }
119
+ const fromAddress = await wallet.getAddress();
120
+ const { signature, authorization } = await signPaymentAuthorization(wallet, requirement, fromAddress);
121
+ const paymentHeader = encodePaymentHeader({
122
+ scheme: requirement.scheme,
123
+ network: requirement.network,
124
+ signature,
125
+ authorization,
126
+ });
127
+ const retryRes = await fetch(url, {
128
+ method,
129
+ headers: { ...headers, 'X-Payment': paymentHeader },
130
+ body,
131
+ });
132
+ return retryRes;
133
+ }
134
+ /**
135
+ * Convenience: run x402Fetch and return parsed JSON body.
136
+ * Throws on non-2xx after payment.
137
+ */
138
+ export async function x402PayJson(wallet, url, options = {}) {
139
+ const res = await x402Fetch(wallet, url, options);
140
+ if (!res.ok) {
141
+ const text = await res.text().catch(() => '');
142
+ throw new Error(`x402 request failed: HTTP ${res.status} ${text}`);
143
+ }
144
+ return (await res.json());
145
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,12 +18,12 @@
18
18
  "prepublishOnly": "npm run build"
19
19
  },
20
20
  "dependencies": {
21
- "awal": "^2.2.0",
22
21
  "@bnbot/cli": "^0.3.0",
23
22
  "@clack/prompts": "^0.7.0",
24
23
  "chalk": "^5.3.0",
25
24
  "commander": "^12.0.0",
26
25
  "ora": "^8.0.0",
26
+ "viem": "^2.48.1",
27
27
  "ws": "^8.20.0",
28
28
  "yaml": "^2.4.0"
29
29
  },