nightpay 0.4.3 → 0.4.4
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 +362 -57
- package/package.json +1 -1
- package/skills/nightpay/rules/content-safety.md +15 -99
- package/skills/nightpay/scripts/gateway.sh +129 -151
package/README.md
CHANGED
|
@@ -6,135 +6,438 @@
|
|
|
6
6
|
|
|
7
7
|
> Built on the [Midnight Network](https://midnight.network).
|
|
8
8
|
|
|
9
|
-
Privacy-preserving bounty pools for AI agents.
|
|
9
|
+
Privacy-preserving bounty pools for AI agents. Midnight ZK proofs for funder anonymity, Masumi for agent hiring, Cardano for settlement.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
|
-
### OpenClaw (primary)
|
|
13
|
+
### OpenClaw (primary platform — two commands)
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
openclaw plugins install nightpay
|
|
17
17
|
openclaw plugins enable nightpay
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Skill files are auto-discovered from the installed package — no `npx nightpay init` needed.
|
|
21
|
+
|
|
22
|
+
Then set credentials:
|
|
21
23
|
|
|
22
24
|
```bash
|
|
23
25
|
openclaw config set skills.entries.nightpay.env.MASUMI_API_KEY "your-key"
|
|
24
26
|
openclaw config set skills.entries.nightpay.env.OPERATOR_ADDRESS "64-char-hex"
|
|
25
27
|
openclaw config set skills.entries.nightpay.env.BRIDGE_URL "https://bridge.nightpay.dev"
|
|
28
|
+
# NIGHTPAY_API_URL defaults to https://api.nightpay.dev
|
|
26
29
|
openclaw gateway restart
|
|
27
30
|
```
|
|
28
31
|
|
|
29
|
-
Verify
|
|
32
|
+
Verify with `/nightpay status` in your connected channel.
|
|
33
|
+
Full guide: [`docs/OPENCLAW_ONBOARDING.md`](docs/OPENCLAW_ONBOARDING.md)
|
|
34
|
+
|
|
35
|
+
### Other platforms (Claude Code, Cursor, Copilot, raw)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx nightpay setup # init + auto-detect platform + validate
|
|
39
|
+
```
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
Or step by step:
|
|
32
42
|
|
|
33
43
|
```bash
|
|
34
|
-
npx nightpay
|
|
44
|
+
npx nightpay init # copy skill files to ./skills/nightpay/
|
|
45
|
+
npx nightpay validate # check env, prerequisites, connectivity
|
|
35
46
|
```
|
|
36
47
|
|
|
48
|
+
|
|
37
49
|
## How It Works
|
|
38
50
|
|
|
39
|
-
1. **Create a pool** — set a funding goal, contribution amount, and max funders
|
|
40
|
-
2. **Funders back it anonymously** — shielded NIGHT via ZK
|
|
51
|
+
1. **Create a pool** — set a funding goal, fixed contribution amount, and max funders
|
|
52
|
+
2. **Funders back it anonymously** — shielded NIGHT via Midnight ZK proofs (funder identity destroyed by nullifier)
|
|
41
53
|
3. **Goal met → pool activates** — an AI agent is hired via Masumi MIP-003
|
|
42
|
-
4. **Goal not met → full refund** — 100
|
|
43
|
-
5. **Work done → ZK receipt** —
|
|
54
|
+
4. **Goal not met → full refund** — funders reclaim 100%, no fee charged
|
|
55
|
+
5. **Work done → ZK receipt** — shielded token proves completion, reveals nothing about funders
|
|
56
|
+
6. **Operator collects infrastructure fee** — configurable bps (default 2%) on successful completions only
|
|
44
57
|
|
|
45
58
|
```
|
|
46
|
-
Pool Creator
|
|
47
|
-
|
|
|
48
|
-
|-- createPool
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
+
Pool Creator NightPay Contract Masumi/Cardano
|
|
60
|
+
| | |
|
|
61
|
+
|-- createPool ----------->| |
|
|
62
|
+
| | |
|
|
63
|
+
Funders (anonymous) | |
|
|
64
|
+
|-- fundPool (× N) ------>| |
|
|
65
|
+
| | |
|
|
66
|
+
| goal met? -----+ |
|
|
67
|
+
| / \ |
|
|
68
|
+
| yes no (deadline) |
|
|
69
|
+
| | \ |
|
|
70
|
+
| activatePool claimRefund (× N) |
|
|
71
|
+
| | (100% returned) |
|
|
72
|
+
| |-- hire agent --------------------------->|
|
|
73
|
+
| |<-- work delivered ------------------------|
|
|
74
|
+
| |-- completeAndReceipt ------------------->|
|
|
75
|
+
| | |
|
|
76
|
+
|<-- ZK receipt (verifiable, anonymous) --------------|
|
|
59
77
|
```
|
|
60
78
|
|
|
61
|
-
**Public:** pool exists, goal, completion status.
|
|
62
|
-
**Private:** who funded, how much, which agent.
|
|
79
|
+
**Public:** pool exists, funding goal, completion status, total pool count.
|
|
80
|
+
**Private:** who funded it, how much each person contributed, which agent did the work.
|
|
81
|
+
|
|
82
|
+
<img src="https://github.com/nightpay/nightpay/blob/master/docs/nightpay-ecosystem.jpg">
|
|
63
83
|
|
|
64
84
|
## Usage
|
|
65
85
|
|
|
86
|
+
### gateway.sh — Pool & Bounty CLI
|
|
87
|
+
|
|
66
88
|
```bash
|
|
67
|
-
#
|
|
89
|
+
# Contract stats
|
|
68
90
|
bash skills/nightpay/scripts/gateway.sh stats
|
|
69
|
-
|
|
91
|
+
|
|
92
|
+
# Create pool: description, contribution (specks), goal (specks)
|
|
93
|
+
bash skills/nightpay/scripts/gateway.sh create-pool "Audit XYZ contract" 10000000 50000000
|
|
94
|
+
|
|
95
|
+
# Fund (returns memoryId when OpenShart is available)
|
|
70
96
|
bash skills/nightpay/scripts/gateway.sh fund-pool <pool_commitment>
|
|
97
|
+
|
|
98
|
+
# Optional pool transitions
|
|
99
|
+
bash skills/nightpay/scripts/gateway.sh activate-pool <pool_commitment>
|
|
100
|
+
bash skills/nightpay/scripts/gateway.sh expire-pool <pool_commitment>
|
|
101
|
+
|
|
102
|
+
# Hire + complete
|
|
71
103
|
bash skills/nightpay/scripts/gateway.sh find-agent "smart contract audit"
|
|
72
|
-
bash skills/nightpay/scripts/gateway.sh hire-and-pay <agent_id> "Audit XYZ" <commitment_hash>
|
|
104
|
+
bash skills/nightpay/scripts/gateway.sh hire-and-pay <agent_id> "Audit XYZ contract" <commitment_hash> [refund_address]
|
|
105
|
+
bash skills/nightpay/scripts/gateway.sh complete <job_id> <commitment_hash>
|
|
73
106
|
|
|
74
|
-
#
|
|
107
|
+
# Refund (expired pool)
|
|
108
|
+
bash skills/nightpay/scripts/gateway.sh claim-refund <pool_commitment> <funder_nullifier>
|
|
109
|
+
bash skills/nightpay/scripts/gateway.sh claim-refund --memory-id <openshart_memory_id>
|
|
110
|
+
|
|
111
|
+
# Emergency refund (gateway offline, 500+ tx passed)
|
|
112
|
+
bash skills/nightpay/scripts/gateway.sh emergency-refund <pool_commitment> <funder_nullifier> <specks> <funded_at_tx> <nonce>
|
|
113
|
+
bash skills/nightpay/scripts/gateway.sh emergency-refund --memory-id <openshart_memory_id> <specks> <funded_at_tx>
|
|
114
|
+
|
|
115
|
+
# Verify receipt (bridge endpoint)
|
|
116
|
+
curl -sS -X POST "${BRIDGE_URL}/verifyReceipt" \
|
|
117
|
+
-H "Content-Type: application/json" \
|
|
118
|
+
-d '{"receiptHash":"<receipt_hash>"}'
|
|
119
|
+
|
|
120
|
+
# Optional sweep helpers
|
|
121
|
+
bash skills/nightpay/scripts/gateway.sh refund-unclaimed --dry-run
|
|
122
|
+
bash skills/nightpay/scripts/gateway.sh optimistic-sweep --dry-run
|
|
123
|
+
|
|
124
|
+
# Browse bounties
|
|
75
125
|
bash skills/nightpay/scripts/bounty-board.sh stats
|
|
76
126
|
```
|
|
77
127
|
|
|
128
|
+
|
|
129
|
+
### OpenClaw
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
openclaw plugins install nightpay
|
|
133
|
+
openclaw plugins enable nightpay
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
> **Note:** Preferred path is plugin install + enable (above). `npx nightpay setup` remains a fallback for non-plugin/manual setups.
|
|
137
|
+
|
|
138
|
+
After setup, merge `skills/nightpay/openclaw-fragment.json` into `~/.openclaw/openclaw.json` and fill in your credentials:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# In openclaw.json, under skills.entries.nightpay.env:
|
|
142
|
+
MASUMI_API_KEY = "your-masumi-api-key"
|
|
143
|
+
OPERATOR_ADDRESS = "your-64-char-hex-address"
|
|
144
|
+
BRIDGE_URL = "https://bridge.nightpay.dev"
|
|
145
|
+
# NIGHTPAY_API_URL defaults to https://api.nightpay.dev — no change needed
|
|
146
|
+
# Optional command overrides for /nightpay wallet*
|
|
147
|
+
# MIDNIGHT_WALLET_CLI_BIN = "midnight"
|
|
148
|
+
# OPENSHART_BIN = "openshart"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Then validate:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
openclaw config validate
|
|
155
|
+
npx nightpay validate
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Optional wallet tooling for agents (`midnight-wallet-cli` + OpenShart):
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm install -g midnight-wallet-cli
|
|
162
|
+
npm install -g openshart
|
|
163
|
+
midnight --version
|
|
164
|
+
midnight info --json
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Inside OpenClaw, NightPay now exposes:
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
/nightpay wallet
|
|
171
|
+
/nightpay wallet status
|
|
172
|
+
/nightpay wallet provision
|
|
173
|
+
/nightpay wallet provision preprod
|
|
174
|
+
/nightpay wallet help
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`/nightpay wallet provision` generates a Midnight wallet and stores seed+mnemonic in OpenShart under encrypted memory.
|
|
178
|
+
The command output includes only address/network/fingerprint + `memoryId` (no plaintext seed or mnemonic).
|
|
179
|
+
|
|
180
|
+
This integration is optional and helps with agent-side wallet workflows (provision/balance/transfer/localnet).
|
|
181
|
+
It does **not** replace NightPay's bridge-side `OPERATOR_ADDRESS` requirement (shielded 64-char hex).
|
|
182
|
+
|
|
183
|
+
### MIP-003 API
|
|
184
|
+
|
|
185
|
+
| Method | Endpoint | Auth | Purpose |
|
|
186
|
+
|--------|----------|------|---------|
|
|
187
|
+
| `GET` | `/availability` | None | Health check |
|
|
188
|
+
| `GET` | `/x402` | None | x402 payment requirements and sample challenge payload |
|
|
189
|
+
| `POST` | `/start_job` | `PAYMENT-SIGNATURE` when x402 is enabled (or none by default) | Create job from funded pool |
|
|
190
|
+
| `POST` | `/claim_job/<job_id>` | Agent token | Claim a job |
|
|
191
|
+
| `POST` | `/provide_result/<job_id>` | Agent token | Submit work |
|
|
192
|
+
| `POST` | `/complete_job/<job_id>` | Operator bearer | Mark job completed after on-chain settle |
|
|
193
|
+
| `GET` | `/status/<job_id>` | Public (or job token/operator bearer for private jobs) | Check job status |
|
|
194
|
+
| `GET` | `/submissions/<job_id>` | Job token | List contest submissions |
|
|
195
|
+
| `POST` | `/vote_submission/<jid>/<sid>` | Agent token | Vote on submission |
|
|
196
|
+
| `POST` | `/select_winner/<job_id>` | Job token | Pick contest winner |
|
|
197
|
+
| `GET` | `/ontology` | None | JSON-LD ontology |
|
|
198
|
+
|
|
78
199
|
### Python SDK
|
|
79
200
|
|
|
80
201
|
```python
|
|
81
202
|
from nightpay_sdk import NightPay
|
|
82
203
|
|
|
83
|
-
np = NightPay()
|
|
84
|
-
np.validate()
|
|
85
|
-
np.stats()
|
|
86
|
-
np.post_bounty("Review this PR", 5000)
|
|
87
|
-
np.find_agent("code review")
|
|
204
|
+
np = NightPay() # auto-discovers skill location
|
|
205
|
+
report = np.validate() # full health check
|
|
206
|
+
stats = np.stats() # contract stats
|
|
207
|
+
np.post_bounty("Review this PR", 5000) # post a bounty
|
|
208
|
+
np.find_agent("code review") # search Masumi registry
|
|
88
209
|
```
|
|
89
210
|
|
|
211
|
+
<img src="https://github.com/nightpay/nightpay/blob/master/docs/nightpay-ecosystem-bountyboard.jpg">
|
|
212
|
+
|
|
90
213
|
## Configuration
|
|
91
214
|
|
|
92
215
|
```bash
|
|
93
216
|
# Required
|
|
94
|
-
MASUMI_API_KEY=your-key
|
|
95
|
-
OPERATOR_ADDRESS
|
|
96
|
-
NIGHTPAY_API_URL=https://api.nightpay.dev
|
|
97
|
-
BRIDGE_URL=https://bridge.nightpay.dev
|
|
217
|
+
export MASUMI_API_KEY="your-key"
|
|
218
|
+
export OPERATOR_ADDRESS="<64-char-hex>"
|
|
219
|
+
export NIGHTPAY_API_URL="https://api.nightpay.dev"
|
|
220
|
+
export BRIDGE_URL="https://bridge.nightpay.dev"
|
|
221
|
+
|
|
222
|
+
# Optional
|
|
223
|
+
export MIDNIGHT_NETWORK="preprod"
|
|
224
|
+
export RECEIPT_CONTRACT_ADDRESS="<64-char-hex>"
|
|
225
|
+
export OPERATOR_FEE_BPS="200" # 2%, max 500 (5%)
|
|
226
|
+
export DEFAULT_POOL_DEADLINE_HOURS="72"
|
|
227
|
+
export JOB_TOKEN_SECRET="<random>"
|
|
228
|
+
export MIP003_MODE="compat" # compat | strict
|
|
229
|
+
export X402_ENABLED="0" # 1 => enforce x402 on paid routes
|
|
230
|
+
export X402_REQUIRE_ROUTES="/start_job" # comma list, '*' suffix supported
|
|
231
|
+
export X402_ACCEPT_AMOUNT="1000" # atomic units in PAYMENT-REQUIRED
|
|
232
|
+
export X402_VERIFY_MODE="none" # none | facilitator
|
|
233
|
+
export X402_FACILITATOR_URL="" # required when verify_mode=facilitator
|
|
234
|
+
export MIP003_PAYMENT_SIGNATURE="" # optional gateway passthrough for hire-direct
|
|
235
|
+
export OPENSHART_BIN="openshart" # optional: override OpenShart command path
|
|
236
|
+
export MIDNIGHT_WALLET_CLI_BIN="midnight" # optional: override midnight-wallet-cli command
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### MIP-003 Modes
|
|
240
|
+
|
|
241
|
+
- `compat` (default): NightPay-rich payloads with `status` + `internal_status`
|
|
242
|
+
- `strict`: canonical MIP shapes with `id`, lifecycle timestamps, `status_id` validation
|
|
243
|
+
|
|
244
|
+
### x402 (Optional, Partial)
|
|
245
|
+
|
|
246
|
+
- When `X402_ENABLED=1`, configured routes (default `/start_job`) return `402` + `PAYMENT-REQUIRED` if `PAYMENT-SIGNATURE` is missing.
|
|
247
|
+
- In partial mode (`X402_VERIFY_MODE=none`), the server only checks header presence (no cryptographic verification).
|
|
248
|
+
- In facilitator mode (`X402_VERIFY_MODE=facilitator` + `X402_FACILITATOR_URL`), NightPay calls facilitator `/verify` and optionally `/settle` (`X402_SETTLE_ON_SUCCESS=1`).
|
|
249
|
+
|
|
250
|
+
### Operator Setup
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
# Get operator address
|
|
254
|
+
curl -sS "${BRIDGE_URL}/operator-address" | python3 -m json.tool
|
|
255
|
+
|
|
256
|
+
# Deploy contract
|
|
257
|
+
curl -sS -X POST "${BRIDGE_URL}/deploy" \
|
|
258
|
+
-H "Authorization: Bearer ${BRIDGE_ADMIN_TOKEN}" \
|
|
259
|
+
-H "Content-Type: application/json" \
|
|
260
|
+
-d '{"contractPath":"skills/nightpay/contracts/receipt.js","zkPath":"skills/nightpay/contracts/receipt.zk","operatorFeeBps":200}' \
|
|
261
|
+
| python3 -m json.tool
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
See [`docs/AGENT_PLAYGROUND.md`](docs/AGENT_PLAYGROUND.md) for the full operator handoff.
|
|
265
|
+
|
|
266
|
+
## Quality Gate
|
|
267
|
+
|
|
268
|
+
Run this before pushing:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
npm test
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
What it runs:
|
|
275
|
+
|
|
276
|
+
1. `test/script-sanity.sh` — shell/python/json syntax and integrity checks
|
|
277
|
+
2. `test/server-sync-start-args.sh` — deploy script CLI + mocked SSH flow
|
|
278
|
+
3. `test/mip003-strict.sh` — strict-mode MIP-003 contract checks
|
|
279
|
+
4. `test/smoke.sh` — end-to-end gateway + MIP + contest/dispute/refund coverage
|
|
280
|
+
5. `test/bridge-runtime.sh` — bridge build + health/runtime sanity
|
|
281
|
+
|
|
282
|
+
Targeted commands:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
npm run test:quality # full quality gate
|
|
286
|
+
npm run test:smoke # smoke only
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Project Structure
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
skills/nightpay/
|
|
293
|
+
├── AGENTS.md # Agent onboarding (AAIF standard)
|
|
294
|
+
├── SKILL.md # Skill manifest — tools, config, trust model
|
|
295
|
+
├── HEARTBEAT.md # Periodic health check contract
|
|
296
|
+
├── openclaw-fragment.json # OpenClaw skill registration
|
|
297
|
+
├── scripts/
|
|
298
|
+
│ ├── gateway.sh # Pool + bounty lifecycle CLI
|
|
299
|
+
│ ├── mip003-server.sh # MIP-003 service endpoint
|
|
300
|
+
│ ├── bounty-board.sh # Public board listing
|
|
301
|
+
│ └── update-blocklist.sh # Content safety blocklist
|
|
302
|
+
├── ontology/
|
|
303
|
+
│ ├── ontology.jsonld # Machine-readable ontology (JSON-LD)
|
|
304
|
+
│ ├── ontology.md # Human/agent ontology guide
|
|
305
|
+
│ ├── context.jsonld # JSON-LD context
|
|
306
|
+
│ └── examples/*.jsonld # Pool, job, receipt examples
|
|
307
|
+
├── rules/
|
|
308
|
+
│ ├── privacy-first.md # Never reveal funder identity
|
|
309
|
+
│ ├── escrow-safety.md # Timeout, refund, pool safety
|
|
310
|
+
│ ├── receipt-format.md # ZK receipt schema
|
|
311
|
+
│ └── content-safety.md # Content classification gate
|
|
312
|
+
└── contracts/
|
|
313
|
+
└── receipt.compact # Midnight ZK contract
|
|
314
|
+
|
|
315
|
+
docs/ # Extended documentation
|
|
316
|
+
bridge/ # Midnight bridge (private git submodule)
|
|
317
|
+
ui/ # Web UI (nightpay.dev)
|
|
318
|
+
sample-agent/ # Example agent implementation
|
|
98
319
|
```
|
|
99
320
|
|
|
100
|
-
|
|
321
|
+
For completion/status sync maintenance after upgrades, use `docs/NIGHTPAY_DEV_COMPLETION_SYNC_RUNBOOK.md`.
|
|
322
|
+
|
|
323
|
+
For root + submodule commit discipline (`nightpay` + `ui/` + `bridge/`), use `docs/SUBMODULE_WORKFLOW.md`.
|
|
324
|
+
|
|
325
|
+
## Contest Mode
|
|
326
|
+
|
|
327
|
+
Jobs with `contest.enabled: true` allow multiple agents to compete:
|
|
328
|
+
|
|
329
|
+
1. Multiple agents claim the same job
|
|
330
|
+
2. Each submits work via `POST /provide_result/<job_id>`
|
|
331
|
+
3. Voter snapshot taken from claimed agents
|
|
332
|
+
4. Voters review: `GET /submissions/<job_id>` (requires job_token)
|
|
333
|
+
5. Voters cast approve/reject: `POST /vote_submission/<job_id>/<sid>`
|
|
334
|
+
6. Winner selected after quorum: `POST /select_winner/<job_id>`
|
|
335
|
+
|
|
336
|
+
Self-voting rejected. One vote per (job, submission, voter) — later POSTs upsert.
|
|
101
337
|
|
|
102
338
|
## Trust Model
|
|
103
339
|
|
|
104
|
-
The Midnight
|
|
340
|
+
The Midnight contract enforces critical guarantees via ZK circuits:
|
|
105
341
|
|
|
106
|
-
- **Fee is immutable** — `operatorFeeBps` set once at
|
|
342
|
+
- **Fee is public and immutable** — `operatorFeeBps` set once at `initialize()`, max 500 (5%)
|
|
107
343
|
- **No double-funding/refund** — nullifier set rejects duplicates
|
|
108
|
-
- **
|
|
344
|
+
- **Gateway-only pool activation/expiry** — `activatePool` and `expirePool` require gateway auth proof
|
|
345
|
+
- **Activation amount is enforced** — `activatePool` checks `totalFunded` against on-chain contribution sum
|
|
346
|
+
- **No fund theft** — contract only releases to locked gateway address
|
|
347
|
+
- **Operator withdrawals are capped** — `withdrawFees` is limited to accumulated fees
|
|
109
348
|
- **Receipts are verifiable** — `verifyReceipt()` is public
|
|
110
349
|
- **Emergency exit** — `emergencyRefund` bypasses gateway after 500+ contract txs
|
|
111
350
|
|
|
112
|
-
The gateway handles deadlines, activation, and agent selection — but cannot steal funds, change fees, or fake receipts.
|
|
351
|
+
The gateway is the only trusted component. It handles deadlines, activation, and agent selection — but **cannot** steal funds, change fees, or fake receipts.
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
# Pre-flight checks before funding or accepting work
|
|
355
|
+
curl -sf "$NIGHTPAY_API_URL/availability"
|
|
356
|
+
bash skills/nightpay/scripts/gateway.sh stats # feeBps, poolCount, initialized
|
|
357
|
+
curl -sS -X POST "$BRIDGE_URL/verifyReceipt" -H "Content-Type: application/json" -d '{"receiptHash":"<hash>"}'
|
|
358
|
+
```
|
|
113
359
|
|
|
114
360
|
See [`skills/nightpay/SKILL.md`](skills/nightpay/SKILL.md) for the full trust checklist.
|
|
115
361
|
|
|
362
|
+
## Deployment
|
|
363
|
+
|
|
364
|
+
### DNS + Caddy
|
|
365
|
+
|
|
366
|
+
```caddy
|
|
367
|
+
nightpay.dev, board.nightpay.dev {
|
|
368
|
+
reverse_proxy 127.0.0.1:3333
|
|
369
|
+
}
|
|
370
|
+
api.nightpay.dev {
|
|
371
|
+
reverse_proxy 127.0.0.1:8090
|
|
372
|
+
}
|
|
373
|
+
bridge.nightpay.dev {
|
|
374
|
+
reverse_proxy 127.0.0.1:4000
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Production Smoke Check
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
curl -sS https://api.nightpay.dev/availability | python3 -m json.tool
|
|
382
|
+
curl -sS https://bridge.nightpay.dev/health | python3 -m json.tool
|
|
383
|
+
curl -sS -o /dev/null -w "%{http_code}\n" https://board.nightpay.dev/
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Expect `bridge.nightpay.dev/health` to report `"network": "preprod"` and `"stub": false` for full on-chain mode.
|
|
387
|
+
|
|
388
|
+
### Staging DNS + Caddy
|
|
389
|
+
|
|
390
|
+
Run staging on separate local ports so it does not collide with production:
|
|
391
|
+
|
|
392
|
+
- `staging.nightpay.dev` -> `127.0.0.1:3334`
|
|
393
|
+
- `api.staging.nightpay.dev` -> `127.0.0.1:8091`
|
|
394
|
+
- `bridge.staging.nightpay.dev` -> `127.0.0.1:4001` (optional, if staging bridge exists)
|
|
395
|
+
|
|
396
|
+
Current CI staging deploys pass `--skip-bridge-restart` to avoid contending with the production bridge path.
|
|
397
|
+
|
|
398
|
+
```caddy
|
|
399
|
+
staging.nightpay.dev {
|
|
400
|
+
reverse_proxy 127.0.0.1:3334
|
|
401
|
+
}
|
|
402
|
+
api.staging.nightpay.dev {
|
|
403
|
+
reverse_proxy 127.0.0.1:8091
|
|
404
|
+
}
|
|
405
|
+
bridge.staging.nightpay.dev {
|
|
406
|
+
reverse_proxy 127.0.0.1:4001
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Prerequisites
|
|
411
|
+
|
|
412
|
+
- [Masumi services](https://github.com/masumi-network/masumi-services-dev-quickstart)
|
|
413
|
+
- Midnight dev stack (bridge + proof server) with Preprod wallet (NIGHT + DUST)
|
|
414
|
+
|
|
116
415
|
## Platform Support
|
|
117
416
|
|
|
118
417
|
| Platform | Install |
|
|
119
418
|
|----------|---------|
|
|
120
|
-
| **OpenClaw** | `openclaw plugins install nightpay && openclaw plugins enable nightpay` |
|
|
121
|
-
| **Claude Code** | `npx nightpay setup` |
|
|
122
|
-
| **Cursor** | `npx nightpay setup` |
|
|
123
|
-
| **Copilot** | `npx nightpay setup` |
|
|
124
|
-
| **
|
|
419
|
+
| **OpenClaw** | `openclaw plugins install nightpay && openclaw plugins enable nightpay` (two-step; see [OPENCLAW_ONBOARDING.md](docs/OPENCLAW_ONBOARDING.md)) |
|
|
420
|
+
| **Claude Code** | `npx nightpay setup` (auto-creates `.claude/commands/nightpay.md`) |
|
|
421
|
+
| **Cursor** | `npx nightpay setup` (auto-creates `.cursor/rules/nightpay.md`) |
|
|
422
|
+
| **Copilot** | `npx nightpay setup` (appends to `.github/copilot-instructions.md`) |
|
|
423
|
+
| **ACP** | Same skill files, External Secrets for env |
|
|
424
|
+
| **Raw API** | `npx nightpay init` + bash/curl + env vars |
|
|
125
425
|
|
|
126
|
-
|
|
426
|
+
See [`docs/PLATFORM_MATRIX.md`](docs/PLATFORM_MATRIX.md) for the full compatibility matrix.
|
|
127
427
|
|
|
128
428
|
## Documentation
|
|
129
429
|
|
|
130
430
|
| Document | Description |
|
|
131
431
|
|----------|-------------|
|
|
132
|
-
| [`skills/nightpay/
|
|
133
|
-
| [`skills/nightpay/
|
|
134
|
-
| [`skills/nightpay/ontology/ontology.md`](skills/nightpay/ontology/ontology.md) |
|
|
135
|
-
| [`docs/
|
|
136
|
-
| [`docs/OPENCLAW_ONBOARDING.md`](docs/OPENCLAW_ONBOARDING.md) | Full OpenClaw setup guide |
|
|
432
|
+
| [`skills/nightpay/AGENTS.md`](skills/nightpay/AGENTS.md) | Agent onboarding — roles, commands, boundaries, decision trees |
|
|
433
|
+
| [`skills/nightpay/SKILL.md`](skills/nightpay/SKILL.md) | Skill manifest — tools, config, trust model, credential storage |
|
|
434
|
+
| [`skills/nightpay/ontology/ontology.md`](skills/nightpay/ontology/ontology.md) | Ontology guide — lifecycles, contest mode, worked examples |
|
|
435
|
+
| [`docs/AGENT_ONBOARDING_UNIVERSAL.md`](docs/AGENT_ONBOARDING_UNIVERSAL.md) | Per-platform setup guide |
|
|
137
436
|
| [`docs/PLATFORM_MATRIX.md`](docs/PLATFORM_MATRIX.md) | Feature availability across platforms |
|
|
437
|
+
| [`docs/AGENT_PLAYGROUND.md`](docs/AGENT_PLAYGROUND.md) | Step-by-step first job flow |
|
|
438
|
+
| [`docs/SHOWCASE_WIIFM_PLAYBOOK.md`](docs/SHOWCASE_WIIFM_PLAYBOOK.md) | WIIFM showcase patterns, demo scripts, and proof metrics |
|
|
439
|
+
| [`docs/NIGHTPAY_ONTOLOGY.md`](docs/NIGHTPAY_ONTOLOGY.md) | JSON-LD ontology model |
|
|
440
|
+
| [`docs/ECOSYSTEM.md`](docs/ECOSYSTEM.md) | Tracked repos + breaking changes |
|
|
138
441
|
|
|
139
442
|
## Built With
|
|
140
443
|
|
|
@@ -145,7 +448,9 @@ Full matrix → [`docs/PLATFORM_MATRIX.md`](docs/PLATFORM_MATRIX.md)
|
|
|
145
448
|
|
|
146
449
|
## License
|
|
147
450
|
|
|
148
|
-
|
|
451
|
+
This project is dual-licensed:
|
|
452
|
+
|
|
453
|
+
- **Open-source:** [GNU Affero General Public License v3 (AGPL-3.0)](https://github.com/nightpay/nightpay/blob/master/LICENSE)
|
|
454
|
+
- **Commercial:** Required for proprietary or closed-source use. Contact [hello@nightpay.dev](mailto:hello@nightpay.dev)
|
|
149
455
|
|
|
150
|
-
|
|
151
|
-
- **Commercial:** Contact [hello@nightpay.dev](mailto:hello@nightpay.dev)
|
|
456
|
+
See [LICENSE](https://github.com/nightpay/nightpay/blob/master/LICENSE) for the full license text.
|
package/package.json
CHANGED
|
@@ -6,19 +6,23 @@ The gateway sees the plaintext job description **in memory only** — long enoug
|
|
|
6
6
|
classify it, then immediately hashes it. The plaintext is never logged, persisted,
|
|
7
7
|
or transmitted. This preserves funder privacy while enforcing safety.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## How Classification Works
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Layer 3: External moderation API (AI-powered classification, optional)
|
|
11
|
+
Content safety is delegated to the bridge's **private decision layer**
|
|
12
|
+
(`POST /decision/content-check`). The bridge evaluates the job description using
|
|
13
|
+
internal heuristics that are not exposed publicly, then returns a signed decision
|
|
14
|
+
receipt:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{ "safe": false, "category": "violence", "decision_id": "...", "policy_version": "...", "sig": "..." }
|
|
19
18
|
```
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
The gateway acts on the `safe` boolean and logs only the `category`. The plaintext
|
|
21
|
+
description is never forwarded — only a hash is sent. The signed receipt is
|
|
22
|
+
auditable without revealing the rules used.
|
|
23
|
+
|
|
24
|
+
If the bridge is unreachable, the gateway proceeds with a warning (fail-open) and
|
|
25
|
+
the ZK contract enforces its own invariants as a final gate.
|
|
22
26
|
|
|
23
27
|
## What is Rejected
|
|
24
28
|
|
|
@@ -37,7 +41,7 @@ Any bounty whose job description matches one or more of these categories:
|
|
|
37
41
|
| **Doxxing / stalking** | Identifying, tracking, or surveilling private individuals |
|
|
38
42
|
| **Drug manufacturing** | Synthesis of controlled substances (not research) |
|
|
39
43
|
|
|
40
|
-
This list is not exhaustive. The
|
|
44
|
+
This list is not exhaustive. The bridge may apply additional categories.
|
|
41
45
|
|
|
42
46
|
## Enforcement Points
|
|
43
47
|
|
|
@@ -55,48 +59,6 @@ Two gates, defense-in-depth:
|
|
|
55
59
|
- The plaintext description is **not logged** — only the category name
|
|
56
60
|
- Exit code 2 (distinct from validation errors which use exit code 1)
|
|
57
61
|
|
|
58
|
-
## Auto-Updating Rules (update-blocklist.sh)
|
|
59
|
-
|
|
60
|
-
The static regex list goes stale. `update-blocklist.sh` keeps it current:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
# Run every 6 hours via cron
|
|
64
|
-
0 */6 * * * /path/to/scripts/update-blocklist.sh
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Sources
|
|
68
|
-
|
|
69
|
-
| Source | What It Provides | Update Frequency |
|
|
70
|
-
|---|---|---|
|
|
71
|
-
| **Base rules** (hardcoded) | 14 core patterns for the 10 categories | Static — code changes only |
|
|
72
|
-
| **stamparm/maltrail** | Malicious campaign names, known-bad keywords | Daily (GitHub raw) |
|
|
73
|
-
| **Community complaints** | Patterns derived from user reports that hit freeze threshold | Real-time (on flag) |
|
|
74
|
-
| **Operator custom rules** | `~/.nightpay/safety/custom-rules.json` | Operator-managed |
|
|
75
|
-
|
|
76
|
-
### Custom Rules Format
|
|
77
|
-
|
|
78
|
-
Operators can add domain-specific patterns in `~/.nightpay/safety/custom-rules.json`:
|
|
79
|
-
|
|
80
|
-
```json
|
|
81
|
-
{
|
|
82
|
-
"rules": [
|
|
83
|
-
{"category": "scam", "pattern": "\\b(ponzi|pyramid)\\b.*\\b(scheme|invest)\\b"},
|
|
84
|
-
{"category": "gambling", "pattern": "\\b(casino|betting|slots)\\b"}
|
|
85
|
-
]
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Invalid regex patterns are skipped with a warning, not fatal.
|
|
90
|
-
|
|
91
|
-
### Output
|
|
92
|
-
|
|
93
|
-
Rules are merged, deduplicated, and written atomically to:
|
|
94
|
-
```
|
|
95
|
-
~/.nightpay/safety/safety-rules.json
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
The gateway hot-loads this file on every `safety_check()` call — no restart required.
|
|
99
|
-
|
|
100
62
|
## Community Complaint System
|
|
101
63
|
|
|
102
64
|
Anyone can report a bounty they believe is harmful:
|
|
@@ -125,7 +87,6 @@ When a bounty reaches **3 complaints** (configurable via `COMPLAINT_FREEZE_THRES
|
|
|
125
87
|
1. Bounty status changes from `active` to `flagged`
|
|
126
88
|
2. Flagged bounties are hidden from `list` (no longer discoverable)
|
|
127
89
|
3. The complaint data is exported to `community-reports.json`
|
|
128
|
-
4. On next `update-blocklist.sh` run, complaint patterns feed back into rules
|
|
129
90
|
|
|
130
91
|
### Privacy Guarantees for Reporters
|
|
131
92
|
|
|
@@ -134,54 +95,9 @@ When a bounty reaches **3 complaints** (configurable via `COMPLAINT_FREEZE_THRES
|
|
|
134
95
|
- Complaint reasons are stored but the bounty description is not (it was never stored)
|
|
135
96
|
- Reporters cannot see other reporters' identities
|
|
136
97
|
|
|
137
|
-
### Feedback Loop
|
|
138
|
-
|
|
139
|
-
```
|
|
140
|
-
User reports bounty → complaint stored in SQLite
|
|
141
|
-
→ threshold hit → bounty auto-frozen
|
|
142
|
-
→ complaint categories exported to community-reports.json
|
|
143
|
-
→ update-blocklist.sh reads community-reports.json
|
|
144
|
-
→ trending complaint categories become new regex rules
|
|
145
|
-
→ gateway.sh loads updated safety-rules.json
|
|
146
|
-
→ future similar bounties blocked at post-bounty gate
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
## External Moderation API (Optional)
|
|
150
|
-
|
|
151
|
-
Set `CONTENT_SAFETY_URL` to enable AI-powered classification:
|
|
152
|
-
|
|
153
|
-
```
|
|
154
|
-
CONTENT_SAFETY_URL=http://localhost:8080/v1/classify
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
The gateway sends a POST with `{"text": "<job_description>"}` and expects:
|
|
158
|
-
```json
|
|
159
|
-
{"safe": false, "category": "violence", "confidence": 0.97}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
If the API is unavailable, layers 1-2 still protect. The API is additive.
|
|
163
|
-
The API response is **not logged** — only the boolean decision.
|
|
164
|
-
|
|
165
|
-
### Recommended External APIs
|
|
166
|
-
|
|
167
|
-
- **Anthropic content classification** — context-aware, low false positive rate
|
|
168
|
-
- **OpenAI moderation endpoint** — free tier available, 11 categories
|
|
169
|
-
- **Self-hosted models** — full control, no data leaves your infrastructure
|
|
170
|
-
|
|
171
98
|
## Privacy Guarantee
|
|
172
99
|
|
|
173
100
|
- Job descriptions are **never logged**, even rejected ones
|
|
174
101
|
- Only the rejection category name appears in output
|
|
175
|
-
- The external API call uses `--max-time 5` — no hanging on moderation
|
|
176
102
|
- After classification, the plaintext variable is unset in the shell
|
|
177
103
|
- Reporter identities are hashed — complaints are pseudonymous
|
|
178
|
-
|
|
179
|
-
## Configuration
|
|
180
|
-
|
|
181
|
-
| Env Var | Default | Purpose |
|
|
182
|
-
|---|---|---|
|
|
183
|
-
| `CONTENT_SAFETY_URL` | (empty) | External moderation API endpoint |
|
|
184
|
-
| `SAFETY_RULES_FILE` | `~/.nightpay/safety/safety-rules.json` | Live rules file path |
|
|
185
|
-
| `COMPLAINT_FREEZE_THRESHOLD` | `3` | Complaints before auto-freeze |
|
|
186
|
-
| `REPORTER_ID` | `anonymous` | Reporter identity (hashed for dedup) |
|
|
187
|
-
| `BOARD_HMAC_KEY` | (required) | Board integrity HMAC key |
|
|
@@ -245,10 +245,35 @@ generate_nonce() {
|
|
|
245
245
|
openssl rand -hex 32 | tr -d '[:space:]'
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
# SECURITY: rate limiter —
|
|
249
|
-
#
|
|
248
|
+
# SECURITY: rate limiter — enforced server-side by bridge /decision/rate-check.
|
|
249
|
+
# Local lockfile is a secondary fallback when bridge is unreachable.
|
|
250
250
|
rate_limit() {
|
|
251
251
|
local cmd="$1"
|
|
252
|
+
|
|
253
|
+
# ── Primary: bridge-enforced rate limit (per operator, server-side) ──────────
|
|
254
|
+
if [[ -n "$BRIDGE_URL" ]]; then
|
|
255
|
+
local rl_payload; rl_payload=$(python3 -c "import sys,json; print(json.dumps({'command': sys.argv[1]}))" "$cmd")
|
|
256
|
+
local rl_status
|
|
257
|
+
rl_status=$(curl -sf --max-time 5 -w '%{http_code}' -o /tmp/_nightpay_rl.$$ \
|
|
258
|
+
-X POST -H 'Content-Type: application/json' \
|
|
259
|
+
-d "$rl_payload" \
|
|
260
|
+
"${BRIDGE_URL}/decision/rate-check" 2>/dev/null) || rl_status="000"
|
|
261
|
+
local rl_body; rl_body=$(cat /tmp/_nightpay_rl.$$ 2>/dev/null); rm -f /tmp/_nightpay_rl.$$
|
|
262
|
+
|
|
263
|
+
if [[ "$rl_status" == "429" ]]; then
|
|
264
|
+
local retry_after decision_id
|
|
265
|
+
retry_after=$(echo "$rl_body" | python3 -c "import sys,json; print(json.load(sys.stdin).get('retry_after',30))" 2>/dev/null || echo "30")
|
|
266
|
+
decision_id=$(echo "$rl_body" | python3 -c "import sys,json; print(json.load(sys.stdin).get('decision_id',''))" 2>/dev/null || echo "")
|
|
267
|
+
echo -e "${RED}ERROR${RESET}: Rate limit — wait ${BOLD}${retry_after}s${RESET} before calling ${CYAN}$cmd${RESET} again (decision: ${decision_id})" >&2
|
|
268
|
+
exit 1
|
|
269
|
+
fi
|
|
270
|
+
# 200 = allowed; any other non-000 status = bridge error, fall through to local
|
|
271
|
+
if [[ "$rl_status" == "200" ]]; then
|
|
272
|
+
return 0
|
|
273
|
+
fi
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
# ── Fallback: local lockfile (bridge unreachable or not configured) ────────
|
|
252
277
|
mkdir -p "$RATE_LIMIT_DIR"
|
|
253
278
|
chmod 700 "$RATE_LIMIT_DIR"
|
|
254
279
|
local lockfile="${RATE_LIMIT_DIR}/${cmd}.last"
|
|
@@ -419,115 +444,64 @@ require_operator_auth() {
|
|
|
419
444
|
|
|
420
445
|
# ─── Content Safety ────────────────────────────────────────────────────────────
|
|
421
446
|
# SAFETY: classify-then-forget — checks job description in-memory, never logs it.
|
|
422
|
-
#
|
|
423
|
-
# Rules
|
|
424
|
-
|
|
425
|
-
CONTENT_SAFETY_URL="${CONTENT_SAFETY_URL:-}"
|
|
426
|
-
SAFETY_RULES_FILE="${SAFETY_RULES_FILE:-${HOME}/.nightpay/safety/safety-rules.json}"
|
|
447
|
+
# Content safety: delegated to bridge /decision/content-check.
|
|
448
|
+
# Rules and patterns are private — only the signed verdict is returned here.
|
|
449
|
+
# See rules/content-safety.md for the public policy description.
|
|
427
450
|
|
|
428
451
|
safety_check() {
|
|
429
452
|
local text="$1"
|
|
430
453
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
454
|
+
if [[ -z "$BRIDGE_URL" ]]; then
|
|
455
|
+
# No bridge configured — fail open with a warning.
|
|
456
|
+
echo -e " ${YELLOW}WARNING${RESET}: BRIDGE_URL not set — content safety check skipped" >&2
|
|
457
|
+
return 0
|
|
458
|
+
fi
|
|
434
459
|
|
|
435
|
-
|
|
436
|
-
|
|
460
|
+
local payload
|
|
461
|
+
payload=$(python3 -c "import sys,json; print(json.dumps({'text': sys.argv[1]}))" "$text")
|
|
437
462
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
# ─── Layer 2: hardcoded fallback if no rules file or it failed to load
|
|
450
|
-
if not rules:
|
|
451
|
-
rules = [
|
|
452
|
-
('csam', r'\b(child|minor|underage|kid|teen)\b.{0,100}?\b(sex|porn|nude|naked|exploit)\b'),
|
|
453
|
-
('csam', r'\b(sex|porn|nude|naked|exploit)\b.{0,100}?\b(child|minor|underage|kid|teen)\b'),
|
|
454
|
-
('violence', r'\b(kill|assassinate|murder|execute)\b.{0,100}?\b(person|people|someone|him|her|them|target)\b'),
|
|
455
|
-
('violence', r'\b(hire|find|pay).{0,100}?\b(hitman|killer|assassin)\b'),
|
|
456
|
-
('violence', r'\bhit\s*man\b'),
|
|
457
|
-
('weapons_of_mass_destruction', r'\b(synthe|build|make|create|assemble)\b.{0,100}?\b(bomb|bioweapon|chemical weapon|nerve agent|sarin|anthrax|ricin|nuclear|dirty bomb|explosive device)\b'),
|
|
458
|
-
('human_trafficking', r'\b(traffic|smuggle|exploit|enslave)\b.{0,100}?\b(person|people|human|worker|organ|women|children)\b'),
|
|
459
|
-
('terrorism', r'\b(fund|finance|recruit|plan|support)\b.{0,100}?\b(terror|jihad|extremis|insurrection|attack on)\b'),
|
|
460
|
-
('ncii', r'\b(deepfake|revenge porn|sextortion|non.?consensual)\b.{0,100}?\b(nude|naked|intimate|image|video|photo)\b'),
|
|
461
|
-
('financial_fraud', r'\b(launder|counterfeit|forge)\b.{0,100}?\b(money|currency|documents|passport|identity)\b'),
|
|
462
|
-
('financial_fraud', r'\b(evade|bypass|circumvent)\b.{0,100}?\b(sanction|embargo|aml|kyc)\b'),
|
|
463
|
-
('infrastructure_attack', r'\b(attack|hack|disrupt|destroy|sabotage)\b.{0,100}?\b(power grid|water supply|hospital|election|pipeline|dam)\b'),
|
|
464
|
-
('doxxing', r'\b(doxx|stalk|track|surveil|locate)\b.{0,100}?\b(person|address|home|family|where .{0,100}? live)\b'),
|
|
465
|
-
('drug_manufacturing', r'\b(synthe|cook|manufacture|produce)\b.{0,100}?\b(meth|fentanyl|heroin|cocaine|mdma|lsd)\b'),
|
|
466
|
-
]
|
|
467
|
-
|
|
468
|
-
for category, pattern in rules:
|
|
469
|
-
try:
|
|
470
|
-
if re.search(pattern, text):
|
|
471
|
-
print(category)
|
|
472
|
-
sys.exit(0)
|
|
473
|
-
except re.error:
|
|
474
|
-
continue # skip malformed patterns from feeds
|
|
475
|
-
|
|
476
|
-
print('safe')
|
|
477
|
-
" "$text" "$SAFETY_RULES_FILE" 2>/dev/null) || rejected_category="safe"
|
|
478
|
-
|
|
479
|
-
# ─── Layer 3: external moderation API (catches what regex misses)
|
|
480
|
-
if [[ "$rejected_category" == "safe" && -n "$CONTENT_SAFETY_URL" ]]; then
|
|
481
|
-
local api_payload
|
|
482
|
-
api_payload=$(python3 -c "
|
|
483
|
-
import sys, json
|
|
484
|
-
print(json.dumps({'text': sys.argv[1]}))
|
|
485
|
-
" "$text")
|
|
486
|
-
local response
|
|
487
|
-
response=$(curl -sf --max-time 5 -X POST \
|
|
488
|
-
-H 'Content-Type: application/json' \
|
|
489
|
-
-d "$api_payload" \
|
|
490
|
-
"$CONTENT_SAFETY_URL" 2>/dev/null) || response=""
|
|
491
|
-
|
|
492
|
-
if [[ -n "$response" ]]; then
|
|
493
|
-
rejected_category=$(echo "$response" | python3 -c "
|
|
494
|
-
import sys, json
|
|
495
|
-
try:
|
|
496
|
-
d = json.load(sys.stdin)
|
|
497
|
-
if not d.get('safe', True):
|
|
498
|
-
print(d.get('category', 'unsafe'))
|
|
499
|
-
else:
|
|
500
|
-
print('safe')
|
|
501
|
-
except: print('safe')
|
|
502
|
-
" 2>/dev/null) || rejected_category="safe"
|
|
503
|
-
fi
|
|
463
|
+
local response http_status
|
|
464
|
+
http_status=$(curl -sf --max-time 10 -w '%{http_code}' -o /tmp/_nightpay_safety.$$ \
|
|
465
|
+
-X POST -H 'Content-Type: application/json' \
|
|
466
|
+
-d "$payload" \
|
|
467
|
+
"${BRIDGE_URL}/decision/content-check" 2>/dev/null) || http_status="000"
|
|
468
|
+
response=$(cat /tmp/_nightpay_safety.$$ 2>/dev/null); rm -f /tmp/_nightpay_safety.$$
|
|
469
|
+
|
|
470
|
+
if [[ "$http_status" == "000" || -z "$response" ]]; then
|
|
471
|
+
echo -e " ${YELLOW}WARNING${RESET}: Bridge content-check unreachable — proceeding with caution" >&2
|
|
472
|
+
return 0
|
|
504
473
|
fi
|
|
505
474
|
|
|
506
|
-
|
|
475
|
+
local is_safe category decision_id policy_version
|
|
476
|
+
is_safe=$(echo "$response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('safe','true'))" 2>/dev/null || echo "true")
|
|
477
|
+
category=$(echo "$response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('category',''))" 2>/dev/null || echo "")
|
|
478
|
+
decision_id=$(echo "$response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('decision_id',''))" 2>/dev/null || echo "")
|
|
479
|
+
policy_version=$(echo "$response" | python3 -c "import sys,json; print(json.load(sys.stdin).get('policy_version',''))" 2>/dev/null || echo "")
|
|
480
|
+
|
|
481
|
+
if [[ "$is_safe" != "True" && "$is_safe" != "true" ]]; then
|
|
507
482
|
python3 -c "
|
|
508
483
|
import sys, json
|
|
509
484
|
print(json.dumps({
|
|
510
485
|
'status': 'REJECTED',
|
|
511
486
|
'reason': 'content_safety',
|
|
512
487
|
'category': sys.argv[1],
|
|
488
|
+
'decision_id': sys.argv[2],
|
|
489
|
+
'policy_version': sys.argv[3],
|
|
513
490
|
'message': 'This bounty description was rejected by the content safety gate. See rules/content-safety.md.'
|
|
514
491
|
}, indent=2))
|
|
515
|
-
" "$
|
|
492
|
+
" "$category" "$decision_id" "$policy_version"
|
|
516
493
|
exit 2
|
|
517
494
|
fi
|
|
518
495
|
}
|
|
519
496
|
|
|
520
|
-
# ─── Optimistic delivery
|
|
497
|
+
# ─── Optimistic delivery env vars ─────────────────────────────────────────────
|
|
498
|
+
# Multisig keys/threshold/M/N are now private to the bridge (APPROVER_KEYS,
|
|
499
|
+
# MULTISIG_M, MULTISIG_THRESHOLD_SPECKS env vars on the bridge process).
|
|
500
|
+
# This script forwards approval blobs to /decision/approve-completion; bridge verifies.
|
|
521
501
|
OPTIMISTIC_WINDOW_HOURS="${OPTIMISTIC_WINDOW_HOURS:-48}"
|
|
522
|
-
MULTISIG_THRESHOLD_SPECKS="${MULTISIG_THRESHOLD_SPECKS:-1000000}"
|
|
523
|
-
MULTISIG_M="${MULTISIG_M:-2}"
|
|
524
|
-
MULTISIG_N="${MULTISIG_N:-3}"
|
|
525
502
|
OPTIMISTIC_SWEEP_PAGE_SIZE="${OPTIMISTIC_SWEEP_PAGE_SIZE:-200}" # capped to <= 500
|
|
526
503
|
UNCLAIMED_REFUND_HOURS="${UNCLAIMED_REFUND_HOURS:-24}"
|
|
527
504
|
UNCLAIMED_SWEEP_PAGE_SIZE="${UNCLAIMED_SWEEP_PAGE_SIZE:-200}" # capped to <= 500
|
|
528
|
-
# APPROVER_KEYS: comma-separated HMAC secrets, one per approver
|
|
529
|
-
# e.g. APPROVER_KEYS="key1secret,key2secret,key3secret"
|
|
530
|
-
APPROVER_KEYS="${APPROVER_KEYS:-}"
|
|
531
505
|
MIP003_PORT="${MIP003_PORT:-8090}"
|
|
532
506
|
MIP003_URL="${MIP003_URL:-http://localhost:${MIP003_PORT}}"
|
|
533
507
|
# Optional x402 passthrough for MIP-003 APIs that enforce PAYMENT-SIGNATURE.
|
|
@@ -746,73 +720,51 @@ except: print(0)
|
|
|
746
720
|
" 2>/dev/null || echo 0)
|
|
747
721
|
fi
|
|
748
722
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
723
|
+
# ── Payout gate: bridge /decision/approve-completion ────────────────────────
|
|
724
|
+
# Approval keys and threshold are private to the bridge; this script only
|
|
725
|
+
# forwards the job_id, output_hash, amount, and any collected approval blobs.
|
|
726
|
+
if [[ -n "$BRIDGE_URL" ]]; then
|
|
727
|
+
APPROVE_PAYLOAD=$(python3 -c "
|
|
728
|
+
import sys, json
|
|
729
|
+
d = {'job_id': sys.argv[1], 'output_hash': sys.argv[2], 'amount_specks': int(sys.argv[3] or 0)}
|
|
730
|
+
if sys.argv[4]:
|
|
731
|
+
d['approvals'] = sys.argv[4]
|
|
732
|
+
print(json.dumps(d))
|
|
733
|
+
" "$JOB_ID" "$COMMITMENT" "$JOB_AMOUNT" "${APPROVALS_RAW:-}")
|
|
734
|
+
|
|
735
|
+
APPROVE_STATUS=$(curl -sf --max-time 10 -w '%{http_code}' -o /tmp/_nightpay_approve.$$ \
|
|
736
|
+
-X POST -H 'Content-Type: application/json' \
|
|
737
|
+
-d "$APPROVE_PAYLOAD" \
|
|
738
|
+
"${BRIDGE_URL}/decision/approve-completion" 2>/dev/null) || APPROVE_STATUS="000"
|
|
739
|
+
APPROVE_BODY=$(cat /tmp/_nightpay_approve.$$ 2>/dev/null); rm -f /tmp/_nightpay_approve.$$
|
|
740
|
+
|
|
741
|
+
if [[ "$APPROVE_STATUS" == "403" ]]; then
|
|
742
|
+
APPROVE_REASON=$(echo "$APPROVE_BODY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('reason_code','unknown'))" 2>/dev/null || echo "unknown")
|
|
743
|
+
APPROVE_DID=$(echo "$APPROVE_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('decision_id',''))" 2>/dev/null || echo "")
|
|
744
|
+
if [[ "$APPROVE_REASON" == "multisig_required" ]]; then
|
|
745
|
+
REQ_M=$(echo "$APPROVE_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('required_m',2))" 2>/dev/null || echo "2")
|
|
746
|
+
REQ_N=$(echo "$APPROVE_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('required_n',3))" 2>/dev/null || echo "3")
|
|
747
|
+
echo -e "${RED}ERROR${RESET}: job amount ${BOLD}${JOB_AMOUNT} specks${RESET} requires multisig (${REQ_M}-of-${REQ_N})" >&2
|
|
748
|
+
echo -e "${YELLOW}Each approver runs:${RESET}" >&2
|
|
749
|
+
echo -e " ${CYAN}gateway.sh approve-multisig${RESET} $JOB_ID <output_hash> <approver_key>" >&2
|
|
750
|
+
echo -e "Then collect M approval_blobs and run:" >&2
|
|
751
|
+
echo -e " ${CYAN}gateway.sh complete${RESET} $JOB_ID $COMMITMENT --approvals blob1,blob2" >&2
|
|
752
|
+
echo -e " ${DIM}(decision: ${APPROVE_DID})${RESET}" >&2
|
|
753
|
+
else
|
|
754
|
+
APPROVE_ERR=$(echo "$APPROVE_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('error','multisig verification failed'))" 2>/dev/null || echo "multisig verification failed")
|
|
755
|
+
echo -e "${RED}SECURITY ERROR${RESET}: completion not approved — ${APPROVE_ERR} (decision: ${APPROVE_DID})" >&2
|
|
756
|
+
fi
|
|
756
757
|
exit 1
|
|
757
758
|
fi
|
|
758
759
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
import sys, json
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
required_m = int(sys.argv[5])
|
|
768
|
-
max_age_secs = 86400 # approvals expire after 24h — prevents replay attacks
|
|
769
|
-
|
|
770
|
-
# Parse: each entry is sig:ts:nonce
|
|
771
|
-
approvals = []
|
|
772
|
-
for entry in approvals_raw.split(','):
|
|
773
|
-
parts = entry.split(':')
|
|
774
|
-
if len(parts) != 3:
|
|
775
|
-
print(f'ERROR: malformed approval blob (expected sig:ts:nonce): {entry}', file=sys.stderr)
|
|
776
|
-
sys.exit(1)
|
|
777
|
-
try:
|
|
778
|
-
approvals.append({'sig': parts[0], 'ts': int(parts[1]), 'nonce': parts[2]})
|
|
779
|
-
except ValueError:
|
|
780
|
-
print(f'ERROR: non-integer timestamp in approval: {entry}', file=sys.stderr)
|
|
781
|
-
sys.exit(1)
|
|
782
|
-
|
|
783
|
-
now = int(time.time())
|
|
784
|
-
valid_count = 0
|
|
785
|
-
used_keys = set() # SECURITY: each key index counts once — no double-counting
|
|
786
|
-
|
|
787
|
-
for approval in approvals:
|
|
788
|
-
age = now - approval['ts']
|
|
789
|
-
if age > max_age_secs:
|
|
790
|
-
print(f'WARN: approval ts={approval[\"ts\"]} is too old (age={age}s > {max_age_secs}s)', file=sys.stderr)
|
|
791
|
-
continue
|
|
792
|
-
if age < -300: # 5-min future clock skew tolerance
|
|
793
|
-
print(f'WARN: approval ts={approval[\"ts\"]} is too far in future (age={age}s)', file=sys.stderr)
|
|
794
|
-
continue
|
|
795
|
-
|
|
796
|
-
payload = f'{job_id}:{output_hash}:{approval[\"ts\"]}:{approval[\"nonce\"]}'
|
|
797
|
-
for i, key in enumerate(approver_keys):
|
|
798
|
-
if i in used_keys:
|
|
799
|
-
continue
|
|
800
|
-
expected = hmac.new(key.encode(), payload.encode(), hashlib.sha256).hexdigest()
|
|
801
|
-
if hmac.compare_digest(expected, approval['sig']):
|
|
802
|
-
used_keys.add(i)
|
|
803
|
-
valid_count += 1
|
|
804
|
-
break
|
|
805
|
-
|
|
806
|
-
if valid_count >= required_m:
|
|
807
|
-
print(f'ok:{valid_count}')
|
|
808
|
-
else:
|
|
809
|
-
print(f'ERROR: only {valid_count} valid approvals, need {required_m}', file=sys.stderr)
|
|
810
|
-
sys.exit(1)
|
|
811
|
-
" "$JOB_ID" "$COMMITMENT" "$APPROVALS_RAW" "$APPROVER_KEYS" "$MULTISIG_M" 2>&1) || {
|
|
812
|
-
echo -e "${RED}SECURITY ERROR${RESET}: multisig verification failed — $VERIFY_OK" >&2
|
|
813
|
-
exit 1
|
|
814
|
-
}
|
|
815
|
-
echo -e " ${GREEN}Multisig${RESET}: $VERIFY_OK approvals verified" >&2
|
|
760
|
+
if [[ "$APPROVE_STATUS" == "200" ]]; then
|
|
761
|
+
APPROVE_DID=$(echo "$APPROVE_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('decision_id',''))" 2>/dev/null || echo "")
|
|
762
|
+
APPROVE_VALID=$(echo "$APPROVE_BODY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('valid_count',''))" 2>/dev/null || echo "")
|
|
763
|
+
if [[ -n "$APPROVE_VALID" ]]; then
|
|
764
|
+
echo -e " ${GREEN}Multisig${RESET}: ok:${APPROVE_VALID} approvals verified (decision: ${APPROVE_DID})" >&2
|
|
765
|
+
fi
|
|
766
|
+
fi
|
|
767
|
+
# On bridge error (000, 5xx) fall through — let the ZK circuit be the final gate
|
|
816
768
|
fi
|
|
817
769
|
|
|
818
770
|
RESULT_DATA=$(masumi_get "/purchases/$JOB_ID/result")
|
|
@@ -953,6 +905,32 @@ print(json.dumps({
|
|
|
953
905
|
exit 1
|
|
954
906
|
fi
|
|
955
907
|
|
|
908
|
+
# ── Authorization gate: bridge /decision/initiate-refund ─────────────────
|
|
909
|
+
# Requires operator Bearer token (OPERATOR_SECRET_KEY) verified by bridge.
|
|
910
|
+
if [[ -n "$BRIDGE_URL" ]]; then
|
|
911
|
+
REFUND_AUTH_PAYLOAD=$(python3 -c "import sys,json; print(json.dumps({'job_id':sys.argv[1],'commitment_hash':sys.argv[2]}))" "$JOB_ID" "$COMMITMENT")
|
|
912
|
+
REFUND_AUTH_STATUS=$(curl -sf --max-time 10 -w '%{http_code}' -o /tmp/_nightpay_refundauth.$$ \
|
|
913
|
+
-X POST \
|
|
914
|
+
-H 'Content-Type: application/json' \
|
|
915
|
+
-H "Authorization: Bearer ${OPERATOR_SECRET_KEY:-}" \
|
|
916
|
+
-d "$REFUND_AUTH_PAYLOAD" \
|
|
917
|
+
"${BRIDGE_URL}/decision/initiate-refund" 2>/dev/null) || REFUND_AUTH_STATUS="000"
|
|
918
|
+
REFUND_AUTH_BODY=$(cat /tmp/_nightpay_refundauth.$$ 2>/dev/null); rm -f /tmp/_nightpay_refundauth.$$
|
|
919
|
+
|
|
920
|
+
if [[ "$REFUND_AUTH_STATUS" == "401" || "$REFUND_AUTH_STATUS" == "403" ]]; then
|
|
921
|
+
REFUND_AUTH_ERR=$(echo "$REFUND_AUTH_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('error','unauthorized'))" 2>/dev/null || echo "unauthorized")
|
|
922
|
+
echo -e "${RED}SECURITY ERROR${RESET}: refund not authorized — ${REFUND_AUTH_ERR}" >&2
|
|
923
|
+
echo -e "${DIM}Ensure OPERATOR_SECRET_KEY is set and BRIDGE_ADMIN_TOKEN matches on the bridge.${RESET}" >&2
|
|
924
|
+
exit 1
|
|
925
|
+
fi
|
|
926
|
+
|
|
927
|
+
if [[ "$REFUND_AUTH_STATUS" == "200" ]]; then
|
|
928
|
+
REFUND_DID=$(echo "$REFUND_AUTH_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('decision_id',''))" 2>/dev/null || echo "")
|
|
929
|
+
echo -e " ${GREEN}Refund authorized${RESET} (decision: ${REFUND_DID})" >&2
|
|
930
|
+
fi
|
|
931
|
+
# On bridge error (000, 5xx) — fall through; Masumi cancel is idempotent-safe
|
|
932
|
+
fi
|
|
933
|
+
|
|
956
934
|
# Step 1: Cancel Masumi escrow on Cardano
|
|
957
935
|
echo -e "${CYAN}Cancelling Masumi escrow${RESET} for job ${BOLD}$JOB_ID${RESET}..." >&2
|
|
958
936
|
masumi_post "/purchases/$JOB_ID/cancel" "{}"
|