clawpowers 1.1.1 → 1.1.3

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.
@@ -0,0 +1,78 @@
1
+ # x402 Demo — Agent Payments in 60 Seconds
2
+
3
+ HTTP has a 402 status code that basically nobody used — until agents needed to pay.
4
+
5
+ This demo runs a local mock x402 merchant so you can see the full payment flow without any real money.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ # Terminal 1: Start the mock merchant
11
+ npx clawpowers demo x402
12
+
13
+ # Terminal 2: Hit the paid endpoint
14
+ curl http://localhost:PORT/api/premium-data
15
+ # → Returns 402 with payment requirements
16
+
17
+ # Simulate payment
18
+ curl -H "x-payment: mock-proof" http://localhost:PORT/api/premium-data
19
+ # → Returns 200 with data
20
+ ```
21
+
22
+ ## What's Happening
23
+
24
+ 1. Your agent calls a premium API
25
+ 2. The server returns **HTTP 402 Payment Required** with x402 payment requirements (amount, asset, recipient, network)
26
+ 3. ClawPowers evaluates the payment against your spending policy (limits, allowlist, mode)
27
+ 4. In **dry-run mode**: logs what would happen, no funds move
28
+ 5. In **live mode**: signs a payment proof via `agentwallet-sdk`, sends payment, retries the request
29
+ 6. Server validates payment proof and returns the data
30
+
31
+ ## The x402 Flow
32
+
33
+ ```
34
+ Agent Mock Merchant
35
+ | |
36
+ |-- GET /api/premium-data ->|
37
+ |<- 402 + requirements -----|
38
+ | |
39
+ | [evaluate policy] |
40
+ | [sign payment proof] |
41
+ | |
42
+ |-- GET + x-payment ------->|
43
+ |<- 200 + data -------------|
44
+ ```
45
+
46
+ ## Payment Requirements Format
47
+
48
+ The 402 response includes a JSON body:
49
+
50
+ ```json
51
+ {
52
+ "x402Version": 1,
53
+ "accepts": [{
54
+ "scheme": "exact",
55
+ "network": "base-sepolia",
56
+ "maxAmountRequired": "100000",
57
+ "resource": "https://localhost:PORT/api/premium-data",
58
+ "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
59
+ "payTo": "0xff86829393C6C26A4EC122bE0Cc3E466Ef876AdD"
60
+ }],
61
+ "error": "Payment Required"
62
+ }
63
+ ```
64
+
65
+ ## Check Payment Logs
66
+
67
+ After running the demo, inspect the payment decisions:
68
+
69
+ ```bash
70
+ npx clawpowers payments log
71
+ npx clawpowers payments summary
72
+ ```
73
+
74
+ ## Next Steps
75
+
76
+ - Read the [agent-payments skill](../../skills/agent-payments/SKILL.md) for full methodology
77
+ - Run `npx clawpowers payments setup` to configure spending limits
78
+ - See the [Security Model](../../README.md#security-model) for guardrails
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ // runtime/demo/x402-mock-server.js — x402 Mock Merchant Demo Server
3
+ //
4
+ // Starts a local HTTP server that demonstrates the x402 payment-required flow:
5
+ // 1. GET /api/premium-data without payment header → 402 Payment Required
6
+ // 2. GET /api/premium-data with 'x-payment: mock-proof' header → 200 OK with data
7
+ //
8
+ // This server is for DEMO and DEVELOPMENT ONLY — it does not process real payments.
9
+ //
10
+ // Usage:
11
+ // node runtime/demo/x402-mock-server.js
12
+ // npx clawpowers demo x402
13
+ //
14
+ // The server picks a random available port and prints instructions on startup.
15
+ // Press Ctrl+C to stop.
16
+ 'use strict';
17
+
18
+ const http = require('http');
19
+ const os = require('os');
20
+
21
+ // Track requests for demo visibility
22
+ let requestCount = 0;
23
+
24
+ /**
25
+ * Returns an ISO 8601 timestamp without milliseconds.
26
+ *
27
+ * @returns {string} e.g. "2026-03-22T21:42:00Z"
28
+ */
29
+ function isoTimestamp() {
30
+ return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
31
+ }
32
+
33
+ /**
34
+ * Logs a request to stdout with timestamp and key details.
35
+ * Allows the demo runner to see exactly what is happening.
36
+ *
37
+ * @param {string} method - HTTP method (GET, POST, etc.)
38
+ * @param {string} url - Request URL path
39
+ * @param {number} status - Response HTTP status code
40
+ * @param {string} [note=''] - Optional human-readable note about this request
41
+ */
42
+ function logRequest(method, url, status, note = '') {
43
+ const ts = isoTimestamp();
44
+ const noteStr = note ? ` — ${note}` : '';
45
+ console.log(` [${ts}] ${method} ${url} → ${status}${noteStr}`);
46
+ }
47
+
48
+ /**
49
+ * Builds the x402 Payment Required response body for the mock merchant.
50
+ * This mimics the response a real x402-compliant API would return.
51
+ *
52
+ * @param {number} port - The port this server is running on (embedded in the resource URL).
53
+ * @returns {object} x402 payment requirements object.
54
+ */
55
+ function buildPaymentRequired(port) {
56
+ return {
57
+ x402Version: 1,
58
+ accepts: [
59
+ {
60
+ scheme: 'exact',
61
+ network: 'base-sepolia',
62
+ maxAmountRequired: '100000',
63
+ resource: `http://localhost:${port}/api/premium-data`,
64
+ asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
65
+ payTo: '0xff86829393C6C26A4EC122bE0Cc3E466Ef876AdD',
66
+ },
67
+ ],
68
+ error: 'Payment Required',
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Builds the mock premium data response returned after (simulated) payment.
74
+ *
75
+ * @returns {object} Mock API data payload.
76
+ */
77
+ function buildPremiumData() {
78
+ return {
79
+ status: 'ok',
80
+ data: {
81
+ market: 'AGENT/USDC',
82
+ price: '4.20',
83
+ volume_24h: '1234567.89',
84
+ change_24h: '+12.3%',
85
+ source: 'mock-premium-api',
86
+ paid_with: 'x402',
87
+ timestamp: isoTimestamp(),
88
+ },
89
+ message: 'Payment accepted. Here is your premium data.',
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Main HTTP request handler for the mock x402 merchant.
95
+ *
96
+ * Routes:
97
+ * GET /api/premium-data — Returns 402 or 200 depending on x-payment header
98
+ * GET / — Returns a simple HTML help page
99
+ * All others — Returns 404
100
+ *
101
+ * @param {http.IncomingMessage} req - Incoming HTTP request
102
+ * @param {http.ServerResponse} res - HTTP response object
103
+ */
104
+ function handleRequest(req, res) {
105
+ requestCount++;
106
+ const url = req.url || '/';
107
+ const method = req.method || 'GET';
108
+
109
+ // CORS headers — allow curl and browser requests during demo
110
+ res.setHeader('Access-Control-Allow-Origin', '*');
111
+ res.setHeader('Access-Control-Allow-Headers', 'x-payment, content-type');
112
+
113
+ if (url === '/api/premium-data' && method === 'GET') {
114
+ const paymentHeader = req.headers['x-payment'];
115
+
116
+ if (!paymentHeader) {
117
+ // No payment header — return 402 Payment Required with x402 spec body
118
+ const body = JSON.stringify(buildPaymentRequired(server.address().port), null, 2);
119
+ res.setHeader('Content-Type', 'application/json');
120
+ res.writeHead(402);
121
+ res.end(body);
122
+ logRequest(method, url, 402, 'No x-payment header — returning payment requirements');
123
+ } else {
124
+ // Payment header present — simulate successful payment verification
125
+ const body = JSON.stringify(buildPremiumData(), null, 2);
126
+ res.setHeader('Content-Type', 'application/json');
127
+ res.writeHead(200);
128
+ res.end(body);
129
+ logRequest(method, url, 200, `x-payment: ${paymentHeader} — payment accepted, returning data`);
130
+ }
131
+
132
+ } else if (url === '/' && method === 'GET') {
133
+ // Help page — shows curl examples
134
+ const port = server.address().port;
135
+ const body = [
136
+ '<html><body><pre>',
137
+ 'x402 Mock Merchant Demo',
138
+ '=======================',
139
+ '',
140
+ 'Try these curl commands:',
141
+ '',
142
+ `# Step 1: Request without payment (returns 402)`,
143
+ `curl -i http://localhost:${port}/api/premium-data`,
144
+ '',
145
+ `# Step 2: Request with payment header (returns 200 + data)`,
146
+ `curl -i -H "x-payment: mock-proof" http://localhost:${port}/api/premium-data`,
147
+ '</pre></body></html>',
148
+ ].join('\n');
149
+ res.setHeader('Content-Type', 'text/html');
150
+ res.writeHead(200);
151
+ res.end(body);
152
+ logRequest(method, url, 200, 'help page');
153
+
154
+ } else {
155
+ res.setHeader('Content-Type', 'application/json');
156
+ res.writeHead(404);
157
+ res.end(JSON.stringify({ error: 'Not Found', path: url }));
158
+ logRequest(method, url, 404);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Finds a random available port by binding to port 0 and reading what the OS assigned.
164
+ * Returns a Promise that resolves to the chosen port number.
165
+ *
166
+ * @returns {Promise<number>} Available port number.
167
+ */
168
+ function getAvailablePort() {
169
+ return new Promise((resolve, reject) => {
170
+ const tmp = http.createServer();
171
+ tmp.listen(0, '127.0.0.1', () => {
172
+ const port = tmp.address().port;
173
+ tmp.close(() => resolve(port));
174
+ });
175
+ tmp.on('error', reject);
176
+ });
177
+ }
178
+
179
+ // The server object is referenced inside handleRequest for the port — declare before use
180
+ /** @type {http.Server} */
181
+ const server = http.createServer(handleRequest);
182
+
183
+ /**
184
+ * Main entry point: picks a port, starts the server, and prints instructions.
185
+ * Waits for Ctrl+C (SIGINT) or SIGTERM to shut down cleanly.
186
+ */
187
+ async function main() {
188
+ let port;
189
+ try {
190
+ port = await getAvailablePort();
191
+ } catch (_) {
192
+ // If dynamic port detection fails, fall back to a fixed high port
193
+ port = 18402;
194
+ }
195
+
196
+ server.listen(port, '127.0.0.1', () => {
197
+ console.log('');
198
+ console.log('╔══════════════════════════════════════════════════════════════╗');
199
+ console.log('║ x402 Mock Merchant — Demo Server ║');
200
+ console.log('╚══════════════════════════════════════════════════════════════╝');
201
+ console.log('');
202
+ console.log(` Mock x402 merchant running on http://localhost:${port}`);
203
+ console.log('');
204
+ console.log(' Try:');
205
+ console.log(` curl http://localhost:${port}/api/premium-data`);
206
+ console.log('');
207
+ console.log(' The server will return 402 (Payment Required) on first request.');
208
+ console.log('');
209
+ console.log(` curl -H 'x-payment: mock-proof' http://localhost:${port}/api/premium-data`);
210
+ console.log('');
211
+ console.log(" Send with header 'x-payment: mock-proof' to simulate payment.");
212
+ console.log('');
213
+ console.log(' Request log:');
214
+ });
215
+
216
+ // Handle Ctrl+C and process termination signals cleanly
217
+ const shutdown = () => {
218
+ console.log('');
219
+ console.log(` Shutting down after ${requestCount} request(s). Goodbye.`);
220
+ server.close(() => process.exit(0));
221
+ };
222
+
223
+ process.on('SIGINT', shutdown);
224
+ process.on('SIGTERM', shutdown);
225
+ }
226
+
227
+ main().catch((err) => {
228
+ process.stderr.write(`Error starting mock server: ${err.message}\n`);
229
+ process.exit(1);
230
+ });
package/runtime/init.js CHANGED
@@ -13,7 +13,7 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const os = require('os');
15
15
 
16
- const VERSION = '1.0.0';
16
+ const VERSION = '1.1.1';
17
17
 
18
18
  // Runtime root — override with CLAWPOWERS_DIR env var for testing or custom locations
19
19
  const CLAWPOWERS_DIR = process.env.CLAWPOWERS_DIR || path.join(os.homedir(), '.clawpowers');
@@ -96,6 +96,43 @@ function writeReadme() {
96
96
  }
97
97
  }
98
98
 
99
+ /**
100
+ * Default configuration written to ~/.clawpowers/config.json on first init.
101
+ * Users can edit this file to enable payments, telemetry, or change skill behavior.
102
+ * Never overwritten once created — user settings are always preserved.
103
+ */
104
+ const DEFAULT_CONFIG = {
105
+ version: VERSION,
106
+ payments: {
107
+ enabled: false,
108
+ mode: 'dry_run',
109
+ per_tx_limit_usd: 0,
110
+ daily_limit_usd: 0,
111
+ weekly_limit_usd: 0,
112
+ allowlist: [],
113
+ require_approval_above_usd: 0,
114
+ },
115
+ telemetry: {
116
+ enabled: false,
117
+ },
118
+ skills: {
119
+ auto_load: true,
120
+ },
121
+ };
122
+
123
+ /**
124
+ * Writes the default config.json to CLAWPOWERS_DIR on first initialization.
125
+ * No-op if config.json already exists — user settings are always preserved.
126
+ * The config file is written with mode 0o600 (owner read/write only).
127
+ */
128
+ function writeConfig() {
129
+ const configFile = path.join(CLAWPOWERS_DIR, 'config.json');
130
+ if (!fs.existsSync(configFile)) {
131
+ const content = JSON.stringify(DEFAULT_CONFIG, null, 2) + '\n';
132
+ fs.writeFileSync(configFile, content, { mode: 0o600 });
133
+ }
134
+ }
135
+
99
136
  /**
100
137
  * Updates the version stamp in .version after initialization.
101
138
  * Currently a no-op placeholder for actual schema migrations; the version
@@ -139,6 +176,7 @@ function main() {
139
176
  const created = createStructure();
140
177
  writeVersion();
141
178
  writeReadme();
179
+ writeConfig();
142
180
 
143
181
  // Only run migrations when .version exists (i.e., after writeVersion)
144
182
  if (fs.existsSync(path.join(CLAWPOWERS_DIR, '.version'))) {
@@ -169,4 +207,4 @@ if (require.main === module) {
169
207
  }
170
208
  }
171
209
 
172
- module.exports = { main, CLAWPOWERS_DIR, VERSION };
210
+ module.exports = { main, CLAWPOWERS_DIR, VERSION, writeConfig, DEFAULT_CONFIG };
package/runtime/init.sh CHANGED
@@ -11,7 +11,7 @@ set -euo pipefail
11
11
 
12
12
  # Runtime root — override with CLAWPOWERS_DIR env var for testing or custom locations
13
13
  CLAWPOWERS_DIR="${CLAWPOWERS_DIR:-$HOME/.clawpowers}"
14
- VERSION="1.0.0"
14
+ VERSION="1.1.1"
15
15
 
16
16
  ## === Directory Setup ===
17
17
 
@@ -93,6 +93,38 @@ EOF
93
93
  fi
94
94
  }
95
95
 
96
+ ## === Config File ===
97
+
98
+ # Writes the default config.json on first initialization.
99
+ # No-op if config.json already exists — user settings are always preserved.
100
+ # The file is written with mode 600 (owner read/write only).
101
+ write_config() {
102
+ local config_file="$CLAWPOWERS_DIR/config.json"
103
+ if [[ ! -f "$config_file" ]]; then
104
+ cat > "$config_file" << EOF
105
+ {
106
+ "version": "$VERSION",
107
+ "payments": {
108
+ "enabled": false,
109
+ "mode": "dry_run",
110
+ "per_tx_limit_usd": 0,
111
+ "daily_limit_usd": 0,
112
+ "weekly_limit_usd": 0,
113
+ "allowlist": [],
114
+ "require_approval_above_usd": 0
115
+ },
116
+ "telemetry": {
117
+ "enabled": false
118
+ },
119
+ "skills": {
120
+ "auto_load": true
121
+ }
122
+ }
123
+ EOF
124
+ chmod 600 "$config_file"
125
+ fi
126
+ }
127
+
96
128
  ## === Migrations ===
97
129
 
98
130
  # Updates the version stamp in .version to the current version.
@@ -120,6 +152,7 @@ main() {
120
152
 
121
153
  write_version
122
154
  write_readme
155
+ write_config
123
156
 
124
157
  # Migrations only apply when the version file exists (guaranteed after write_version)
125
158
  if [[ -f "$CLAWPOWERS_DIR/.version" ]]; then