clawmoney 0.16.0 → 0.17.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/dist/commands/gig.js +12 -9
- package/dist/commands/hub.js +16 -8
- package/dist/commands/promote.js +7 -6
- package/dist/commands/setup.js +103 -218
- package/dist/commands/wallet.js +44 -85
- package/dist/promote/auto-verify.js +16 -18
- package/dist/wallet/cdp-provider.d.ts +15 -0
- package/dist/wallet/cdp-provider.js +52 -0
- package/dist/wallet/provider.d.ts +28 -0
- package/dist/wallet/provider.js +1 -0
- package/dist/wallet/x402-client.d.ts +41 -0
- package/dist/wallet/x402-client.js +145 -0
- package/package.json +2 -2
package/dist/commands/gig.js
CHANGED
|
@@ -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
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
|
88
|
+
console.log(chalk.dim(` Fund via x402 in another client, or use Stripe checkout above.`));
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
}
|
package/dist/commands/hub.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
250
|
+
let payData;
|
|
246
251
|
try {
|
|
247
|
-
|
|
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(
|
|
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
|
package/dist/commands/promote.js
CHANGED
|
@@ -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
|
-
|
|
86
|
-
|
|
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 —
|
|
96
|
-
const
|
|
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)}`));
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,252 +1,136 @@
|
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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));
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
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;
|
|
55
|
-
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
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
needsLogin = true;
|
|
80
|
-
walletSpinner.info('Wallet not authenticated');
|
|
81
|
-
}
|
|
82
|
-
// Step 3: Ask for email
|
|
41
|
+
// Step 1: Ask for email.
|
|
83
42
|
const email = await prompt(chalk.cyan('? ') + 'Enter your email: ');
|
|
84
43
|
if (!email || !email.includes('@')) {
|
|
85
44
|
console.log(chalk.red('Invalid email address.'));
|
|
86
45
|
return;
|
|
87
46
|
}
|
|
88
|
-
// Step
|
|
89
|
-
if (needsLogin) {
|
|
90
|
-
const loginSpinner = ora('Logging in to wallet...').start();
|
|
91
|
-
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.'));
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
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));
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
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
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// Address fetch still failed — continue anyway; the agent
|
|
141
|
-
// register/login flow below doesn't strictly require a wallet.
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
loginSpinner.fail('Wallet login failed');
|
|
146
|
-
console.error(chalk.red(msg));
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
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
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// Step 5: Check agent status
|
|
47
|
+
// Step 2: Check if this email already has an agent.
|
|
163
48
|
const agentSpinner = ora('Checking agent status...').start();
|
|
49
|
+
let existingStatus = '';
|
|
164
50
|
try {
|
|
165
51
|
const checkResp = await apiGet(`/api/v1/claw-agents/check-email?email=${encodeURIComponent(email)}`);
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
});
|
|
52
|
+
if (checkResp.ok) {
|
|
53
|
+
const info = checkResp.data.agent ?? {};
|
|
54
|
+
existingStatus = (info.status ?? checkResp.data.status ?? '').toUpperCase();
|
|
204
55
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
});
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Fall through — if the check fails, we still try registering.
|
|
59
|
+
}
|
|
60
|
+
if (existingStatus === 'ACTIVE') {
|
|
61
|
+
agentSpinner.warn('An active agent already exists for this email.');
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(chalk.yellow('If you have lost your API key, re-register and a new claim link will be sent.'));
|
|
64
|
+
const proceed = await prompt(chalk.cyan('? ') + 'Continue with re-registration? (y/N): ');
|
|
65
|
+
if (!/^y(es)?$/i.test(proceed.trim())) {
|
|
66
|
+
return;
|
|
237
67
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
68
|
+
agentSpinner.start('Checking agent status...');
|
|
69
|
+
}
|
|
70
|
+
// Step 3: Register agent (or re-send claim link for an UNCLAIMED agent).
|
|
71
|
+
// The backend generates the anonymous slug from email hash; we never send a name.
|
|
72
|
+
agentSpinner.text = 'Registering agent...';
|
|
73
|
+
let regData;
|
|
74
|
+
try {
|
|
75
|
+
const regResp = await apiPost('/api/v1/claw-agents/register', { email });
|
|
76
|
+
if (!regResp.ok) {
|
|
77
|
+
agentSpinner.fail('Registration failed');
|
|
78
|
+
console.error(chalk.red(JSON.stringify(regResp.data)));
|
|
241
79
|
return;
|
|
242
80
|
}
|
|
81
|
+
regData = regResp.data;
|
|
243
82
|
}
|
|
244
83
|
catch (err) {
|
|
245
|
-
agentSpinner.fail('
|
|
84
|
+
agentSpinner.fail('Registration failed');
|
|
246
85
|
console.error(chalk.red(err.message));
|
|
247
86
|
return;
|
|
248
87
|
}
|
|
249
|
-
|
|
88
|
+
agentSpinner.succeed(`Agent registered: ${regData.agent.slug}`);
|
|
89
|
+
// Persist the api_key and agent_id immediately — the key only activates
|
|
90
|
+
// after the claim link is clicked, but we save it now so nothing is lost
|
|
91
|
+
// if the user ctrl-C's during the claim step.
|
|
92
|
+
saveConfig({
|
|
93
|
+
api_key: regData.api_key,
|
|
94
|
+
agent_id: regData.agent.id,
|
|
95
|
+
agent_slug: regData.agent.slug,
|
|
96
|
+
email,
|
|
97
|
+
});
|
|
98
|
+
// Step 4: Instruct the user to click the claim link in their email.
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(chalk.bold(' Check your email'));
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(chalk.dim(' We sent a claim link to'), chalk.cyan(email));
|
|
103
|
+
console.log(chalk.dim(' Click the link to complete setup. Your CDP wallet will be'));
|
|
104
|
+
console.log(chalk.dim(' provisioned automatically and this CLI will unlock.'));
|
|
105
|
+
console.log('');
|
|
106
|
+
// Step 5: Poll for claim completion.
|
|
107
|
+
const pollSpinner = ora('Waiting for claim link to be clicked...').start();
|
|
108
|
+
let tickCount = 0;
|
|
109
|
+
const completed = await pollForClaim(regData.api_key, () => {
|
|
110
|
+
tickCount++;
|
|
111
|
+
if (tickCount % 5 === 0) {
|
|
112
|
+
pollSpinner.text = `Waiting for claim link... (${Math.floor((tickCount * CLAIM_POLL_INTERVAL_MS) / 1000)}s)`;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
if (!completed) {
|
|
116
|
+
pollSpinner.warn('Claim not completed within 15 minutes.');
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(chalk.yellow(' You can re-run'), chalk.cyan('clawmoney setup'), chalk.yellow('later to resume.'));
|
|
119
|
+
console.log(chalk.dim(' Your API key is already saved and will activate once you click the claim link.'));
|
|
120
|
+
console.log('');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const walletAddress = completed.wallet_address ?? '';
|
|
124
|
+
pollSpinner.succeed('Agent claimed');
|
|
125
|
+
// Step 6: Update config with the now-known wallet address.
|
|
126
|
+
saveConfig({
|
|
127
|
+
api_key: regData.api_key,
|
|
128
|
+
agent_id: completed.id,
|
|
129
|
+
agent_slug: completed.slug,
|
|
130
|
+
email,
|
|
131
|
+
wallet_address: walletAddress || undefined,
|
|
132
|
+
});
|
|
133
|
+
// Step 7: Summary.
|
|
250
134
|
const config = loadConfig();
|
|
251
135
|
console.log('');
|
|
252
136
|
console.log(chalk.green.bold(' Setup complete!'));
|
|
@@ -256,6 +140,7 @@ export async function setupCommand() {
|
|
|
256
140
|
console.log(chalk.dim(` Agent Slug: ${config.agent_slug}`));
|
|
257
141
|
if (walletAddress) {
|
|
258
142
|
console.log(chalk.dim(` Wallet: ${walletAddress}`));
|
|
143
|
+
console.log(chalk.dim(` Custody: Coinbase CDP Server Wallet`));
|
|
259
144
|
}
|
|
260
145
|
console.log(chalk.dim(` Config: ${getConfigPath()}`));
|
|
261
146
|
}
|
package/dist/commands/wallet.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
|
|
47
|
-
const
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
//
|
|
82
|
-
//
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
228
|
-
console.log(` ${result.
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
79
|
-
const
|
|
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.
|
|
3
|
+
"version": "0.17.0",
|
|
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
|
},
|