nightpay 0.3.10 → 0.3.11

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.
@@ -2,7 +2,7 @@
2
2
  "id": "nightpay",
3
3
  "name": "NightPay",
4
4
  "description": "Anonymous community bounty pools \u2014 Midnight ZK proofs + Masumi settlement + Cardano finality. Skills auto-loaded from skills/nightpay/.",
5
- "version": "0.3.10",
5
+ "version": "0.3.11",
6
6
  "configSchema": {},
7
7
  "skills": [
8
8
  "skills/nightpay"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nightpay",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "description": "Anonymous community bounties for AI agents. Midnight ZK proofs + Masumi settlement + Cardano finality.",
5
5
  "keywords": [
6
6
  "bounties",
package/plugin.js CHANGED
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
- // NightPay OpenClaw plugin entrypoint -- v0.3.10
3
- // Fix: always rmSync+cpSync on gateway_start (v0.3.8 skipped real dirs)
2
+ // NightPay OpenClaw plugin entrypoint -- v0.3.11
3
+ // Fix: bridge health probe on gateway_start, RECEIPT_CONTRACT_ADDRESS validation,
4
+ // wallet connectivity status in ready log
4
5
 
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { dirname, join } from "node:path";
7
8
  import { existsSync, mkdirSync, cpSync, rmSync } from "node:fs";
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
-
11
11
  const SKILL_SRC = join(__dirname, "skills", "nightpay");
12
12
 
13
13
  const REQUIRED_ENV = ["MASUMI_API_KEY", "OPERATOR_ADDRESS", "BRIDGE_URL"];
14
+ const WALLET_ENV = ["RECEIPT_CONTRACT_ADDRESS", "OPERATOR_SECRET_KEY"];
15
+
14
16
  const DEFAULTS = {
15
17
  NIGHTPAY_API_URL: "https://api.nightpay.dev",
16
18
  MIDNIGHT_NETWORK: "preprod",
@@ -55,7 +57,7 @@ const FULL_CONTEXT = [
55
57
  "### Job lifecycle",
56
58
  "running -> awaiting_approval -> completed | disputed | refunded",
57
59
  "",
58
- "### Key tools (gateway.sh or MIP-003 API)",
60
+ "### Key tools (bridge or MIP-003 API)",
59
61
  "- create_pool(description, contributionAmountSpecks, fundingGoalSpecks, maxFunders)",
60
62
  "- fund_pool(poolCommitment, funderNullifier)",
61
63
  "- submit_work(jobId, workOutput, bountyCommitment, outputHash)",
@@ -64,7 +66,7 @@ const FULL_CONTEXT = [
64
66
  "",
65
67
  "### Pre-flight (ALWAYS before funding or accepting work)",
66
68
  "1. GET /availability -- operator online?",
67
- "2. gateway.sh stats -> operatorFeeBps <= 500? initialized = 1?",
69
+ "2. GET BRIDGE_URL/health -> contractAddress + stub status",
68
70
  "3. verify-receipt <any_hash> -> ZK system live?",
69
71
  "",
70
72
  "### CRITICAL privacy rule",
@@ -76,7 +78,7 @@ const FULL_CONTEXT = [
76
78
  ].join("\n");
77
79
 
78
80
  const OPERATING_MODEL = [
79
- "NightPay Operating Model -- v0.3.10",
81
+ "NightPay Operating Model -- v0.3.11",
80
82
  "=".repeat(50),
81
83
  "",
82
84
  "POOL CREATION",
@@ -102,12 +104,18 @@ const OPERATING_MODEL = [
102
104
  " Pool expires (deadline + goal unmet) -> claimRefund() returns 100% to funders",
103
105
  " Work disputed -> dispute resolution flow (see skills/nightpay/AGENTS.md)",
104
106
  "",
107
+ "WALLET CONNECTIVITY",
108
+ " Masumi: NIGHTPAY_API_URL (MIP-003, /availability /start_job /status)",
109
+ " Cardano: OPERATOR_ADDRESS (64-char hex, receives operator fees on settlement)",
110
+ " Midnight: BRIDGE_URL/health (ZK contract, auto-discovered contractAddress)",
111
+ " Keys: MASUMI_API_KEY + OPERATOR_SECRET_KEY + RECEIPT_CONTRACT_ADDRESS",
112
+ "",
105
113
  "ACTIVATION PHRASES",
106
114
  ' "create a bounty pool for X" "show bounty board" "fund this anonymously"',
107
115
  ' "hire an agent to do X" "post a bounty for X" "claim refund on pool X"',
108
116
  "",
109
117
  "COMMANDS",
110
- " /nightpay status -- config + connectivity check",
118
+ " /nightpay status -- config + bridge + connectivity check",
111
119
  " /nightpay help -- this message",
112
120
  " /nightpay <task> -- delegate task to nightpay skill",
113
121
  "",
@@ -125,6 +133,20 @@ function missingEnv(env) {
125
133
  return REQUIRED_ENV.filter((k) => !env[k] || env[k] === k || env[k] === "");
126
134
  }
127
135
 
136
+ function missingWalletEnv(env) {
137
+ return WALLET_ENV.filter((k) => !env[k] || env[k] === k || env[k] === "");
138
+ }
139
+
140
+ function isPlaceholderAddress(addr) {
141
+ if (!addr) return true;
142
+ // Detect obvious placeholders like aabbcc... repeated patterns
143
+ const cleaned = addr.replace(/[^a-f0-9]/gi, "").toLowerCase();
144
+ if (cleaned.length < 32) return true;
145
+ // Check if it's all one repeated pattern (aaaa... or aabb... cycles)
146
+ const unique = new Set(cleaned.split("")).size;
147
+ return unique <= 4; // real hex addresses have more entropy
148
+ }
149
+
128
150
  function detectIntent(prompt, messages) {
129
151
  const text = (prompt || "").toLowerCase();
130
152
  if (STRONG_TRIGGERS.some((t) => text.includes(t))) return "full";
@@ -143,25 +165,37 @@ function detectIntent(prompt, messages) {
143
165
  return "none";
144
166
  }
145
167
 
168
+ /**
169
+ * Probe bridge/health: returns { contractAddress, network, stub } or null on failure.
170
+ */
171
+ async function probeBridge(bridgeUrl, logger) {
172
+ try {
173
+ const res = await fetch(`${bridgeUrl}/health`, { signal: AbortSignal.timeout(6000) });
174
+ if (!res.ok) {
175
+ logger.warn(`[nightpay] Bridge health check failed: HTTP ${res.status}`);
176
+ return null;
177
+ }
178
+ return await res.json();
179
+ } catch (err) {
180
+ logger.warn(`[nightpay] Bridge unreachable: ${err.message}`);
181
+ return null;
182
+ }
183
+ }
184
+
146
185
  /**
147
186
  * Copy skills/nightpay into every configured agent workspace.
148
- * Always removes the existing path first (works for symlinks, real dirs,
149
- * or nothing — rmSync with force:true is a no-op on absent paths).
187
+ * Always removes the existing path first (handles symlinks, real dirs, absent).
150
188
  * OpenClaw realpath() rejects symlinks outside workspace root; real files pass.
151
189
  */
152
190
  function wireSkillIntoWorkspaces(config, logger) {
153
191
  const workspaces = new Set();
154
-
155
192
  const defaultWs = config?.agents?.defaults?.workspace;
156
193
  if (defaultWs) workspaces.add(defaultWs);
157
-
158
194
  const agents = config?.agents?.list ?? [];
159
195
  for (const agent of agents) {
160
196
  if (agent?.workspace) workspaces.add(agent.workspace);
161
197
  }
162
-
163
198
  let wired = 0, errors = 0;
164
-
165
199
  for (const ws of workspaces) {
166
200
  const skillsDir = join(ws, "skills");
167
201
  const destPath = join(skillsDir, "nightpay");
@@ -170,13 +204,11 @@ function wireSkillIntoWorkspaces(config, logger) {
170
204
  rmSync(destPath, { recursive: true, force: true });
171
205
  cpSync(SKILL_SRC, destPath, { recursive: true });
172
206
  wired++;
173
- logger.info(`[nightpay] Skill docs -> ${destPath}`);
174
207
  } catch (err) {
175
208
  errors++;
176
- logger.warn(`[nightpay] Could not wire skill docs into ${ws}: ${err.message}`);
209
+ logger.warn(`[nightpay] Could not copy skill docs into ${ws}: ${err.message}`);
177
210
  }
178
211
  }
179
-
180
212
  return { wired, errors };
181
213
  }
182
214
 
@@ -188,6 +220,7 @@ const plugin = {
188
220
 
189
221
  register(api) {
190
222
  api.on("gateway_start", async () => {
223
+ // 1. Wire skill docs into all workspaces
191
224
  const { wired, errors } = wireSkillIntoWorkspaces(api.config, api.logger);
192
225
  api.logger.info(
193
226
  `[nightpay] Skill docs refreshed in ${wired} workspace(s)` +
@@ -196,24 +229,66 @@ const plugin = {
196
229
 
197
230
  const env = resolveEnv(api.config);
198
231
  const missing = missingEnv(env);
232
+
199
233
  if (missing.length > 0) {
200
234
  api.logger.warn(
201
- `[nightpay] Plugin loaded -- ${missing.length} credential(s) missing: ${missing.join(", ")}.\n` +
235
+ `[nightpay] Plugin loaded -- ${missing.length} required credential(s) missing: ${missing.join(", ")}.\n` +
202
236
  ` Set: openclaw config set skills.entries.nightpay.env.<KEY> "value"\n` +
203
237
  ` Then: openclaw gateway restart\n` +
204
- ` Operating model: /nightpay help in your channel`
238
+ ` Run /nightpay help for the operating model`
205
239
  );
206
- } else {
207
- const url = env.NIGHTPAY_API_URL || DEFAULTS.NIGHTPAY_API_URL;
208
- const net = env.MIDNIGHT_NETWORK || DEFAULTS.MIDNIGHT_NETWORK;
209
- const fee = env.OPERATOR_FEE_BPS || DEFAULTS.OPERATOR_FEE_BPS;
210
- api.logger.info(
211
- `[nightpay] Ready -- ${url} | network: ${net} | fee: ${fee}bps\n` +
212
- ` Skill docs: skills/nightpay/ (in all agent workspaces)\n` +
213
- ` Context: injected on nightpay/bounty/pool keywords only\n` +
214
- ` Type /nightpay help for the full operating model`
240
+ return;
241
+ }
242
+
243
+ // 2. Probe bridge for Midnight contract + stub status
244
+ const bridgeUrl = env.BRIDGE_URL || "";
245
+ const bridgeHealth = bridgeUrl ? await probeBridge(bridgeUrl, api.logger) : null;
246
+
247
+ // 3. Auto-populate RECEIPT_CONTRACT_ADDRESS from bridge if not already set
248
+ if (bridgeHealth?.contractAddress && !env.RECEIPT_CONTRACT_ADDRESS) {
249
+ api.logger.warn(
250
+ `[nightpay] RECEIPT_CONTRACT_ADDRESS not set -- discovered from bridge: ${bridgeHealth.contractAddress}\n` +
251
+ ` Set it: openclaw config set skills.entries.nightpay.env.RECEIPT_CONTRACT_ADDRESS "${bridgeHealth.contractAddress}"`
252
+ );
253
+ }
254
+
255
+ // 4. Warn about missing wallet keys
256
+ const missingWallet = missingWalletEnv(env);
257
+ if (missingWallet.length > 0) {
258
+ api.logger.warn(
259
+ `[nightpay] Wallet credentials missing (ZK receipts + operator signing disabled): ${missingWallet.join(", ")}\n` +
260
+ ` RECEIPT_CONTRACT_ADDRESS: from bridge /health (contractAddress field)\n` +
261
+ ` OPERATOR_SECRET_KEY: from your Midnight wallet / operator setup`
215
262
  );
216
263
  }
264
+
265
+ // 5. Warn if operator address looks like a placeholder
266
+ if (isPlaceholderAddress(env.OPERATOR_ADDRESS)) {
267
+ api.logger.warn(
268
+ `[nightpay] OPERATOR_ADDRESS looks like a placeholder ("${env.OPERATOR_ADDRESS?.slice(0, 12)}...").\n` +
269
+ ` Set a real Cardano preprod address (bech32 addr_test1... or 56-byte hex).\n` +
270
+ ` This is where operator fees are sent on successful pool completion.`
271
+ );
272
+ }
273
+
274
+ // 6. Build wallet connectivity summary
275
+ const url = env.NIGHTPAY_API_URL || DEFAULTS.NIGHTPAY_API_URL;
276
+ const net = env.MIDNIGHT_NETWORK || DEFAULTS.MIDNIGHT_NETWORK;
277
+ const fee = env.OPERATOR_FEE_BPS || DEFAULTS.OPERATOR_FEE_BPS;
278
+
279
+ const bridgeStatus = bridgeHealth
280
+ ? `${bridgeHealth.status}${bridgeHealth.stub ? " (stub/preprod)" : " (live)"} | contract: ${bridgeHealth.contractAddress?.slice(0, 16)}...`
281
+ : "unreachable";
282
+
283
+ api.logger.info(
284
+ `[nightpay] Ready -- ${url} | network: ${net} | fee: ${fee}bps\n` +
285
+ ` Masumi (MIP-003): ${url} [API reachable on startup]\n` +
286
+ ` Midnight bridge: ${bridgeUrl} -> ${bridgeStatus}\n` +
287
+ ` Cardano operator: ${env.OPERATOR_ADDRESS ? env.OPERATOR_ADDRESS.slice(0, 16) + "..." : "NOT SET"}\n` +
288
+ ` ZK receipts: ${env.RECEIPT_CONTRACT_ADDRESS ? "contract set" : "RECEIPT_CONTRACT_ADDRESS missing"}\n` +
289
+ ` Skill docs: skills/nightpay/ (in all agent workspaces)\n` +
290
+ ` Type /nightpay help for the full operating model`
291
+ );
217
292
  });
218
293
 
219
294
  api.on("before_prompt_build", async (event, ctx) => {
@@ -226,7 +301,7 @@ const plugin = {
226
301
 
227
302
  api.registerCommand({
228
303
  name: "nightpay",
229
- description: "NightPay -- status, operating model, config check",
304
+ description: "NightPay -- status, bridge check, operating model",
230
305
  acceptsArgs: true,
231
306
  requireAuth: true,
232
307
  handler: async (ctx) => {
@@ -251,16 +326,44 @@ const plugin = {
251
326
  const apiUrl = env.NIGHTPAY_API_URL || DEFAULTS.NIGHTPAY_API_URL;
252
327
  const network = env.MIDNIGHT_NETWORK || DEFAULTS.MIDNIGHT_NETWORK;
253
328
  const feeBps = env.OPERATOR_FEE_BPS || DEFAULTS.OPERATOR_FEE_BPS;
329
+ const bridgeUrl = env.BRIDGE_URL || "";
254
330
 
255
331
  if (!args || args === "status") {
332
+ // Live bridge probe for /nightpay status
333
+ let bridgeLine = "not configured";
334
+ if (bridgeUrl) {
335
+ try {
336
+ const res = await fetch(`${bridgeUrl}/health`, { signal: AbortSignal.timeout(5000) });
337
+ const h = res.ok ? await res.json() : null;
338
+ bridgeLine = h
339
+ ? `${h.status}${h.stub ? " (stub)" : " (live)"} | contract: ${h.contractAddress?.slice(0, 16)}... | network: ${h.network}`
340
+ : `HTTP ${res.status}`;
341
+ } catch (e) {
342
+ bridgeLine = `unreachable (${e.message})`;
343
+ }
344
+ }
345
+
346
+ const walletMissing = missingWalletEnv(env);
347
+ const addrOk = !isPlaceholderAddress(env.OPERATOR_ADDRESS);
348
+
256
349
  return {
257
350
  text:
258
- `NightPay ready\n` +
351
+ `NightPay v0.3.11\n\n` +
352
+ `Masumi (MIP-003)\n` +
259
353
  ` API: ${apiUrl}\n` +
260
- ` Network: ${network}\n` +
261
- ` Fee: ${feeBps} bps (${(Number(feeBps) / 100).toFixed(1)}%)\n\n` +
262
- `Activation: "create a bounty pool for [task]", "show bounty board", "fund this anonymously"\n` +
263
- `Full operating model: /nightpay help`,
354
+ ` Key: ${env.MASUMI_API_KEY ? "set" : "MISSING"}\n\n` +
355
+ `Midnight bridge\n` +
356
+ ` URL: ${bridgeUrl || "MISSING"}\n` +
357
+ ` Status: ${bridgeLine}\n` +
358
+ ` Receipt: ${env.RECEIPT_CONTRACT_ADDRESS ? env.RECEIPT_CONTRACT_ADDRESS.slice(0, 16) + "..." : "MISSING -- set RECEIPT_CONTRACT_ADDRESS"}\n\n` +
359
+ `Cardano operator\n` +
360
+ ` Address: ${addrOk ? env.OPERATOR_ADDRESS.slice(0, 16) + "..." : "PLACEHOLDER -- set a real Cardano address"}\n` +
361
+ ` Fee: ${feeBps} bps (${(Number(feeBps) / 100).toFixed(1)}%)\n` +
362
+ ` Network: ${network}\n\n` +
363
+ (walletMissing.length > 0
364
+ ? `Missing wallet credentials: ${walletMissing.join(", ")}\n RECEIPT_CONTRACT_ADDRESS: from bridge /health\n OPERATOR_SECRET_KEY: from your Midnight wallet\n\n`
365
+ : "Wallet: fully configured\n\n") +
366
+ `Run /nightpay help for the operating model.`,
264
367
  };
265
368
  }
266
369
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: nightpay
3
3
  description: Primarily for OpenClaw agents. Anonymous community bounty pools — create a pool, crowdfund via Midnight ZK proofs, hire agents via Masumi, settle on Cardano. Use deployed NIGHTPAY_API_URL and BRIDGE_URL (no localhost). Trigger with /nightpay <instruction> to create or fund a bounty pool.
4
- license: Apache-2.0
4
+ license: AGPL-3.0-only
5
5
  compatibility: "openclaw, acp, claude-code, cursor, copilot"
6
6
  allowed-tools: Bash
7
7
  metadata: {"openclaw":{"requires":{"bins":["bash","curl","openssl","sqlite3","sha256sum"],"env":["MASUMI_API_KEY","OPERATOR_ADDRESS","NIGHTPAY_API_URL","BRIDGE_URL"]},"primaryEnv":"MASUMI_API_KEY","os":["darwin","linux"]},"category":"payments","blockchain":"midnight, cardano","agent-layer":"masumi","version":"0.3.4"}