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 +11 -43
- package/bin/nanobazaar +294 -13
- package/package.json +3 -5
- package/tools/cli_smoke_test.sh +1 -1
- package/tools/setup.js +112 -14
- package/docs/AUTH.md +0 -41
- package/docs/CLAW_HUB.md +0 -32
- package/docs/COMMANDS.md +0 -238
- package/docs/CRON.md +0 -19
- package/docs/PAYLOADS.md +0 -90
- package/docs/PAYMENTS.md +0 -140
- package/docs/POLLING.md +0 -28
- package/prompts/buyer.md +0 -26
- package/prompts/seller.md +0 -22
- package/skill.json +0 -6
package/README.md
CHANGED
|
@@ -1,50 +1,18 @@
|
|
|
1
|
-
# NanoBazaar
|
|
1
|
+
# NanoBazaar CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line client for the NanoBazaar Relay.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
```
|
|
10
|
+
npm install -g nanobazaar-cli
|
|
11
|
+
nanobazaar --help
|
|
12
|
+
```
|
|
16
13
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
276
|
+
expanded = HOME_DIR;
|
|
88
277
|
} else if (expanded.startsWith('~/') || expanded.startsWith('~\\')) {
|
|
89
|
-
expanded = path.join(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"skill.json",
|
|
17
|
-
"tools/"
|
|
14
|
+
"tools/",
|
|
15
|
+
"README.md"
|
|
18
16
|
],
|
|
19
17
|
"dependencies": {
|
|
20
18
|
"libsodium-wrappers": "^0.7.11"
|
package/tools/cli_smoke_test.sh
CHANGED
|
@@ -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/
|
|
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(
|
|
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 =
|
|
186
|
+
expanded = HOME_DIR;
|
|
91
187
|
} else if (expanded.startsWith('~/') || expanded.startsWith('~\\')) {
|
|
92
|
-
expanded = path.join(
|
|
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,
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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`.
|