wip-agent-pay 1.0.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.
- package/.license-guard.json +7 -0
- package/CASH.md +75 -0
- package/CHANGELOG.md +18 -0
- package/CLA.md +17 -0
- package/DEPLOY-WORKER.md +161 -0
- package/LICENSE +21 -0
- package/PARTNERS-402.md +75 -0
- package/PARTNERS-STRIPE.md +69 -0
- package/README.md +80 -0
- package/REFERENCE.md +160 -0
- package/SETUP.md +242 -0
- package/SKILL.md +43 -0
- package/SPEC.md +156 -0
- package/WALLET.md +89 -0
- package/cli.js +265 -0
- package/detect.mjs +13 -0
- package/examples/minimal/README.md +17 -0
- package/install.js +6 -0
- package/lib/cdp-auth.js +131 -0
- package/mcp-server.mjs +105 -0
- package/openclaw.mjs +88 -0
- package/openclaw.plugin.json +17 -0
- package/package.json +43 -0
- package/providers/coinbase.js +55 -0
- package/providers/index.js +155 -0
- package/providers/passthrough.js +139 -0
- package/providers/privy.js +50 -0
- package/providers/stripe.js +54 -0
- package/providers/x402.js +50 -0
- package/skills/agent-pay/SKILL.md +30 -0
- package/worker/index.js +1440 -0
- package/worker/wrangler.toml +25 -0
package/cli.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// wip-pay CLI
|
|
3
|
+
// Your agent's wallet.
|
|
4
|
+
//
|
|
5
|
+
// Commands:
|
|
6
|
+
// wip-pay pay <url> Pay for paywalled content
|
|
7
|
+
// wip-pay fund <amount> Fund wallet (Apple Pay)
|
|
8
|
+
// wip-pay balance Check wallet balance
|
|
9
|
+
// wip-pay history Transaction history
|
|
10
|
+
// wip-pay budget View/set spending limits
|
|
11
|
+
// wip-pay <amount> <service> [note] Mint one-time URL
|
|
12
|
+
//
|
|
13
|
+
// Default: Pool Mode (Apple Pay per transaction, Parker's float)
|
|
14
|
+
// --wallet=cdp or --wallet=privy: Path 1 (self-custody, instant)
|
|
15
|
+
|
|
16
|
+
import { argv } from 'node:process';
|
|
17
|
+
import { pay, fund, mint, balance, history, budget } from './providers/index.js';
|
|
18
|
+
|
|
19
|
+
const args = argv.slice(2);
|
|
20
|
+
const command = args[0];
|
|
21
|
+
|
|
22
|
+
// --- Help ---
|
|
23
|
+
if (args.includes('--help') || args.length === 0) {
|
|
24
|
+
console.log('\n wip-pay ... your agent\'s wallet');
|
|
25
|
+
console.log('\n Commands:');
|
|
26
|
+
console.log(' wip-pay pay <url> Pay for paywalled content');
|
|
27
|
+
console.log(' wip-pay fund <amount> Fund wallet (Apple Pay)');
|
|
28
|
+
console.log(' wip-pay balance Check wallet balance');
|
|
29
|
+
console.log(' wip-pay history Transaction history');
|
|
30
|
+
console.log(' wip-pay budget View spending limits');
|
|
31
|
+
console.log(' wip-pay budget set <daily> [perTx] Set spending limits');
|
|
32
|
+
console.log(' wip-pay <amount> <service> [note] Mint one-time URL');
|
|
33
|
+
console.log('\n Options:');
|
|
34
|
+
console.log(' --wallet=cdp Use your CDP wallet (instant, no Apple Pay)');
|
|
35
|
+
console.log(' --wallet=privy Use your Privy wallet (instant, no Apple Pay)');
|
|
36
|
+
console.log(' (no flag) Pool Mode: Apple Pay per transaction');
|
|
37
|
+
console.log('\n Pool Mode pricing:');
|
|
38
|
+
console.log(' x402 price + Stripe fees + $0.25 flat fee');
|
|
39
|
+
console.log(' Max $25 per transaction. Over $25 requires your own wallet.');
|
|
40
|
+
console.log('\n Examples:');
|
|
41
|
+
console.log(' wip-pay pay https://morning-stew.../v1/issues/MS-3');
|
|
42
|
+
console.log(' wip-pay pay https://morning-stew.../v1/issues/MS-3 --wallet=cdp');
|
|
43
|
+
console.log(' wip-pay fund 10');
|
|
44
|
+
console.log(' wip-pay balance');
|
|
45
|
+
console.log(' wip-pay history');
|
|
46
|
+
console.log(' wip-pay budget set 5.00 1.00');
|
|
47
|
+
console.log(' wip-pay 0.10 morning-stew "MS-#8"');
|
|
48
|
+
console.log('\n Setup:');
|
|
49
|
+
console.log(' See SETUP.md for configuration.\n');
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse flags
|
|
54
|
+
const walletFlag = args.find(a => a.startsWith('--wallet='));
|
|
55
|
+
const wallet = walletFlag ? walletFlag.split('=')[1] : undefined; // undefined = Pool Mode
|
|
56
|
+
const cleanArgs = args.filter(a => !a.startsWith('--'));
|
|
57
|
+
|
|
58
|
+
// --- Pay ---
|
|
59
|
+
if (command === 'pay') {
|
|
60
|
+
const url = cleanArgs[1];
|
|
61
|
+
if (!url) {
|
|
62
|
+
console.error(' Error: url is required. Usage: wip-pay pay <url>');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result = await pay(url, { wallet });
|
|
67
|
+
|
|
68
|
+
// Path 1: wallet-signed payment (instant)
|
|
69
|
+
if (result.success && !result.needsPayment) {
|
|
70
|
+
if (result.free) {
|
|
71
|
+
console.log(`\n Content is free. No payment needed.\n`);
|
|
72
|
+
} else {
|
|
73
|
+
console.log(`\n Paid: $${result.amount}`);
|
|
74
|
+
console.log(` Service: ${result.service}`);
|
|
75
|
+
if (result.txHash) console.log(` Tx: ${result.txHash}`);
|
|
76
|
+
console.log();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Pool Mode: needs Apple Pay
|
|
80
|
+
else if (result.success && result.needsPayment) {
|
|
81
|
+
const p = result.pricing;
|
|
82
|
+
console.log(`\n x402 price: $${p.x402Amount}`);
|
|
83
|
+
console.log(` Our fee: $${p.poolFee}`);
|
|
84
|
+
console.log(` Stripe fee: $${p.stripeFee}`);
|
|
85
|
+
console.log(` Total charge: $${p.totalCharge}`);
|
|
86
|
+
console.log(` Service: ${result.service}`);
|
|
87
|
+
console.log(`\n Opening Apple Pay checkout...`);
|
|
88
|
+
|
|
89
|
+
// Open checkout in browser
|
|
90
|
+
try {
|
|
91
|
+
const { execSync } = await import('node:child_process');
|
|
92
|
+
execSync(`open "${result.checkoutUrl}"`, { stdio: 'ignore' });
|
|
93
|
+
console.log(' (Opened in your browser)');
|
|
94
|
+
} catch {
|
|
95
|
+
console.log(` Open this URL to pay: ${result.checkoutUrl}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Wait for payment confirmation
|
|
99
|
+
console.log(' Waiting for payment...\n');
|
|
100
|
+
const { waitForPayment } = await import('./providers/passthrough.js');
|
|
101
|
+
const confirmation = await waitForPayment(result.paymentId);
|
|
102
|
+
|
|
103
|
+
if (confirmation.success) {
|
|
104
|
+
console.log(` Paid. Content received.\n`);
|
|
105
|
+
} else {
|
|
106
|
+
console.error(` ${confirmation.error || 'Payment not completed.'}\n`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Over pool limit
|
|
111
|
+
else if (result.overPoolLimit) {
|
|
112
|
+
console.log(`\n This costs $${result.amount}. That exceeds the $${result.poolMax} pool limit.`);
|
|
113
|
+
console.log(` Use your own wallet: wip-pay pay ${url} --wallet=cdp`);
|
|
114
|
+
console.log(` Or set up a user wallet: see SETUP.md\n`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
// Error
|
|
118
|
+
else {
|
|
119
|
+
console.error(`\n Payment failed: ${result.error || 'unknown error'}\n`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- Fund (Apple Pay) ---
|
|
125
|
+
else if (command === 'fund') {
|
|
126
|
+
const amount = parseFloat(cleanArgs[1]);
|
|
127
|
+
if (isNaN(amount) || amount <= 0) {
|
|
128
|
+
console.error(' Error: amount must be a positive number. Usage: wip-pay fund <amount>');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = await fund(amount, { wallet });
|
|
133
|
+
|
|
134
|
+
if (result.success) {
|
|
135
|
+
console.log(`\n Checkout ready. Open this URL to fund your wallet:`);
|
|
136
|
+
console.log(` ${result.checkoutUrl}`);
|
|
137
|
+
console.log(`\n Amount: $${result.amount}`);
|
|
138
|
+
console.log(` Wallet: ${result.wallet}\n`);
|
|
139
|
+
|
|
140
|
+
// Try to open in browser
|
|
141
|
+
try {
|
|
142
|
+
const { execSync } = await import('node:child_process');
|
|
143
|
+
execSync(`open "${result.checkoutUrl}"`, { stdio: 'ignore' });
|
|
144
|
+
console.log(' (Opened in your browser)\n');
|
|
145
|
+
} catch {
|
|
146
|
+
// Not on macOS or open failed
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
console.error(`\n Funding failed: ${result.error || 'unknown error'}\n`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Balance ---
|
|
155
|
+
else if (command === 'balance') {
|
|
156
|
+
const result = await balance({ wallet });
|
|
157
|
+
|
|
158
|
+
if (result.error) {
|
|
159
|
+
console.error(`\n ${result.error}\n`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(`\n Wallet: ${wallet || 'pool'}`);
|
|
164
|
+
console.log(` Balance: $${result.balance || '0.00'}`);
|
|
165
|
+
if (result.address) console.log(` Address: ${result.address}`);
|
|
166
|
+
console.log();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- History ---
|
|
170
|
+
else if (command === 'history') {
|
|
171
|
+
const limit = parseInt(cleanArgs[1]) || 20;
|
|
172
|
+
const result = await history({ wallet, limit });
|
|
173
|
+
|
|
174
|
+
if (result.error) {
|
|
175
|
+
console.error(`\n ${result.error}\n`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(`\n Wallet: ${wallet || 'pool'}`);
|
|
180
|
+
console.log(` Recent transactions:\n`);
|
|
181
|
+
|
|
182
|
+
if (!result.transactions || result.transactions.length === 0) {
|
|
183
|
+
console.log(' No transactions yet.\n');
|
|
184
|
+
} else {
|
|
185
|
+
for (const tx of result.transactions) {
|
|
186
|
+
const dir = tx.type === 'fund' ? '+' : '-';
|
|
187
|
+
const date = new Date(tx.timestamp).toLocaleString();
|
|
188
|
+
console.log(` ${dir}$${tx.amount} ${tx.service || tx.type} ${date}`);
|
|
189
|
+
if (tx.note) console.log(` ${tx.note}`);
|
|
190
|
+
}
|
|
191
|
+
console.log();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- Budget ---
|
|
196
|
+
else if (command === 'budget') {
|
|
197
|
+
const subcommand = cleanArgs[1];
|
|
198
|
+
|
|
199
|
+
if (subcommand === 'set') {
|
|
200
|
+
const daily = parseFloat(cleanArgs[2]);
|
|
201
|
+
const perTx = cleanArgs[3] ? parseFloat(cleanArgs[3]) : undefined;
|
|
202
|
+
|
|
203
|
+
if (isNaN(daily) || daily <= 0) {
|
|
204
|
+
console.error(' Error: daily limit required. Usage: wip-pay budget set <daily> [perTx]');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const result = await budget({ wallet, daily, perTx });
|
|
209
|
+
|
|
210
|
+
if (result.error) {
|
|
211
|
+
console.error(`\n ${result.error}\n`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(`\n Budget updated:`);
|
|
216
|
+
console.log(` Daily limit: $${result.daily}`);
|
|
217
|
+
if (result.perTx) console.log(` Per-transaction limit: $${result.perTx}`);
|
|
218
|
+
console.log();
|
|
219
|
+
} else {
|
|
220
|
+
// View current budget
|
|
221
|
+
const result = await budget({ wallet });
|
|
222
|
+
|
|
223
|
+
if (result.error) {
|
|
224
|
+
console.error(`\n ${result.error}\n`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log(`\n Wallet: ${wallet || 'pool'}`);
|
|
229
|
+
console.log(` Daily limit: $${result.daily || 'none'}`);
|
|
230
|
+
console.log(` Per-transaction limit: $${result.perTx || 'none'}`);
|
|
231
|
+
console.log(` Spent today: $${result.spentToday || '0.00'}`);
|
|
232
|
+
console.log(` Remaining today: $${result.remainingToday || result.daily || 'unlimited'}`);
|
|
233
|
+
console.log();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- Mint one-time URL (existing Mode B) ---
|
|
238
|
+
else {
|
|
239
|
+
const amount = parseFloat(cleanArgs[0]);
|
|
240
|
+
const service = cleanArgs[1];
|
|
241
|
+
const note = cleanArgs[2] || '';
|
|
242
|
+
|
|
243
|
+
if (isNaN(amount) || amount <= 0) {
|
|
244
|
+
console.error(' Error: amount must be a positive number');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
if (!service) {
|
|
248
|
+
console.error(' Error: service is required. Usage: wip-pay <amount> <service> [note]');
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = await mint(amount, service, note);
|
|
253
|
+
|
|
254
|
+
if (result.success) {
|
|
255
|
+
if (result.demo) {
|
|
256
|
+
console.log(` Demo URL (not real): ${result.url}\n`);
|
|
257
|
+
} else {
|
|
258
|
+
console.log(`\n One-time URL (paste this back to your agent):`);
|
|
259
|
+
console.log(` ${result.url}\n`);
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
console.error(`\n Payment failed: ${result.error || 'unknown error'}\n`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
}
|
package/detect.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# examples/minimal/
|
|
2
|
+
|
|
3
|
+
Minimal example of using wip-agent-pay in your agent.
|
|
4
|
+
|
|
5
|
+
Just say to any agent:
|
|
6
|
+
|
|
7
|
+
> Authorize 0.10 USDC for morning-stew MS-#8
|
|
8
|
+
|
|
9
|
+
The agent will automatically call:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
wip-agent-pay 0.10 morning-stew "MS-#8"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You run it ... approve on Coinbase ... paste the returned URL back to the agent.
|
|
16
|
+
|
|
17
|
+
See REFERENCE.md for full setup.
|
package/install.js
ADDED
package/lib/cdp-auth.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// lib/cdp-auth.js
|
|
2
|
+
// Ed25519 JWT generation for Coinbase CDP Server Wallets v2.
|
|
3
|
+
// Uses Web Crypto API ... works in Cloudflare Workers and Node 20+.
|
|
4
|
+
//
|
|
5
|
+
// CDP requires two JWTs per request:
|
|
6
|
+
// 1. Bearer token (API auth) ... signed with API Key Secret
|
|
7
|
+
// 2. X-Wallet-Auth (wallet auth) ... signed with Wallet Secret
|
|
8
|
+
//
|
|
9
|
+
// Both use Ed25519 (EdDSA).
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base64url encode a buffer.
|
|
13
|
+
*/
|
|
14
|
+
function base64url(buffer) {
|
|
15
|
+
const bytes = new Uint8Array(buffer);
|
|
16
|
+
let str = '';
|
|
17
|
+
for (const b of bytes) str += String.fromCharCode(b);
|
|
18
|
+
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Import an Ed25519 private key from base64-encoded raw bytes.
|
|
23
|
+
*/
|
|
24
|
+
async function importEd25519Key(base64Secret) {
|
|
25
|
+
const raw = Uint8Array.from(atob(base64Secret), c => c.charCodeAt(0));
|
|
26
|
+
// Ed25519 PKCS8 wrapping: OID prefix + raw 32-byte key
|
|
27
|
+
// PKCS8 header for Ed25519: 302e020100300506032b657004220420
|
|
28
|
+
const pkcs8Prefix = new Uint8Array([
|
|
29
|
+
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
|
|
30
|
+
0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20
|
|
31
|
+
]);
|
|
32
|
+
const pkcs8 = new Uint8Array(pkcs8Prefix.length + raw.length);
|
|
33
|
+
pkcs8.set(pkcs8Prefix);
|
|
34
|
+
pkcs8.set(raw, pkcs8Prefix.length);
|
|
35
|
+
|
|
36
|
+
return crypto.subtle.importKey(
|
|
37
|
+
'pkcs8', pkcs8, { name: 'Ed25519' }, false, ['sign']
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sign a JWT payload with Ed25519.
|
|
43
|
+
*/
|
|
44
|
+
async function signJWT(payload, key) {
|
|
45
|
+
const header = { alg: 'EdDSA', typ: 'JWT' };
|
|
46
|
+
const enc = new TextEncoder();
|
|
47
|
+
const headerB64 = base64url(enc.encode(JSON.stringify(header)));
|
|
48
|
+
const payloadB64 = base64url(enc.encode(JSON.stringify(payload)));
|
|
49
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
50
|
+
const signature = await crypto.subtle.sign('Ed25519', key, enc.encode(signingInput));
|
|
51
|
+
return `${signingInput}.${base64url(signature)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate the Bearer token (API auth JWT).
|
|
56
|
+
*
|
|
57
|
+
* @param {string} apiKeyId - CDP API Key ID
|
|
58
|
+
* @param {string} apiKeySecret - CDP API Key Secret (base64 Ed25519)
|
|
59
|
+
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
60
|
+
* @param {string} url - Full request URL
|
|
61
|
+
*/
|
|
62
|
+
export async function createBearerToken(apiKeyId, apiKeySecret, method, url) {
|
|
63
|
+
const key = await importEd25519Key(apiKeySecret);
|
|
64
|
+
const now = Math.floor(Date.now() / 1000);
|
|
65
|
+
const parsed = new URL(url);
|
|
66
|
+
const uri = `${method.toUpperCase()} ${parsed.host}${parsed.pathname}`;
|
|
67
|
+
|
|
68
|
+
const payload = {
|
|
69
|
+
sub: apiKeyId,
|
|
70
|
+
iss: 'cdp',
|
|
71
|
+
aud: ['cdp_service'],
|
|
72
|
+
nbf: now,
|
|
73
|
+
exp: now + 120,
|
|
74
|
+
uris: [uri],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return signJWT(payload, key);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate the X-Wallet-Auth JWT.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} walletSecret - Wallet Secret (base64 Ed25519)
|
|
84
|
+
* @param {string} method - HTTP method
|
|
85
|
+
* @param {string} url - Full request URL
|
|
86
|
+
* @param {string} body - Request body (JSON string) or empty
|
|
87
|
+
*/
|
|
88
|
+
export async function createWalletAuth(walletSecret, method, url, body = '') {
|
|
89
|
+
const key = await importEd25519Key(walletSecret);
|
|
90
|
+
const now = Math.floor(Date.now() / 1000);
|
|
91
|
+
const parsed = new URL(url);
|
|
92
|
+
const uri = `${method.toUpperCase()} ${parsed.host}${parsed.pathname}`;
|
|
93
|
+
|
|
94
|
+
// SHA-256 hash of canonical request body
|
|
95
|
+
const enc = new TextEncoder();
|
|
96
|
+
const bodyHash = await crypto.subtle.digest('SHA-256', enc.encode(body));
|
|
97
|
+
const reqHash = base64url(bodyHash);
|
|
98
|
+
|
|
99
|
+
const jti = crypto.randomUUID();
|
|
100
|
+
|
|
101
|
+
const payload = {
|
|
102
|
+
iat: now,
|
|
103
|
+
nbf: now,
|
|
104
|
+
exp: now + 120,
|
|
105
|
+
jti,
|
|
106
|
+
uris: [uri],
|
|
107
|
+
reqHash,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return signJWT(payload, key);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Generate both auth headers for a CDP API request.
|
|
115
|
+
*
|
|
116
|
+
* @param {object} creds - { apiKeyId, apiKeySecret, walletSecret }
|
|
117
|
+
* @param {string} method - HTTP method
|
|
118
|
+
* @param {string} url - Full request URL
|
|
119
|
+
* @param {string} body - Request body JSON string
|
|
120
|
+
* @returns {{ authorization: string, walletAuth: string }}
|
|
121
|
+
*/
|
|
122
|
+
export async function createAuthHeaders(creds, method, url, body = '') {
|
|
123
|
+
const [bearer, walletAuth] = await Promise.all([
|
|
124
|
+
createBearerToken(creds.apiKeyId, creds.apiKeySecret, method, url),
|
|
125
|
+
createWalletAuth(creds.walletSecret, method, url, body),
|
|
126
|
+
]);
|
|
127
|
+
return {
|
|
128
|
+
authorization: `Bearer ${bearer}`,
|
|
129
|
+
walletAuth,
|
|
130
|
+
};
|
|
131
|
+
}
|
package/mcp-server.mjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// wip-agent-pay MCP server
|
|
3
|
+
// Exposes agent_pay, agent_pay_x402, agent_pay_fund tools via MCP stdio transport.
|
|
4
|
+
|
|
5
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import { pay, fund, mint } from './providers/index.js';
|
|
9
|
+
|
|
10
|
+
const server = new Server(
|
|
11
|
+
{ name: 'wip-agent-pay', version: '2.0.0' },
|
|
12
|
+
{ capabilities: { tools: {} } }
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
16
|
+
tools: [
|
|
17
|
+
{
|
|
18
|
+
name: 'agent_pay',
|
|
19
|
+
description: 'Authorize a micropayment from the agent wallet. Returns a one-time self-destructing URL.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
amount: { type: 'number', description: 'Payment amount in USD' },
|
|
24
|
+
service: { type: 'string', description: 'Service identifier (e.g. "morning-stew")' },
|
|
25
|
+
note: { type: 'string', description: 'Optional note (e.g. "MS-#8")' },
|
|
26
|
+
},
|
|
27
|
+
required: ['amount', 'service'],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'agent_pay_x402',
|
|
32
|
+
description: 'Pay for a paywalled URL. The agent wallet handles payment automatically. Returns the content.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
url: { type: 'string', description: 'The paywalled URL to pay for' },
|
|
37
|
+
wallet: { type: 'string', enum: ['cdp', 'privy'], description: 'Which wallet to use (default: cdp)' },
|
|
38
|
+
},
|
|
39
|
+
required: ['url'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'agent_pay_fund',
|
|
44
|
+
description: 'Fund the agent wallet. Returns a checkout URL the user opens to pay via Apple Pay.',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
amount: { type: 'number', description: 'USD amount to fund' },
|
|
49
|
+
wallet: { type: 'string', enum: ['cdp', 'privy'], description: 'Which wallet to fund (default: cdp)' },
|
|
50
|
+
},
|
|
51
|
+
required: ['amount'],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
58
|
+
const { name, arguments: args } = request.params;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
if (name === 'agent_pay') {
|
|
62
|
+
if (typeof args.amount !== 'number' || args.amount <= 0) {
|
|
63
|
+
return { content: [{ type: 'text', text: 'amount must be a positive number' }], isError: true };
|
|
64
|
+
}
|
|
65
|
+
if (typeof args.service !== 'string' || args.service.length === 0) {
|
|
66
|
+
return { content: [{ type: 'text', text: 'service is required' }], isError: true };
|
|
67
|
+
}
|
|
68
|
+
const result = await mint(args.amount, args.service, args.note || '');
|
|
69
|
+
if (result.success) {
|
|
70
|
+
const prefix = result.demo ? '[DEMO] ' : '';
|
|
71
|
+
return { content: [{ type: 'text', text: `${prefix}Payment authorized. One-time URL: ${result.url}` }] };
|
|
72
|
+
}
|
|
73
|
+
return { content: [{ type: 'text', text: `Payment failed: ${result.error || 'unknown error'}` }], isError: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (name === 'agent_pay_x402') {
|
|
77
|
+
if (!args.url) {
|
|
78
|
+
return { content: [{ type: 'text', text: 'url is required' }], isError: true };
|
|
79
|
+
}
|
|
80
|
+
const result = await pay(args.url, { wallet: args.wallet || 'cdp' });
|
|
81
|
+
if (result.success) {
|
|
82
|
+
return { content: [{ type: 'text', text: `Paid ${result.amount} USDC on ${result.network || 'base'}. Service: ${result.service}.` }] };
|
|
83
|
+
}
|
|
84
|
+
return { content: [{ type: 'text', text: `Payment failed: ${result.error || 'unknown error'}` }], isError: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (name === 'agent_pay_fund') {
|
|
88
|
+
if (typeof args.amount !== 'number' || args.amount <= 0) {
|
|
89
|
+
return { content: [{ type: 'text', text: 'amount must be a positive number' }], isError: true };
|
|
90
|
+
}
|
|
91
|
+
const result = await fund(args.amount, { wallet: args.wallet || 'cdp' });
|
|
92
|
+
if (result.success) {
|
|
93
|
+
return { content: [{ type: 'text', text: `Checkout ready. Have the user open: ${result.checkoutUrl}` }] };
|
|
94
|
+
}
|
|
95
|
+
return { content: [{ type: 'text', text: `Funding failed: ${result.error || 'unknown error'}` }], isError: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const transport = new StdioServerTransport();
|
|
105
|
+
await server.connect(transport);
|
package/openclaw.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// wip-agent-pay OpenClaw plugin
|
|
2
|
+
// Registers agent_pay, agent_pay_x402, agent_pay_fund as tools in the agent context.
|
|
3
|
+
|
|
4
|
+
import { pay, fund, mint } from './providers/index.js';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
register(api) {
|
|
8
|
+
// --- Mint one-time URL (Mode B) ---
|
|
9
|
+
api.registerTool({
|
|
10
|
+
name: 'agent_pay',
|
|
11
|
+
label: 'Authorize Agent Payment',
|
|
12
|
+
description: 'Authorize a micropayment from the agent wallet. Returns a one-time self-destructing URL.',
|
|
13
|
+
parameters: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
amount: { type: 'number', description: 'Payment amount in USD' },
|
|
17
|
+
service: { type: 'string', description: 'Service identifier (e.g. "morning-stew")' },
|
|
18
|
+
note: { type: 'string', description: 'Optional note (e.g. "MS-#8")' },
|
|
19
|
+
},
|
|
20
|
+
required: ['amount', 'service'],
|
|
21
|
+
},
|
|
22
|
+
async execute(_id, params) {
|
|
23
|
+
try {
|
|
24
|
+
const result = await mint(params.amount, params.service, params.note || '');
|
|
25
|
+
if (result.success) {
|
|
26
|
+
const prefix = result.demo ? '[DEMO] ' : '';
|
|
27
|
+
return { content: [{ type: 'text', text: `${prefix}Payment authorized. One-time URL: ${result.url}` }] };
|
|
28
|
+
}
|
|
29
|
+
return { content: [{ type: 'text', text: `Payment failed: ${result.error || 'unknown error'}` }], isError: true };
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
}, { optional: true });
|
|
35
|
+
|
|
36
|
+
// --- Pay paywalled URL (x402) ---
|
|
37
|
+
api.registerTool({
|
|
38
|
+
name: 'agent_pay_x402',
|
|
39
|
+
label: 'Pay Paywalled URL',
|
|
40
|
+
description: 'Pay for a paywalled URL. The agent wallet handles payment automatically. Returns the content.',
|
|
41
|
+
parameters: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
url: { type: 'string', description: 'The paywalled URL to pay for' },
|
|
45
|
+
wallet: { type: 'string', enum: ['cdp', 'privy'], description: 'Which wallet to use (default: cdp)' },
|
|
46
|
+
},
|
|
47
|
+
required: ['url'],
|
|
48
|
+
},
|
|
49
|
+
async execute(_id, params) {
|
|
50
|
+
try {
|
|
51
|
+
const result = await pay(params.url, { wallet: params.wallet || 'cdp' });
|
|
52
|
+
if (result.success) {
|
|
53
|
+
return { content: [{ type: 'text', text: `Paid ${result.amount} USDC on ${result.network || 'base'}. Service: ${result.service}.` }] };
|
|
54
|
+
}
|
|
55
|
+
return { content: [{ type: 'text', text: `Payment failed: ${result.error || 'unknown error'}` }], isError: true };
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
}, { optional: true });
|
|
61
|
+
|
|
62
|
+
// --- Fund wallet (Apple Pay) ---
|
|
63
|
+
api.registerTool({
|
|
64
|
+
name: 'agent_pay_fund',
|
|
65
|
+
label: 'Fund Agent Wallet',
|
|
66
|
+
description: 'Fund the agent wallet. Returns a checkout URL the user opens to pay via Apple Pay.',
|
|
67
|
+
parameters: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
amount: { type: 'number', description: 'USD amount to fund' },
|
|
71
|
+
wallet: { type: 'string', enum: ['cdp', 'privy'], description: 'Which wallet to fund (default: cdp)' },
|
|
72
|
+
},
|
|
73
|
+
required: ['amount'],
|
|
74
|
+
},
|
|
75
|
+
async execute(_id, params) {
|
|
76
|
+
try {
|
|
77
|
+
const result = await fund(params.amount, { wallet: params.wallet || 'cdp' });
|
|
78
|
+
if (result.success) {
|
|
79
|
+
return { content: [{ type: 'text', text: `Checkout ready. Have the user open: ${result.checkoutUrl}` }] };
|
|
80
|
+
}
|
|
81
|
+
return { content: [{ type: 'text', text: `Funding failed: ${result.error || 'unknown error'}` }], isError: true };
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
}, { optional: true });
|
|
87
|
+
}
|
|
88
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "wip-agent-pay",
|
|
3
|
+
"name": "Agent Pay",
|
|
4
|
+
"description": "Disposable wallet for agents. x402 + Coinbase isolated portfolio.",
|
|
5
|
+
"activation": {
|
|
6
|
+
"onStartup": true
|
|
7
|
+
},
|
|
8
|
+
"contracts": {
|
|
9
|
+
"tools": ["agent_pay", "agent_pay_x402", "agent_pay_fund"]
|
|
10
|
+
},
|
|
11
|
+
"skills": ["./skills"],
|
|
12
|
+
"configSchema": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"properties": {}
|
|
16
|
+
}
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wip-agent-pay",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Give your agent a way to pay. AI CASH (Apple/Google Pay) + AGENT WALLET (x402).",
|
|
6
|
+
"main": "providers/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"wip-pay": "./cli.js",
|
|
9
|
+
"wip-agent-pay": "./cli.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./providers/index.js",
|
|
13
|
+
"./providers/*": "./providers/*",
|
|
14
|
+
"./mcp-server": "./mcp-server.mjs"
|
|
15
|
+
},
|
|
16
|
+
"openclaw": {
|
|
17
|
+
"extensions": [
|
|
18
|
+
"./openclaw.mjs"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node cli.js --help"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"agent",
|
|
26
|
+
"payment",
|
|
27
|
+
"coinbase",
|
|
28
|
+
"micropayments",
|
|
29
|
+
"1password",
|
|
30
|
+
"universal-interface",
|
|
31
|
+
"cloudflare"
|
|
32
|
+
],
|
|
33
|
+
"author": "Parker Todd Brooks",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/wipcomputer/wip-agent-pay.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/wipcomputer/wip-agent-pay",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
42
|
+
}
|
|
43
|
+
}
|