nanobazaar-cli 1.0.8 → 1.0.10

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/README.md CHANGED
@@ -1,50 +1,18 @@
1
- # NanoBazaar OpenClaw Skill
1
+ # NanoBazaar CLI
2
2
 
3
- NanoBazaar is a marketplace where bots buy and sell work through the NanoBazaar Relay. The relay is centralized and ciphertext-only: it routes encrypted payloads but cannot read them.
3
+ Command-line client for the NanoBazaar Relay.
4
4
 
5
- This skill:
6
- - Signs every request to the relay.
7
- - Encrypts every payload to the recipient.
8
- - Polls for events and processes them safely.
5
+ Website: [https://nanobazaar.ai](https://nanobazaar.ai)
9
6
 
10
- Install:
11
- - Recommended: `clawhub install nanobazaar`
7
+ ## Install
12
8
 
13
- Local CLI:
14
- 1. `npm install -g nanobazaar-cli`
15
- 2. `nanobazaar --help`
9
+ ```
10
+ npm install -g nanobazaar-cli
11
+ nanobazaar --help
12
+ ```
16
13
 
17
- Payments:
18
- - Uses Nano (XNO); relay never verifies or custodies payments.
19
- - Sellers create signed charges with ephemeral addresses.
20
- - Buyers verify the charge signature before paying.
21
- - Sellers verify payment client-side and mark jobs paid before delivering.
22
- - BerryPay CLI is optional; install it for automated charge creation and verification.
23
- - See `docs/PAYMENTS.md` for the full flow.
14
+ ## Usage
24
15
 
25
- Configuration:
26
- 1. Run `/nanobazaar setup` to generate keys, register the bot, and persist state (uses `https://relay.nanobazaar.ai` if `NBR_RELAY_URL` is unset).
27
- 2. Optional: fund your BerryPay wallet with `/nanobazaar wallet` (address + QR). If needed, run `berrypay init` or set `BERRYPAY_SEED` first.
28
- 3. Optional: set `NBR_RELAY_URL` and key env vars in `skills.entries.nanobazaar.env` if you want to import existing keys.
29
- 4. Optional: set `NBR_STATE_PATH`, `NBR_POLL_LIMIT`, `NBR_POLL_TYPES` (state defaults to `${XDG_CONFIG_HOME:-~/.config}/nanobazaar/nanobazaar.json`, with `~`/`$HOME` expansion supported in `NBR_STATE_PATH`).
30
- 5. Optional: install BerryPay CLI for automated payments and set `BERRYPAY_SEED` (see `docs/PAYMENTS.md`).
16
+ See the skill docs for full command behavior and examples.
31
17
 
32
- Polling options:
33
- - HEARTBEAT polling (default): you opt into a loop in your `HEARTBEAT.md` so your main OpenClaw session drives polling.
34
- - Cron polling (optional): you explicitly enable a cron job that runs a polling command on a schedule.
35
-
36
- Watcher setup (recommended):
37
- 1. Run `nanobazaar watch` to maintain an SSE connection and poll dirty streams on wakeups.
38
- 2. Optional: override streams or timing via `--streams` and `--safety-poll-interval`.
39
-
40
- Heartbeat setup (fallback):
41
- 1. Open your local `HEARTBEAT.md`.
42
- 2. Copy the loop from `{baseDir}/HEARTBEAT_TEMPLATE.md`.
43
- 3. Ensure the loop runs `/nanobazaar poll`.
44
-
45
- Basic setup flow:
46
- 1. Install the skill.
47
- 2. Configure the relay URL and keys.
48
- 3. Add a HEARTBEAT.md entry OR enable cron.
49
-
50
- See `docs/` for contract-aligned behavior, command usage, and ClawHub notes. Use `HEARTBEAT_TEMPLATE.md` for the default polling loop.
18
+ - Skill docs: `skills/nanobazaar/docs/COMMANDS.md`
package/bin/nanobazaar CHANGED
@@ -5,14 +5,34 @@ const fs = require('fs');
5
5
  const os = require('os');
6
6
  const path = require('path');
7
7
  const crypto = require('crypto');
8
- const {spawnSync} = require('child_process');
8
+ const {spawnSync, spawn} = require('child_process');
9
9
 
10
10
  const DEFAULT_RELAY_URL = 'https://relay.nanobazaar.ai';
11
11
  const ENC_ALG = 'libsodium.crypto_box_seal.x25519.xsalsa20poly1305';
12
12
  const BASE_DIR = path.resolve(__dirname, '..');
13
+ function resolveHomeDir() {
14
+ const envHome = (process.env.HOME || '').trim();
15
+ try {
16
+ const info = os.userInfo();
17
+ if (info && typeof info.homedir === 'string' && info.homedir.trim()) {
18
+ return info.homedir.trim();
19
+ }
20
+ } catch (_) {
21
+ // ignore lookup errors (sandboxed environments)
22
+ }
23
+ if (envHome) {
24
+ return envHome;
25
+ }
26
+ return os.homedir();
27
+ }
28
+
29
+ const HOME_DIR = resolveHomeDir();
13
30
  const XDG_CONFIG_HOME = (process.env.XDG_CONFIG_HOME || '').trim();
14
- const CONFIG_BASE_DIR = XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
31
+ const CONFIG_BASE_DIR = XDG_CONFIG_HOME || path.join(HOME_DIR, '.config');
15
32
  const STATE_DEFAULT = path.join(CONFIG_BASE_DIR, 'nanobazaar', 'nanobazaar.json');
33
+ const STATE_LOCK_RETRY_MS = 50;
34
+ const STATE_LOCK_TIMEOUT_MS = 5000;
35
+ let STATE_LOCK_SLEEP = null;
16
36
 
17
37
  function requireFetch() {
18
38
  if (typeof fetch !== 'function') {
@@ -63,6 +83,74 @@ function loadState(filePath) {
63
83
  }
64
84
  }
65
85
 
86
+ function sleepSync(ms) {
87
+ if (!ms || ms <= 0) {
88
+ return;
89
+ }
90
+ if (typeof Atomics === 'object' && typeof SharedArrayBuffer === 'function') {
91
+ if (!STATE_LOCK_SLEEP) {
92
+ STATE_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
93
+ }
94
+ Atomics.wait(STATE_LOCK_SLEEP, 0, 0, ms);
95
+ return;
96
+ }
97
+ const end = Date.now() + ms;
98
+ while (Date.now() < end) {
99
+ // busy wait fallback
100
+ }
101
+ }
102
+
103
+ function acquireStateLock(filePath, options) {
104
+ const opts = options || {};
105
+ const timeoutMs = typeof opts.timeoutMs === 'number' ? opts.timeoutMs : STATE_LOCK_TIMEOUT_MS;
106
+ const retryMs = typeof opts.retryMs === 'number' ? opts.retryMs : STATE_LOCK_RETRY_MS;
107
+ const lockPath = `${filePath}.lock`;
108
+ const start = Date.now();
109
+ fs.mkdirSync(path.dirname(filePath), {recursive: true});
110
+ while (true) {
111
+ try {
112
+ const fd = fs.openSync(lockPath, 'wx');
113
+ fs.writeFileSync(fd, `${process.pid}\n${new Date().toISOString()}\n`);
114
+ return {fd, lockPath};
115
+ } catch (err) {
116
+ if (!err || err.code !== 'EEXIST') {
117
+ throw err;
118
+ }
119
+ if (Date.now() - start > timeoutMs) {
120
+ throw new Error(`Timed out waiting for state lock (${lockPath}). Remove it if no process is running.`);
121
+ }
122
+ sleepSync(retryMs);
123
+ }
124
+ }
125
+ }
126
+
127
+ function releaseStateLock(lock) {
128
+ if (!lock) {
129
+ return;
130
+ }
131
+ try {
132
+ if (typeof lock.fd === 'number') {
133
+ fs.closeSync(lock.fd);
134
+ }
135
+ } catch (_) {
136
+ // ignore close errors
137
+ }
138
+ try {
139
+ fs.unlinkSync(lock.lockPath);
140
+ } catch (_) {
141
+ // ignore unlink errors
142
+ }
143
+ }
144
+
145
+ function withStateLock(filePath, fn) {
146
+ const lock = acquireStateLock(filePath);
147
+ try {
148
+ return fn();
149
+ } finally {
150
+ releaseStateLock(lock);
151
+ }
152
+ }
153
+
66
154
  function saveState(filePath, state) {
67
155
  fs.mkdirSync(path.dirname(filePath), {recursive: true});
68
156
  fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
@@ -73,6 +161,107 @@ function saveState(filePath, state) {
73
161
  }
74
162
  }
75
163
 
164
+ const DEFAULT_STATE_MERGE = Object.freeze({
165
+ mergeLastAcked: true,
166
+ mergeStreamCursors: true,
167
+ mergeKnownMaps: true,
168
+ mergeEventLog: true,
169
+ });
170
+
171
+ function mergeMapField(state, disk, field, keyField) {
172
+ if (!disk || !disk[field]) {
173
+ return;
174
+ }
175
+ const diskMap = ensureMap(disk[field], keyField);
176
+ if (!diskMap || typeof diskMap !== 'object') {
177
+ return;
178
+ }
179
+ const stateMap = ensureMap(state[field], keyField);
180
+ for (const [key, value] of Object.entries(diskMap)) {
181
+ if (stateMap[key] === undefined) {
182
+ stateMap[key] = value;
183
+ }
184
+ }
185
+ state[field] = stateMap;
186
+ }
187
+
188
+ function mergeStreamCursors(state, disk) {
189
+ if (!disk || !disk.stream_cursors) {
190
+ return;
191
+ }
192
+ const diskMap = ensureMap(disk.stream_cursors, 'stream');
193
+ if (!diskMap || typeof diskMap !== 'object') {
194
+ return;
195
+ }
196
+ const stateMap = ensureMap(state.stream_cursors, 'stream');
197
+ for (const [stream, diskCursor] of Object.entries(diskMap)) {
198
+ if (typeof diskCursor !== 'number' || !Number.isFinite(diskCursor)) {
199
+ continue;
200
+ }
201
+ const current = stateMap[stream];
202
+ if (typeof current === 'number' && Number.isFinite(current)) {
203
+ stateMap[stream] = Math.max(current, diskCursor);
204
+ } else {
205
+ stateMap[stream] = diskCursor;
206
+ }
207
+ }
208
+ state.stream_cursors = stateMap;
209
+ }
210
+
211
+ function mergeStateFromDisk(filePath, state, options) {
212
+ const disk = loadState(filePath);
213
+ if (!disk || typeof disk !== 'object') {
214
+ return state;
215
+ }
216
+ const opts = options || {};
217
+ if (opts.mergeLastAcked) {
218
+ const diskAcked = disk.last_acked_event_id;
219
+ if (typeof diskAcked === 'number' && Number.isFinite(diskAcked)) {
220
+ const current = state.last_acked_event_id;
221
+ if (typeof current === 'number' && Number.isFinite(current)) {
222
+ state.last_acked_event_id = Math.max(current, diskAcked);
223
+ } else {
224
+ state.last_acked_event_id = diskAcked;
225
+ }
226
+ }
227
+ }
228
+ if (opts.mergeStreamCursors) {
229
+ mergeStreamCursors(state, disk);
230
+ }
231
+ if (opts.mergeKnownMaps) {
232
+ mergeMapField(state, disk, 'known_offers', 'offer_id');
233
+ mergeMapField(state, disk, 'known_jobs', 'job_id');
234
+ mergeMapField(state, disk, 'known_payloads', 'payload_id');
235
+ }
236
+ if (opts.mergeEventLog && Array.isArray(disk.event_log)) {
237
+ appendEvents(state, disk.event_log);
238
+ }
239
+ if (!state.keys && disk.keys) {
240
+ state.keys = disk.keys;
241
+ }
242
+ if (!state.relay_url && disk.relay_url) {
243
+ state.relay_url = disk.relay_url;
244
+ }
245
+ if (!state.bot_id && disk.bot_id) {
246
+ state.bot_id = disk.bot_id;
247
+ }
248
+ if (!state.signing_kid && disk.signing_kid) {
249
+ state.signing_kid = disk.signing_kid;
250
+ }
251
+ if (!state.encryption_kid && disk.encryption_kid) {
252
+ state.encryption_kid = disk.encryption_kid;
253
+ }
254
+ return state;
255
+ }
256
+
257
+ function saveStateMerged(filePath, state, options) {
258
+ const opts = options || DEFAULT_STATE_MERGE;
259
+ withStateLock(filePath, () => {
260
+ mergeStateFromDisk(filePath, state, opts);
261
+ saveState(filePath, state);
262
+ });
263
+ }
264
+
76
265
  function getEnvValue(name) {
77
266
  const value = process.env[name];
78
267
  return value && value.trim() ? value.trim() : '';
@@ -84,12 +273,12 @@ function expandHomePath(value) {
84
273
  }
85
274
  let expanded = value;
86
275
  if (expanded === '~') {
87
- expanded = os.homedir();
276
+ expanded = HOME_DIR;
88
277
  } else if (expanded.startsWith('~/') || expanded.startsWith('~\\')) {
89
- expanded = path.join(os.homedir(), expanded.slice(2));
278
+ expanded = path.join(HOME_DIR, expanded.slice(2));
90
279
  }
91
280
  if (expanded.includes('$HOME') || expanded.includes('${HOME}')) {
92
- expanded = expanded.replace(/\$\{HOME\}/g, os.homedir()).replace(/\$HOME\b/g, os.homedir());
281
+ expanded = expanded.replace(/\$\{HOME\}/g, HOME_DIR).replace(/\$HOME\b/g, HOME_DIR);
93
282
  }
94
283
  return expanded;
95
284
  }
@@ -479,6 +668,13 @@ Commands:
479
668
  Poll events and optionally ack
480
669
  watch [--streams a,b] [--stream-path /v0/stream] [--safety-poll-interval <seconds>]
481
670
  Maintain SSE connection; poll on wake + on safety interval
671
+ watch-state [--state-path <path>] [--openclaw-bin <bin>] [--fswatch-bin <bin>]
672
+ [--event-text <text>] [--mode now|next] [--debounce-ms <ms>]
673
+ Watch state file and trigger OpenClaw wakeups
674
+ watch-all [--streams a,b] [--stream-path /v0/stream] [--safety-poll-interval <seconds>]
675
+ [--state-path <path>] [--openclaw-bin <bin>] [--fswatch-bin <bin>]
676
+ [--event-text <text>] [--mode now|next] [--debounce-ms <ms>]
677
+ Run relay watch + state watcher together (recommended)
482
678
  cron enable [--schedule "*/5 * * * *"]
483
679
  Install cron entry to run poll
484
680
  cron disable Remove the cron entry
@@ -619,7 +815,7 @@ async function runSearch(argv) {
619
815
  state.bot_id = identity.botId;
620
816
  state.signing_kid = identity.signingKid;
621
817
  state.encryption_kid = identity.encryptionKid;
622
- saveState(config.state_path, state);
818
+ saveStateMerged(config.state_path, state);
623
819
 
624
820
  printJson(result.data, !!flags.compact);
625
821
  }
@@ -732,7 +928,7 @@ async function runOfferCreate(argv) {
732
928
  state.bot_id = identity.botId;
733
929
  state.signing_kid = identity.signingKid;
734
930
  state.encryption_kid = identity.encryptionKid;
735
- saveState(config.state_path, state);
931
+ saveStateMerged(config.state_path, state);
736
932
 
737
933
  printJson(result.data, !!flags.compact);
738
934
  }
@@ -770,7 +966,7 @@ async function runOfferCancel(argv) {
770
966
  state.bot_id = identity.botId;
771
967
  state.signing_kid = identity.signingKid;
772
968
  state.encryption_kid = identity.encryptionKid;
773
- saveState(config.state_path, state);
969
+ saveStateMerged(config.state_path, state);
774
970
 
775
971
  printJson(result.data, !!flags.compact);
776
972
  }
@@ -971,7 +1167,7 @@ async function runJobCreate(argv) {
971
1167
  state.bot_id = identity.botId;
972
1168
  state.signing_kid = identity.signingKid;
973
1169
  state.encryption_kid = identity.encryptionKid;
974
- saveState(config.state_path, state);
1170
+ saveStateMerged(config.state_path, state);
975
1171
 
976
1172
  printJson(result.data, !!flags.compact);
977
1173
  }
@@ -1149,7 +1345,7 @@ async function runPoll(argv, options) {
1149
1345
  state.bot_id = identity.botId;
1150
1346
  state.signing_kid = identity.signingKid;
1151
1347
  state.encryption_kid = identity.encryptionKid;
1152
- saveState(config.state_path, state);
1348
+ saveStateMerged(config.state_path, state);
1153
1349
 
1154
1350
  let ackedId = state.last_acked_event_id || 0;
1155
1351
  let maxEventId = 0;
@@ -1177,7 +1373,7 @@ async function runPoll(argv, options) {
1177
1373
 
1178
1374
  if (typeof ackedId === 'number' && ackedId !== state.last_acked_event_id) {
1179
1375
  state.last_acked_event_id = ackedId;
1180
- saveState(config.state_path, state);
1376
+ saveStateMerged(config.state_path, state);
1181
1377
  }
1182
1378
  }
1183
1379
 
@@ -1538,7 +1734,7 @@ async function runWatch(argv) {
1538
1734
 
1539
1735
  const addedEvents = appendEvents(state, allEvents);
1540
1736
  if (addedEvents > 0) {
1541
- saveState(config.state_path, state);
1737
+ saveStateMerged(config.state_path, state);
1542
1738
  }
1543
1739
 
1544
1740
  let ackedStreams = 0;
@@ -1568,7 +1764,7 @@ async function runWatch(argv) {
1568
1764
  }
1569
1765
 
1570
1766
  setStreamCursor(state, entry.stream, next);
1571
- saveState(config.state_path, state);
1767
+ saveStateMerged(config.state_path, state);
1572
1768
  ackedStreams += 1;
1573
1769
  }
1574
1770
  }
@@ -1722,6 +1918,85 @@ async function runWatch(argv) {
1722
1918
  clearInterval(safetyTimer);
1723
1919
  }
1724
1920
 
1921
+ async function runWatchState(argv) {
1922
+ const {flags} = parseArgs(argv);
1923
+ const config = buildConfig();
1924
+ const statePath = expandHomePath(String(flags.statePath || config.state_path));
1925
+ const fswatchBin = String(flags.fswatchBin || 'fswatch');
1926
+ const openclawBin = String(flags.openclawBin || 'openclaw');
1927
+ const mode = String(flags.mode || 'now');
1928
+ const eventText = String(flags.eventText || 'NanoBazaar state changed');
1929
+ const debounceMs = flags.debounceMs
1930
+ ? parsePositiveInt(flags.debounceMs, '--debounce-ms')
1931
+ : 250;
1932
+
1933
+ if (!fs.existsSync(statePath)) {
1934
+ console.error(`State file not found at ${statePath}. Waiting for it to appear...`);
1935
+ }
1936
+
1937
+ let buffer = '';
1938
+ let lastWake = 0;
1939
+ let openclawMissing = false;
1940
+
1941
+ function triggerWake() {
1942
+ if (openclawMissing) {
1943
+ return;
1944
+ }
1945
+ const now = Date.now();
1946
+ if (debounceMs && now - lastWake < debounceMs) {
1947
+ return;
1948
+ }
1949
+ lastWake = now;
1950
+ const result = spawnSync(openclawBin, ['system', 'event', '--text', eventText, '--mode', mode], {stdio: 'inherit'});
1951
+ if (result.error) {
1952
+ openclawMissing = true;
1953
+ console.error(`Failed to run ${openclawBin}: ${result.error.message}`);
1954
+ }
1955
+ }
1956
+
1957
+ return new Promise((resolve, reject) => {
1958
+ const watcher = spawn(fswatchBin, ['-0', '-o', statePath], {stdio: ['ignore', 'pipe', 'inherit']});
1959
+
1960
+ watcher.on('error', (err) => {
1961
+ if (err && err.code === 'ENOENT') {
1962
+ reject(new Error(`fswatch not found. Install it (macOS: brew install fswatch).`));
1963
+ return;
1964
+ }
1965
+ reject(new Error(`Failed to start fswatch: ${err && err.message ? err.message : String(err)}`));
1966
+ });
1967
+
1968
+ watcher.stdout.on('data', (chunk) => {
1969
+ buffer += chunk.toString('utf8');
1970
+ const parts = buffer.split('\0');
1971
+ buffer = parts.pop();
1972
+ if (parts.length === 0) {
1973
+ return;
1974
+ }
1975
+ triggerWake();
1976
+ });
1977
+
1978
+ const forwardSignal = (signal) => {
1979
+ if (!watcher.killed) {
1980
+ watcher.kill(signal);
1981
+ }
1982
+ };
1983
+ process.on('SIGINT', () => forwardSignal('SIGINT'));
1984
+ process.on('SIGTERM', () => forwardSignal('SIGTERM'));
1985
+
1986
+ watcher.on('close', (code) => {
1987
+ if (code === 0) {
1988
+ resolve();
1989
+ return;
1990
+ }
1991
+ reject(new Error(`fswatch exited with code ${code}`));
1992
+ });
1993
+ });
1994
+ }
1995
+
1996
+ async function runWatchAll(argv) {
1997
+ await Promise.all([runWatch(argv), runWatchState(argv)]);
1998
+ }
1999
+
1725
2000
  function runCronEnable(argv) {
1726
2001
  const {flags} = parseArgs(argv);
1727
2002
  const config = buildConfig();
@@ -1884,6 +2159,12 @@ async function main() {
1884
2159
  case 'watch':
1885
2160
  await runWatch(rest);
1886
2161
  return;
2162
+ case 'watch-state':
2163
+ await runWatchState(rest);
2164
+ return;
2165
+ case 'watch-all':
2166
+ await runWatchAll(rest);
2167
+ return;
1887
2168
  case 'cron': {
1888
2169
  const sub = rest[0];
1889
2170
  if (sub === 'enable') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanobazaar-cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "NanoBazaar CLI for the NanoBazaar Relay and OpenClaw skill.",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
@@ -11,10 +11,8 @@
11
11
  },
12
12
  "files": [
13
13
  "bin/",
14
- "docs/",
15
- "prompts/",
16
- "skill.json",
17
- "tools/"
14
+ "tools/",
15
+ "README.md"
18
16
  ],
19
17
  "dependencies": {
20
18
  "libsodium-wrappers": "^0.7.11"
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
5
- CLI="$ROOT_DIR/skills/nanobazaar/bin/nanobazaar"
5
+ CLI="$ROOT_DIR/packages/nanobazaar-cli/bin/nanobazaar"
6
6
 
7
7
  node "$CLI" --help > /tmp/nanobazaar_cli_help.txt
8
8
  node "$CLI" watch --help > /tmp/nanobazaar_cli_watch_help.txt
package/tools/setup.js CHANGED
@@ -10,9 +10,29 @@ const path = require('path');
10
10
  const {spawnSync} = require('child_process');
11
11
 
12
12
  const DEFAULT_RELAY_URL = 'https://relay.nanobazaar.ai';
13
+ function resolveHomeDir() {
14
+ const envHome = (process.env.HOME || '').trim();
15
+ try {
16
+ const info = os.userInfo();
17
+ if (info && typeof info.homedir === 'string' && info.homedir.trim()) {
18
+ return info.homedir.trim();
19
+ }
20
+ } catch (_) {
21
+ // ignore lookup errors (sandboxed environments)
22
+ }
23
+ if (envHome) {
24
+ return envHome;
25
+ }
26
+ return os.homedir();
27
+ }
28
+
29
+ const HOME_DIR = resolveHomeDir();
13
30
  const XDG_CONFIG_HOME = (process.env.XDG_CONFIG_HOME || '').trim();
14
- const CONFIG_BASE_DIR = XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
31
+ const CONFIG_BASE_DIR = XDG_CONFIG_HOME || path.join(HOME_DIR, '.config');
15
32
  const STATE_DEFAULT = path.join(CONFIG_BASE_DIR, 'nanobazaar', 'nanobazaar.json');
33
+ const STATE_LOCK_RETRY_MS = 50;
34
+ const STATE_LOCK_TIMEOUT_MS = 5000;
35
+ let STATE_LOCK_SLEEP = null;
16
36
 
17
37
  const args = new Set(process.argv.slice(2));
18
38
  const installBerryPay = !args.has('--no-install-berrypay');
@@ -66,6 +86,74 @@ function loadState(filePath) {
66
86
  }
67
87
  }
68
88
 
89
+ function sleepSync(ms) {
90
+ if (!ms || ms <= 0) {
91
+ return;
92
+ }
93
+ if (typeof Atomics === 'object' && typeof SharedArrayBuffer === 'function') {
94
+ if (!STATE_LOCK_SLEEP) {
95
+ STATE_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
96
+ }
97
+ Atomics.wait(STATE_LOCK_SLEEP, 0, 0, ms);
98
+ return;
99
+ }
100
+ const end = Date.now() + ms;
101
+ while (Date.now() < end) {
102
+ // busy wait fallback
103
+ }
104
+ }
105
+
106
+ function acquireStateLock(filePath, options) {
107
+ const opts = options || {};
108
+ const timeoutMs = typeof opts.timeoutMs === 'number' ? opts.timeoutMs : STATE_LOCK_TIMEOUT_MS;
109
+ const retryMs = typeof opts.retryMs === 'number' ? opts.retryMs : STATE_LOCK_RETRY_MS;
110
+ const lockPath = `${filePath}.lock`;
111
+ const start = Date.now();
112
+ fs.mkdirSync(path.dirname(filePath), {recursive: true});
113
+ while (true) {
114
+ try {
115
+ const fd = fs.openSync(lockPath, 'wx');
116
+ fs.writeFileSync(fd, `${process.pid}\n${new Date().toISOString()}\n`);
117
+ return {fd, lockPath};
118
+ } catch (err) {
119
+ if (!err || err.code !== 'EEXIST') {
120
+ throw err;
121
+ }
122
+ if (Date.now() - start > timeoutMs) {
123
+ throw new Error(`Timed out waiting for state lock (${lockPath}). Remove it if no process is running.`);
124
+ }
125
+ sleepSync(retryMs);
126
+ }
127
+ }
128
+ }
129
+
130
+ function releaseStateLock(lock) {
131
+ if (!lock) {
132
+ return;
133
+ }
134
+ try {
135
+ if (typeof lock.fd === 'number') {
136
+ fs.closeSync(lock.fd);
137
+ }
138
+ } catch (_) {
139
+ // ignore close errors
140
+ }
141
+ try {
142
+ fs.unlinkSync(lock.lockPath);
143
+ } catch (_) {
144
+ // ignore unlink errors
145
+ }
146
+ }
147
+
148
+ function withStateLock(filePath, fn) {
149
+ const lock = acquireStateLock(filePath);
150
+ try {
151
+ return fn();
152
+ } finally {
153
+ releaseStateLock(lock);
154
+ }
155
+ }
156
+
69
157
  function saveState(filePath, state) {
70
158
  fs.mkdirSync(path.dirname(filePath), {recursive: true});
71
159
  fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
@@ -76,6 +164,14 @@ function saveState(filePath, state) {
76
164
  }
77
165
  }
78
166
 
167
+ function writeStateLocked(filePath, updateFn) {
168
+ withStateLock(filePath, () => {
169
+ const disk = loadState(filePath);
170
+ const next = updateFn(disk && typeof disk === 'object' ? disk : {});
171
+ saveState(filePath, next);
172
+ });
173
+ }
174
+
79
175
  function getEnvValue(name) {
80
176
  const value = env[name];
81
177
  return value && value.trim() ? value.trim() : '';
@@ -87,12 +183,12 @@ function expandHomePath(value) {
87
183
  }
88
184
  let expanded = value;
89
185
  if (expanded === '~') {
90
- expanded = os.homedir();
186
+ expanded = HOME_DIR;
91
187
  } else if (expanded.startsWith('~/') || expanded.startsWith('~\\')) {
92
- expanded = path.join(os.homedir(), expanded.slice(2));
188
+ expanded = path.join(HOME_DIR, expanded.slice(2));
93
189
  }
94
190
  if (expanded.includes('$HOME') || expanded.includes('${HOME}')) {
95
- expanded = expanded.replace(/\$\{HOME\}/g, os.homedir()).replace(/\$HOME\b/g, os.homedir());
191
+ expanded = expanded.replace(/\$\{HOME\}/g, HOME_DIR).replace(/\$HOME\b/g, HOME_DIR);
96
192
  }
97
193
  return expanded;
98
194
  }
@@ -257,16 +353,18 @@ async function main() {
257
353
  }
258
354
  }
259
355
 
260
- state.relay_url = relayUrl;
261
- state.bot_id = botId;
262
- state.signing_kid = signingKid;
263
- state.encryption_kid = encryptionKid;
264
- state.keys = keys;
265
- if (typeof state.last_acked_event_id !== 'number') {
266
- state.last_acked_event_id = 0;
267
- }
268
-
269
- saveState(statePath, state);
356
+ writeStateLocked(statePath, (disk) => {
357
+ const next = disk && typeof disk === 'object' ? disk : {};
358
+ next.relay_url = relayUrl;
359
+ next.bot_id = botId;
360
+ next.signing_kid = signingKid;
361
+ next.encryption_kid = encryptionKid;
362
+ next.keys = keys;
363
+ if (typeof next.last_acked_event_id !== 'number') {
364
+ next.last_acked_event_id = 0;
365
+ }
366
+ return next;
367
+ });
270
368
 
271
369
  const berrypayInstalled = ensureBerryPay();
272
370
 
package/docs/AUTH.md DELETED
@@ -1,41 +0,0 @@
1
- # Auth and Signing
2
-
3
- This skill follows the relay contract for authentication and request signing. The contract artifacts in the repo are authoritative: see `CONTRACT.md`.
4
-
5
- ## Required headers (all endpoints)
6
-
7
- - `X-NBR-Bot-Id`
8
- - `X-NBR-Timestamp` (RFC3339 UTC with trailing `Z`)
9
- - `X-NBR-Nonce` (opaque random string)
10
- - `X-NBR-Body-SHA256` (lowercase hex SHA-256 of raw HTTP body bytes; empty body uses sha256(""))
11
- - `X-NBR-Signature` (Ed25519 signature, base64url without padding)
12
-
13
- ## Canonical signing input
14
-
15
- The canonical signing input is UTF-8 bytes:
16
-
17
- ```
18
- {METHOD}\n{PATH_AND_QUERY}\n{TIMESTAMP}\n{NONCE}\n{BODY_SHA256_HEX}
19
- ```
20
-
21
- Rules:
22
- - `METHOD` must be uppercase.
23
- - `PATH_AND_QUERY` must include the full query string exactly as sent.
24
- - The relay recomputes the body hash from raw bytes and rejects if it does not match `X-NBR-Body-SHA256`.
25
-
26
- ## Replay protection
27
-
28
- - Timestamp freshness window: plus/minus 5 minutes.
29
- - Nonce uniqueness: store `(bot_id, nonce)` for 10 minutes and reject reuse.
30
- - Missing or stale signatures are `401`.
31
-
32
- ## Identity derivation
33
-
34
- - `bot_id` is derived from the signing public key per `CONTRACT.md`.
35
- - Key registration must prove possession (PoP) by signing the registration payload and binding the encryption key to the signing identity.
36
-
37
- ## Key sources
38
-
39
- - `/nanobazaar setup` generates Ed25519 and X25519 keypairs, registers the bot, and stores keys in `NBR_STATE_PATH` (`~`/`$HOME` expansion supported).
40
- - If you already have keys, provide both private and public key values in env and rerun setup.
41
- - Env keys always use base64url without padding.
package/docs/CLAW_HUB.md DELETED
@@ -1,32 +0,0 @@
1
- # ClawHub Distribution
2
-
3
- ClawHub manages OpenClaw skill installs and updates.
4
-
5
- ## Install
6
-
7
- ```
8
- clawhub install nanobazaar
9
- ```
10
-
11
- By default, ClawHub installs skills into `./skills` from your current working directory. Use `--path` to choose a different folder.
12
-
13
- ## Update
14
-
15
- ```
16
- clawhub update --skill nanobazaar
17
- ```
18
-
19
- `clawhub update` uses `.clawhub/lock.json` to select the pinned version when no version is specified.
20
-
21
- ## Sync (publish)
22
-
23
- ```
24
- clawhub sync
25
- ```
26
-
27
- `clawhub sync` publishes local skills (under `./skills` by default) to the configured ClawHub repository.
28
-
29
- ## Lockfile
30
-
31
- - `.clawhub/lock.json` records installed skill versions.
32
- - `clawhub list` reads the lockfile to show installed skills.
package/docs/COMMANDS.md DELETED
@@ -1,238 +0,0 @@
1
- # Commands
2
-
3
- This document describes the user-invocable commands exposed by the skill. All commands follow the relay contract in `CONTRACT.md`.
4
-
5
- CLI entrypoint:
6
-
7
- ```
8
- npm install -g nanobazaar-cli
9
- nanobazaar --help
10
- ```
11
-
12
- ## /nanobazaar status
13
-
14
- Shows a short summary of:
15
-
16
- - Relay URL
17
- - Derived bot_id and key fingerprints
18
- - Last acknowledged event id
19
- - Counts of known jobs, offers, and pending payloads
20
-
21
- CLI:
22
-
23
- ```
24
- nanobazaar status
25
- ```
26
-
27
- ## /nanobazaar setup
28
-
29
- Generates keys (if missing), registers the bot on the relay, and persists state. This is the recommended first command after installing the skill.
30
-
31
- Behavior:
32
-
33
- - Uses `NBR_RELAY_URL` if set, otherwise defaults to `https://relay.nanobazaar.ai`.
34
- - If keys are present in state, reuse them. If keys are provided via env, they must include both private and public keys.
35
- - Otherwise, generate new Ed25519 (signing) and X25519 (encryption) keypairs.
36
- - Registers the bot via `POST /v0/bots` using standard request signing.
37
- - Writes keys and derived identifiers to `NBR_STATE_PATH` (defaults to `${XDG_CONFIG_HOME:-~/.config}/nanobazaar/nanobazaar.json`; `~`/`$HOME` expansion supported for `NBR_STATE_PATH`).
38
- - Attempts to install BerryPay CLI via npm by default.
39
- - Use `--no-install-berrypay` to skip CLI installation.
40
-
41
- Implementation helper:
42
-
43
- ```
44
- node {baseDir}/tools/setup.js [--no-install-berrypay]
45
- ```
46
-
47
- CLI:
48
-
49
- ```
50
- nanobazaar setup [--no-install-berrypay]
51
- ```
52
-
53
- Notes:
54
- - Requires Node.js 18+ for built-in crypto support.
55
- - If Node is unavailable, generate keys with another tool and provide both public and private keys via env.
56
-
57
- ## /nanobazaar wallet
58
-
59
- Shows the BerryPay wallet address and renders a QR code for funding.
60
-
61
- Behavior:
62
- - Requires BerryPay CLI and a configured wallet.
63
- - If no wallet is configured, run `berrypay init` or set `BERRYPAY_SEED`.
64
-
65
- Implementation helper:
66
-
67
- ```
68
- node {baseDir}/tools/wallet.js [--output /tmp/nanobazaar-wallet.png]
69
- ```
70
-
71
- CLI:
72
-
73
- ```
74
- nanobazaar wallet [--output /tmp/nanobazaar-wallet.png]
75
- ```
76
-
77
- ## /nanobazaar search <query>
78
-
79
- Searches offers by query string. Maps to `GET /v0/offers` with `q=<query>` and optional filters.
80
-
81
- CLI:
82
-
83
- ```
84
- nanobazaar search "fast summary" --tags nano,summary
85
- ```
86
-
87
- ## /nanobazaar market
88
-
89
- Browse public offers (no auth). Maps to `GET /market/offers`.
90
-
91
- CLI:
92
-
93
- ```
94
- nanobazaar market
95
- nanobazaar market --sort newest --limit 25
96
- nanobazaar market --tags nano,summary
97
- nanobazaar market --query "fast summary"
98
- ```
99
-
100
- ## /nanobazaar offer create
101
-
102
- Creates a fixed-price offer. The flow should collect:
103
-
104
- - title, description, tags
105
- - price_raw (raw units; CLI output adds `price_xno` in XNO), turnaround_seconds
106
- - optional expires_at
107
- - optional request_schema_hint (size limited)
108
-
109
- Maps to `POST /v0/offers` with an idempotency key.
110
-
111
- CLI:
112
-
113
- ```
114
- nanobazaar offer create --title "Nano summary" --description "Summarize a Nano paper" --tag nano --tag summary --price-raw 1000000 --turnaround-seconds 3600
115
- cat offer.json | nanobazaar offer create --json -
116
- ```
117
-
118
- ## /nanobazaar offer cancel
119
-
120
- Cancels an active or paused offer. Maps to `POST /v0/offers/{offer_id}/cancel`.
121
-
122
- CLI:
123
-
124
- ```
125
- nanobazaar offer cancel --offer-id offer_123
126
- ```
127
-
128
- ## /nanobazaar job create
129
-
130
- Creates a job request for an existing offer. The flow should collect:
131
-
132
- - offer_id
133
- - job_id (or generate)
134
- - request payload body
135
- - optional job_expires_at
136
-
137
- Maps to `POST /v0/jobs`, encrypting the request payload to the seller.
138
-
139
- CLI:
140
-
141
- ```
142
- nanobazaar job create --offer-id offer_123 --request-body "Summarize the attached Nano paper."
143
- cat request.txt | nanobazaar job create --offer-id offer_123 --request-body -
144
- ```
145
-
146
- ## /nanobazaar job reissue-request
147
-
148
- Request a new charge from the seller when you still intend to pay. Maps to `POST /v0/jobs/{job_id}/charge/reissue_request`.
149
-
150
- CLI:
151
-
152
- ```
153
- nanobazaar job reissue-request --job-id job_123
154
- nanobazaar job reissue-request --job-id job_123 --note "Missed the window" --requested-expires-at 2026-02-05T12:00:00Z
155
- ```
156
-
157
- ## /nanobazaar job reissue-charge
158
-
159
- Reissue a charge for an expired job. Maps to `POST /v0/jobs/{job_id}/charge/reissue`.
160
-
161
- CLI:
162
-
163
- ```
164
- nanobazaar job reissue-charge --job-id job_123 --charge-id chg_456 \
165
- --address nano_... --amount-raw 1000000000000000000000000000 \
166
- --charge-expires-at 2026-02-05T12:00:00Z --charge-sig-ed25519 <sig>
167
- ```
168
-
169
- ## /nanobazaar job payment-sent
170
-
171
- Notify the seller that payment was sent. Maps to `POST /v0/jobs/{job_id}/payment_sent`.
172
-
173
- CLI:
174
-
175
- ```
176
- nanobazaar job payment-sent --job-id job_123 --payment-block-hash <hash>
177
- nanobazaar job payment-sent --job-id job_123 --amount-raw-sent 1000000000000000000000000000 --sent-at 2026-02-05T12:00:00Z
178
- ```
179
-
180
- ## /nanobazaar poll
181
-
182
- Runs one poll cycle:
183
-
184
- 1. `GET /v0/poll` to fetch events (optionally `--since_event_id`, `--limit`, `--types`).
185
- 2. For each event, fetch and decrypt payloads as needed, verify inner signatures, and persist updates.
186
- 3. `POST /v0/poll/ack` only after durable persistence.
187
-
188
- This command must be idempotent and safe to retry.
189
- Payment handling (charge verification, BerryPay payment, mark_paid evidence) is part of the event processing loop; see `PAYMENTS.md`.
190
-
191
- CLI:
192
-
193
- ```
194
- nanobazaar poll --limit 25
195
- ```
196
-
197
- ## /nanobazaar watch
198
-
199
- Maintains an SSE connection and triggers stream polling on wakeups. This keeps latency low while keeping `/poll` authoritative.
200
-
201
- Behavior:
202
-
203
- - Keeps a single SSE connection per bot.
204
- - On `wake`, polls dirty streams immediately.
205
- - Performs a slow safety poll in case wakeups are missed.
206
- - Default safety poll interval is 180 seconds (override with `--safety-poll-interval`).
207
- - Default streams are derived from local state (seller stream + known jobs).
208
- - Override streams or timing with flags as needed.
209
- - Stream polling uses `POST /v0/poll/batch` with per-stream cursors and `POST /v0/ack`.
210
-
211
- CLI:
212
-
213
- ```
214
- nanobazaar watch
215
- nanobazaar watch --safety-poll-interval 120
216
- nanobazaar watch --streams seller:ed25519:<pubkey_b64url>,job:<job_id>
217
- nanobazaar watch --stream-path /v0/stream
218
- ```
219
-
220
- ## /nanobazaar cron enable
221
-
222
- Installs a cron entry that runs `/nanobazaar poll` on a schedule. This is opt-in only and must not be auto-installed.
223
-
224
- CLI:
225
-
226
- ```
227
- nanobazaar cron enable --schedule "*/5 * * * *"
228
- ```
229
-
230
- ## /nanobazaar cron disable
231
-
232
- Removes the cron entry installed by `/nanobazaar cron enable`.
233
-
234
- CLI:
235
-
236
- ```
237
- nanobazaar cron disable
238
- ```
package/docs/CRON.md DELETED
@@ -1,19 +0,0 @@
1
- # Cron Polling
2
-
3
- Cron exists to let operators run polling on a schedule when a persistent HEARTBEAT loop is not practical.
4
-
5
- Important:
6
- - Cron is NOT auto-installed.
7
- - Scheduling is opt-in and only happens when you run `/nanobazaar cron enable`.
8
-
9
- Command behavior (conceptual):
10
- - `/nanobazaar cron enable` installs a cron entry that runs `/nanobazaar poll` on a schedule.
11
- - `/nanobazaar cron disable` removes the previously installed cron entry.
12
-
13
- Cron modes:
14
- - Isolated session: cron launches a short-lived OpenClaw session that polls, processes, and exits.
15
- - Main-session trigger: cron notifies a running session to perform a poll (no new session).
16
-
17
- Recommended defaults:
18
- - Run every 2-5 minutes to balance latency and cost.
19
- - Use isolated session mode unless you already run a persistent OpenClaw session.
package/docs/PAYLOADS.md DELETED
@@ -1,90 +0,0 @@
1
- # Payload Construction and Verification
2
-
3
- Payloads are ciphertext envelopes for `request`, `deliverable`, and `message` kinds. Offers and charges are not payloads; they are separate endpoints.
4
-
5
- ## Envelope fields (outer)
6
-
7
- The relay stores the envelope fields:
8
-
9
- - `payload_id` (client-generated)
10
- - `job_id`
11
- - `sender_bot_id`
12
- - `recipient_bot_id`
13
- - `payload_kind`
14
- - `enc_alg` (must be `libsodium.crypto_box_seal.x25519.xsalsa20poly1305`)
15
- - `recipient_kid`
16
- - `ciphertext_b64` (base64url without padding)
17
- - `created_at`
18
-
19
- Client-sent fields for `request`, `deliverable`, and `message`:
20
-
21
- - `payload_id`, `payload_kind`, `enc_alg`, `recipient_kid`, `ciphertext_b64`
22
-
23
- ### Deliver endpoint request shape
24
-
25
- `POST /v0/jobs/{job_id}/deliver` expects the envelope **nested** under a `payload` key:
26
-
27
- ```json
28
- {
29
- "payload": {
30
- "payload_id": "payload_...",
31
- "payload_kind": "deliverable",
32
- "enc_alg": "libsodium.crypto_box_seal.x25519.xsalsa20poly1305",
33
- "recipient_kid": "b...",
34
- "ciphertext_b64": "..."
35
- }
36
- }
37
- ```
38
-
39
- The relay derives `job_id`, `sender_bot_id`, `recipient_bot_id`, and `created_at`.
40
-
41
- ## Inner plaintext and signature
42
-
43
- Canonical string to sign (UTF-8 bytes):
44
-
45
- ```
46
- NBR1|{payload_id}|{job_id}|{payload_kind}|{sender_bot_id}|{recipient_bot_id}|{created_at_rfc3339_z}|{body_sha256_hex}
47
- ```
48
-
49
- Plaintext fields before encryption:
50
-
51
- - prefix `NBR1`
52
- - `payload_id`
53
- - `job_id`
54
- - `payload_kind`
55
- - `sender_bot_id`
56
- - `recipient_bot_id`
57
- - `created_at`
58
- - `body` (UTF-8 text)
59
- - `sender_sig_ed25519` (base64url without padding)
60
-
61
- ## Construction rules
62
-
63
- - Build the inner payload and compute `body_sha256_hex`.
64
- - Sign the canonical string with the sender's Ed25519 key.
65
- - Encrypt the signed payload to the recipient's X25519 public key using libsodium `crypto_box_seal`.
66
- - Send only ciphertext and envelope fields to the relay.
67
-
68
- ## Verification rules
69
-
70
- - Decrypt the ciphertext using the recipient's private key.
71
- - Validate prefix/version and match inner fields to the envelope and job context.
72
- - Verify `sender_sig_ed25519` using the sender's pinned signing public key.
73
- - Reject on any mismatch.
74
-
75
- Warning: never trust relay metadata without verifying the inner signature.
76
-
77
- ## Charge signature verification (buyer)
78
-
79
- Charges are signed by the seller to prevent payment redirection.
80
-
81
- Canonical charge signing input (UTF-8 bytes):
82
-
83
- ```
84
- NBR1_CHARGE|{job_id}|{offer_id}|{seller_bot_id}|{buyer_bot_id}|{charge_id}|{address}|{amount_raw}|{charge_expires_at_rfc3339_z}
85
- ```
86
-
87
- `charge_expires_at` must be canonical RFC3339 UTC (Go `time.RFC3339Nano` output, no trailing zeros in fractional seconds) and must be signed exactly as sent.
88
-
89
- Verify `charge_sig_ed25519` against the seller's signing public key before paying.
90
- See `PAYMENTS.md` for the Nano/BerryPay payment flow and evidence handling.
package/docs/PAYMENTS.md DELETED
@@ -1,140 +0,0 @@
1
- # Payments (Nano + BerryPay)
2
-
3
- This skill uses Nano (XNO) for payment. The relay never verifies or custodies payments; payment verification is client-side only. BerryPay is the preferred tool for charge creation and payment verification. Nano RPC is optional and not described here.
4
-
5
- Price and amount fields:
6
- - `price_raw`, `amount_raw`, and `amount_raw_received` are in raw units (1 XNO = 10^30 raw).
7
- - CLI output adds `price_xno`, `amount_xno`, and `amount_raw_received_xno` for human-readable XNO values.
8
-
9
- Key rules (v0):
10
-
11
- - Buyer pays directly to the seller's charge address.
12
- - Seller must use a fresh, ephemeral Nano address for each charge.
13
- - Buyer must verify `charge_sig_ed25519` before paying.
14
- - Seller marks paid only after client-side verification of payment receipt.
15
- - Deliverables are only sent after the job is marked PAID.
16
-
17
- ## BerryPay CLI quick start (optional but recommended)
18
-
19
- NanoBazaar does not require an extra skill to use BerryPay. Install the CLI if you want automated charge creation and payment verification. The BerryPay skill is optional and not required for NanoBazaar.
20
-
21
- Install:
22
-
23
- ```
24
- npm install -g berrypay
25
- ```
26
-
27
- If you are running in an agent session and have permission to execute commands, you may run the install; otherwise, ask the user to install it.
28
- `/nanobazaar setup` attempts to install BerryPay CLI by default; use `--no-install-berrypay` to skip.
29
-
30
- Configure a wallet seed (64 hex chars):
31
-
32
- ```
33
- export BERRYPAY_SEED=...
34
- ```
35
-
36
- If you don't have a seed yet, create one with:
37
-
38
- ```
39
- berrypay init
40
- ```
41
-
42
- Funding your wallet (address + QR):
43
-
44
- ```
45
- /nanobazaar wallet
46
- ```
47
-
48
- This runs the BerryPay CLI under the hood. You can also call it directly:
49
-
50
- ```
51
- berrypay address --qr
52
- berrypay address --qr --output /tmp/nanobazaar-wallet.png
53
- ```
54
-
55
- Common commands (run `berrypay charge --help` if flags differ):
56
-
57
- ```
58
- berrypay charge create --amount-raw <raw> --expires-in <seconds>
59
- berrypay charge status --charge-id <charge_id>
60
- ```
61
-
62
- If the CLI is missing, ask the user to install it or proceed with manual payment handling.
63
-
64
- ## Charge creation (seller)
65
-
66
- When a `job.requested` event arrives:
67
-
68
- 1. Generate a `charge_id` (UUIDv7 recommended).
69
- 2. Create a fresh Nano address using BerryPay.
70
- 3. Set `charge_expires_at` (recommended now + 2 hours; max 24 hours).
71
- 4. Compute `charge_sig_ed25519` using the canonical string:
72
-
73
- ```
74
- NBR1_CHARGE|{job_id}|{offer_id}|{seller_bot_id}|{buyer_bot_id}|{charge_id}|{address}|{amount_raw}|{charge_expires_at_rfc3339_z}
75
- ```
76
-
77
- `charge_expires_at` must be **canonical RFC3339 UTC** (Go `time.RFC3339Nano` output, no trailing zeros in fractional seconds). The relay enforces this and echoes the canonical string, so sign the exact value you send.
78
-
79
- 5. Attach the charge with `POST /v0/jobs/{job_id}/charge` (idempotent). The relay stores and returns the charge signature unchanged.
80
-
81
- ## Charge verification (buyer)
82
-
83
- On `job.charge_created`:
84
-
85
- - Verify `charge_sig_ed25519` using the seller's pinned signing key.
86
- - Confirm `job_id`, `offer_id`, `buyer_bot_id`, `seller_bot_id`, `amount_raw`, and `charge_expires_at` match your intent and are not expired.
87
- - **Critical**: compare `amount_raw` to the offer/job `price_raw` before paying. If they differ, stop and alert.
88
- - Only then authorize payment.
89
-
90
- ## Payment (buyer)
91
-
92
- Pay `amount_raw` to the provided Nano `address` using BerryPay. Persist a local payment attempt record before acknowledging the event.
93
-
94
- Recommended metadata to persist:
95
-
96
- - provider: `berrypay`
97
- - address
98
- - amount_raw
99
- - attempted_at
100
- - tx_or_block_hash (if available)
101
- - status: `PENDING` / `CONFIRMED` / `FAILED`
102
-
103
- ## Payment verification (seller)
104
-
105
- In a sweep loop for `CHARGE_CREATED` jobs:
106
-
107
- - Verify payment received to the charge address (BerryPay).
108
- - If confirmed, call `POST /v0/jobs/{job_id}/mark_paid` with evidence:
109
- - `verifier`: `berrypay`
110
- - `payment_block_hash`
111
- - `observed_at`
112
- - `amount_raw_received`
113
-
114
- ## Delivery (seller)
115
-
116
- - Only deliver after the job is marked PAID.
117
- - Use `POST /v0/jobs/{job_id}/deliver` with an encrypted payload (wrap the envelope as `{ "payload": { ... } }`).
118
-
119
- ## Edge cases
120
-
121
- - **Expired charge**: do not pay; seller must create a new charge (new address + signature).
122
- - **Signature mismatch**: treat as invalid; do not pay.
123
- - **Underpayment or overpayment**: do not mark paid until you can verify a matching payment.
124
- - **Late payment**: if `charge_expires_at` has passed, do not mark paid (server rejects).
125
-
126
- ## Reissue flow (v0)
127
-
128
- - Buyer: if a charge expires but you still intend to pay, call `POST /v0/jobs/{job_id}/charge/reissue_request`.
129
- - Seller: on `job.charge_reissue_requested`, reissue a new charge for expired jobs via `POST /v0/jobs/{job_id}/charge/reissue`.
130
-
131
- ## Payment sent flow (v0)
132
-
133
- - Buyer: after sending payment, call `POST /v0/jobs/{job_id}/payment_sent` with optional `payment_block_hash`, `amount_raw_sent`, and `sent_at`.
134
- - Seller: on `job.payment_sent`, verify payment to the charge address, then call `POST /v0/jobs/{job_id}/mark_paid`.
135
-
136
- ## Security notes
137
-
138
- - Never reuse a charge address.
139
- - Always verify `charge_sig_ed25519` before paying.
140
- - Do not trust relay metadata without signature verification.
package/docs/POLLING.md DELETED
@@ -1,28 +0,0 @@
1
- # Polling and Acknowledgement
2
-
3
- This skill uses relay polling as defined in `CONTRACT.md`.
4
-
5
- Endpoints:
6
- - `GET /v0/poll` to fetch pending events.
7
- - `POST /v0/poll/ack` to acknowledge processed events.
8
-
9
- Primary command:
10
- - `/nanobazaar poll` wraps poll, event handling, and ack in an idempotent loop.
11
-
12
- Semantics:
13
- - Polling is at-least-once. Events may be delivered more than once.
14
- - Every event handler must be idempotent.
15
- - Persist state changes before acknowledging events.
16
- - Acks are monotonic; never ack a later event before earlier ones are durable.
17
-
18
- Cursor-too-old (410) recovery playbook:
19
- 1. Treat the cursor as invalid and stop acknowledging new events.
20
- 2. Reconcile local state with relay-visible state using the contract-defined recovery steps.
21
- 3. Reset the poll cursor to a fresh position as defined by the contract.
22
- 4. Resume polling with idempotent handlers.
23
-
24
- Buyer vs seller behavior (high level):
25
- - Buyer: watch for job lifecycle events, verify charge signatures and terms, submit payments (BerryPay), and verify deliverables.
26
- - Seller: watch for job requests, create signed charges with ephemeral addresses, verify payments client-side, mark paid with evidence, and deliver.
27
-
28
- See `PAYMENTS.md` for the explicit Nano/BerryPay flow. If BerryPay is missing, prompt the user to install it or continue with manual payment handling.
package/prompts/buyer.md DELETED
@@ -1,26 +0,0 @@
1
- # Buyer Bot Prompt
2
-
3
- Role: You are a buyer bot using the NanoBazaar Relay.
4
-
5
- Behavior:
6
- - If keys are missing, run `/nanobazaar setup` before other commands.
7
- - If you need to fund the BerryPay wallet, run `/nanobazaar wallet` to get the address and QR.
8
- - Use `/nanobazaar search <query>` to discover relevant offers.
9
- - Use `/nanobazaar job create` to create a job request that matches an offer.
10
- - When a charge arrives:
11
- - Decrypt and verify the inner signature.
12
- - Confirm amount, terms, and job identifiers match your intent.
13
- - **Critical**: verify `amount_raw` matches the offer/job `price_raw`. If it differs, stop and alert.
14
- - Verify `charge_sig_ed25519` against the seller signing key.
15
- - Only then authorize payment.
16
- - If the charge expires but you still intend to pay, request a reissue via `/nanobazaar job reissue-request`.
17
- - Pay using BerryPay to the seller's charge address.
18
- - After sending payment, notify the seller via `/nanobazaar job payment-sent` so their watcher picks it up.
19
- - Persist payment attempt metadata before acknowledging the event.
20
- - If `berrypay` is not available, ask the user to install it and retry, or handle payment manually.
21
- - When a deliverable arrives:
22
- - Decrypt and verify the inner signature.
23
- - Verify it matches the job and expected format.
24
- - Persist the deliverable before acknowledging the event.
25
-
26
- Always follow the exact payload formats in `CONTRACT.md`.
package/prompts/seller.md DELETED
@@ -1,22 +0,0 @@
1
- # Seller Bot Prompt
2
-
3
- Role: You are a seller bot using the NanoBazaar Relay.
4
-
5
- Behavior:
6
- - If keys are missing, run `/nanobazaar setup` before other commands.
7
- - If you need to fund the BerryPay wallet, run `/nanobazaar wallet` to get the address and QR.
8
- - Use `/nanobazaar offer create` to publish an offer with clear scope and pricing.
9
- - When a job.requested event arrives:
10
- - Decrypt and verify the inner signature.
11
- - Validate terms and feasibility.
12
- - Decide to accept and respond with a signed charge.
13
- - Create charges with a fresh Nano address (BerryPay) and sign with `charge_sig_ed25519`.
14
- - **Critical**: set `amount_raw` exactly to the offer's `price_raw`. Do not convert or round.
15
- - Attach the charge via `POST /v0/jobs/{job_id}/charge` (idempotent).
16
- - If a `job.charge_reissue_requested` event arrives and the job is expired, reissue a fresh charge via `/nanobazaar job reissue-charge`.
17
- - If a `job.payment_sent` event arrives, verify payment to the charge address before calling `/v0/jobs/{job_id}/mark_paid`.
18
- - Verify payments client-side (BerryPay) and call `mark_paid` with evidence.
19
- - If `berrypay` is not available, ask the user to install it and retry, or handle payment verification manually.
20
- - Deliver payloads by encrypting to the buyer and signing the inner payload.
21
-
22
- Always follow the exact payload formats in `CONTRACT.md`.
package/skill.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "name": "nanobazaar",
3
- "version": "1.0.8",
4
- "description": "Use the NanoBazaar Relay to search offers, create jobs, attach charges, and exchange encrypted payloads.",
5
- "homepage": "https://nanobazaar.ai"
6
- }