pivx-402 1.0.0

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/AGENTS.md ADDED
@@ -0,0 +1,261 @@
1
+ # AGENTS.md: pivx-402 for AI agents
2
+
3
+ For **AI agents** (Claude, GPT-class models, autonomous task runners,
4
+ tool-using LLM apps) and the engineers wiring them up.
5
+
6
+ Two roles:
7
+
8
+ 1. **Consumer**: an agent that *pays* a 402-gated PIVX endpoint to use it.
9
+ 2. **Builder**: an agent (or a human plus agent) that *builds* such an
10
+ endpoint using this library.
11
+
12
+ Both paths are short and predictable. There's no UI, no out-of-band auth,
13
+ no per-customer accounts. The protocol is the account.
14
+
15
+ ---
16
+
17
+ ## Why this works well for agents
18
+
19
+ - **No accounts.** No signup form to fill out, no API key to leak in logs.
20
+ The only credential is funds the agent already controls.
21
+ - **Machine-readable price.** The 402 response is a versioned JSON envelope.
22
+ Agents don't have to scrape a pricing page.
23
+ - **Stable error codes.** Every verification failure returns a known
24
+ `reason` string (see README), so an agent can react programmatically:
25
+ "wait for confirmations", "top up wallet", "retry with bigger amount".
26
+ - **One handshake.** No webhooks, no callbacks, no polling: request, pay,
27
+ retry, done.
28
+ - **Shielded payments available.** For agents that need to spend without
29
+ revealing amounts on-chain (price-discovery probes, competitive ops),
30
+ switch the scheme to `pivx-shield`. Same envelope, same proof shape.
31
+
32
+ ---
33
+
34
+ ## Role 1: An agent that pays an endpoint
35
+
36
+ ### Minimal recipe
37
+
38
+ ```ts
39
+ import { payAndFetch, type Payer } from "pivx-402";
40
+
41
+ const payer: Payer = async (req) => {
42
+ // req.scheme: "pivx-transparent" | "pivx-shield"
43
+ // req.payTo: PIVX address
44
+ // req.maxAmountRequired: decimal PIV (string), e.g. "0.0001"
45
+ // req.nonce: server-issued; MUST appear in OP_RETURN or shielded memo
46
+ // req.network: "pivx-mainnet" | "pivx-testnet" | "pivx-regtest"
47
+ //
48
+ // Build, sign, broadcast. Return the broadcast txid.
49
+ return await myWallet.sendWithOpReturn(req.payTo, req.maxAmountRequired, req.nonce);
50
+ };
51
+
52
+ const { response } = await payAndFetch("https://api.example.com/paid-thing", payer);
53
+ const data = await response.json();
54
+ ```
55
+
56
+ That's the whole loop. `payAndFetch`:
57
+
58
+ 1. GETs the URL.
59
+ 2. If it's 200, returns immediately.
60
+ 3. If it's 402, decodes `X-Payment-Required`, calls your `payer`, retries
61
+ with `X-Payment` set to the proof.
62
+ 4. Anything else surfaces on `response` for you to handle.
63
+
64
+ ### Wallets / signing options
65
+
66
+ Agents typically reach the chain through one of:
67
+
68
+ - A **local `pivxd`** the agent operator controls. Most production-quality
69
+ option; use the JSON-RPC `sendmany` / `shieldsendmany` to embed the nonce.
70
+ `demo/pay-cli.ts` is a working reference.
71
+ - A **custodial signing service** (your wallet provider's HTTP API).
72
+ - A **hosted PIVX node** accessed over HTTPS.
73
+
74
+ The library doesn't care which. `Payer` is intentionally one async
75
+ function: given a requirement, return a broadcast txid.
76
+
77
+ ### Failure modes and what an agent should do
78
+
79
+ | `error` reason | Cause | What the agent should do |
80
+ | ----------------------------- | ---------------------------------------- | -------------------------------------------------------- |
81
+ | `tx_not_found` | Propagation delay, or wrong network | Wait a few seconds and re-submit the same proof. |
82
+ | `insufficient_confirmations` | Tx broadcast but not yet deep enough | Wait, then re-submit. Don't re-pay. |
83
+ | `insufficient_amount` | You paid less than `maxAmountRequired` | Re-pay with at least the quoted amount on a new nonce. |
84
+ | `wrong_recipient` | Wrong `payTo` | Re-pay to the correct address on a new nonce. |
85
+ | `missing_nonce` | Forgot the `OP_RETURN` / memo | Re-pay including the nonce. Don't reuse old txid. |
86
+ | `nonce_replayed` | This nonce was already spent | Re-issue (`GET` again to get a fresh nonce), re-pay. |
87
+ | `scheme_unsupported` | Used wrong scheme | Re-pay with the scheme the server advertises. |
88
+ | `network_mismatch` | Wrong network | Switch the network on the wallet / endpoint. |
89
+ | `shielded_backend_unavailable`| Server misconfigured for shield | Fall back to transparent if the server lists both. |
90
+
91
+ The error JSON is small enough to feed back into the model. A good agent
92
+ prompt: *"If the response contains `error: nonce_replayed`, re-issue the
93
+ request to obtain a new nonce; do not re-broadcast the old tx."*
94
+
95
+ ### Budgets and guardrails
96
+
97
+ Agents that pay real money should have:
98
+
99
+ - **A per-call price ceiling.** Inspect `requirement.maxAmountRequired` before
100
+ calling the `payer`. Refuse anything over your budget.
101
+ - **A per-session spending cap.** Track total paid; halt above a threshold.
102
+ - **A whitelist of `payTo` addresses or domains.** Don't pay an arbitrary
103
+ address an attacker injected into a 402.
104
+ - **Idempotency tracking.** Store `txid` against the original request so a
105
+ retry doesn't pay twice.
106
+
107
+ A simple wrapper:
108
+
109
+ ```ts
110
+ const MAX_PIV_PER_CALL = "0.001";
111
+ const MAX_PIV_PER_SESSION = "0.05";
112
+ let spent = 0n;
113
+
114
+ const guardedPayer: Payer = async (req) => {
115
+ const need = pivToSats(req.maxAmountRequired);
116
+ if (need > pivToSats(MAX_PIV_PER_CALL)) throw new Error("over per-call cap");
117
+ if (spent + need > pivToSats(MAX_PIV_PER_SESSION)) throw new Error("session cap");
118
+ const txid = await payer(req);
119
+ spent += need;
120
+ return txid;
121
+ };
122
+ ```
123
+
124
+ `pivToSats` is exported from this package.
125
+
126
+ ---
127
+
128
+ ## Role 2: An agent (or LLM-assisted dev) building a 402-gated endpoint
129
+
130
+ This is the workflow the model should follow when a user asks for a
131
+ "pay-per-request endpoint with PIVX" or "x402 endpoint for my API".
132
+
133
+ ### Decision tree
134
+
135
+ 1. **Does the resource have to be private on-chain?**
136
+ - If **no** (typical case): `scheme: "pivx-transparent"`. Cheapest, works
137
+ with any backend including explorers.
138
+ - If **yes** (amount/sender must be hidden): `scheme: "pivx-shield"`. The
139
+ server must run `pivxd` with the viewing key for the receiving shielded
140
+ address.
141
+ 2. **Where does on-chain data come from?**
142
+ - Local `pivxd` → `NodeRpcBackend` (required for shield).
143
+ - Public explorer → `ExplorerBackend` (transparent only; trust the explorer).
144
+ 3. **What confirmations?**
145
+ - `minConfirmations: 0` for tiny prices (<<$0.10 worth of PIV).
146
+ - `minConfirmations: 1` for default.
147
+ - `minConfirmations: 6` for anything you'd be sad to lose to a re-org.
148
+ 4. **What price?**
149
+ - Static for a single SKU: `price: { amount: "0.001", payTo: "...", description: "..." }`.
150
+ - Per-request (dynamic): pass a function. Async is fine, and useful
151
+ when the price depends on a database row, the request body, or a
152
+ real-time rate.
153
+ 5. **What nonce store?**
154
+ - Single-process / dev: omit, defaults to in-memory.
155
+ - Production: implement `NonceStore` against Redis/Postgres. Just two
156
+ async methods. Don't skip this. Replay protection lives here.
157
+
158
+ ### Template the agent should generate
159
+
160
+ ```ts
161
+ import express from "express";
162
+ import { pivx402, NodeRpcBackend } from "pivx-402";
163
+
164
+ const app = express();
165
+
166
+ app.get(
167
+ "/path/to/resource",
168
+ pivx402({
169
+ backend: new NodeRpcBackend({
170
+ url: process.env.PIVX_RPC_URL!,
171
+ username: process.env.PIVX_RPC_USER,
172
+ password: process.env.PIVX_RPC_PASSWORD,
173
+ }),
174
+ network: "pivx-mainnet", // or pivx-testnet / pivx-regtest
175
+ scheme: "pivx-transparent", // or pivx-shield
176
+ minConfirmations: 1,
177
+ price: {
178
+ amount: "0.0001",
179
+ payTo: process.env.PIVX_PAY_TO!,
180
+ description: "what they're buying",
181
+ },
182
+ }),
183
+ (req, res) => {
184
+ // req.pivx402.txid is the on-chain proof of payment for this request.
185
+ // Use it for receipts, audit logs, or download tokens.
186
+ res.send(theThing);
187
+ },
188
+ );
189
+
190
+ app.listen(Number(process.env.PORT ?? 4403));
191
+ ```
192
+
193
+ ### Things the agent should NOT do
194
+
195
+ - **Do not roll your own verification.** Use `pivx402(...)`. The verification
196
+ logic (output aggregation, OP_RETURN parsing, memo decryption, nonce
197
+ claiming) has subtle edge cases that are already tested in
198
+ `test/verifier*.test.ts`.
199
+ - **Do not weaken the nonce check.** The OP_RETURN / memo nonce is what
200
+ binds a public payment to a *specific server-issued challenge*. Without it
201
+ any payment to `payTo` would unlock the resource for everyone.
202
+ - **Do not reuse a single `NonceStore` instance across unrelated routes
203
+ without thinking about it.** Different routes can share one, but each
204
+ successful payment burns its nonce. That's the whole point: the same
205
+ txid plus nonce cannot satisfy two routes.
206
+ - **Do not commit RPC credentials or `payTo` private keys.** `payTo` is a
207
+ public address, which is fine in code or config. The private key for
208
+ it belongs in the `pivxd` wallet only.
209
+ - **Do not catch and silently 200 on verification failures.** The middleware
210
+ intentionally re-issues a fresh nonce on every failure; trust it.
211
+
212
+ ### Verifying your build
213
+
214
+ 1. Run `npm test`. Covers all 9 verification reasons plus the client helper.
215
+ 2. Run `./install.sh` and pay the cat demo end-to-end:
216
+ ```bash
217
+ set -a; source .env.local; set +a
218
+ npx tsx demo/pay-cli.ts -v --out /tmp/cat.svg http://127.0.0.1:4403/cat
219
+ ```
220
+ This proves the full loop works on regtest.
221
+ 3. Hit your endpoint without payment. Confirm 402 with a valid
222
+ `X-Payment-Required` header.
223
+ 4. Hit it twice with the same proof. The second call must return 402
224
+ with `error: nonce_replayed`.
225
+
226
+ ---
227
+
228
+ ## Quick reference card
229
+
230
+ ```
231
+ GET /resource
232
+ → 402 + X-Payment-Required: base64({ x402Version: 1, accepts: [requirement] })
233
+
234
+ requirement = {
235
+ scheme: "pivx-transparent" | "pivx-shield",
236
+ network: "pivx-mainnet" | "pivx-testnet" | "pivx-regtest",
237
+ asset: "PIV",
238
+ maxAmountRequired: "0.0001", // decimal PIV string
239
+ payTo: "D...", // ps1... for shielded
240
+ nonce: "<32-hex-chars>",
241
+ minConfirmations: number,
242
+ maxTimeoutSeconds: number,
243
+ resource: "/resource",
244
+ description?: string,
245
+ }
246
+
247
+ Pay: send >= maxAmountRequired PIV to payTo,
248
+ embed nonce in OP_RETURN (transparent) or memo (shielded).
249
+
250
+ GET /resource
251
+ X-Payment: base64({
252
+ x402Version: 1,
253
+ scheme, network,
254
+ payload: { txid, nonce }
255
+ })
256
+ → 200 + resource body
257
+ → 402 + { error: <reason> } if verification failed
258
+ ```
259
+
260
+ If your agent stays inside this card, it can talk to any `pivx-402`
261
+ endpoint without further help.
package/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. This project adheres
4
+ to [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [1.0.0] - 2026-05-22
7
+
8
+ First stable release. The transparent and shielded payment flows, the Express
9
+ middleware, the verifier, and the node-rpc/explorer backends are considered
10
+ stable; breaking changes will bump the major version from here.
11
+
12
+ ### Added
13
+ - Strict validation of decoded `X-Payment` proofs (`decodeProof`): unknown
14
+ scheme/network, missing or empty `txid`/`nonce`, and non-object payloads are
15
+ now rejected before reaching the verifier (previously could surface as a 500).
16
+ - `MAX_PAYMENT_HEADER_BYTES` (8 KB) cap on the `X-Payment` header to bound the
17
+ work done on untrusted input.
18
+ - `decodeProof` now requires `txid` to be a 64-char hex string, rejecting
19
+ malformed ids before they reach the backend RPC/explorer.
20
+ - Middleware integration test suite (`test/middleware.test.ts`) covering 402
21
+ issuance, malformed/oversized/structurally-invalid proofs, successful
22
+ verification with `req.pivx402` context, the backend-error retryable-402 path,
23
+ and cross-route replay protection with a shared `nonceStore`.
24
+
25
+ ### Changed
26
+ - `NodeRpcBackend.viewShieldedTransaction` now issues `viewshieldtransaction`
27
+ and `getrawtransaction` in parallel, roughly halving shielded-verify latency.
28
+ - `InMemoryNonceStore` sweeps expired nonces at most once per 60s instead of on
29
+ every `claim()`, removing per-request O(n) work under load.
30
+ - `pay-cli` validates that the server-issued nonce is hex before splicing it
31
+ into the `pivx-tx` OP_RETURN argument.
32
+ - Verifier scheme dispatch uses an exhaustive `switch` with `assertNever`.
33
+ - Demo servers share `networkFromEnv`/`makeBackend` via `demo/common.ts`.
34
+
35
+ ### Documentation
36
+ - Documented that multiple `pivx402()` middlewares sharing a `payTo` must share
37
+ a single `nonceStore` instance to prevent cross-route replay (README +
38
+ `MiddlewareOptions` JSDoc).
39
+ - Documented the front-running / unauthenticated-proof caveat for transparent
40
+ payments and its mitigations (README, Production deployment).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luke Larsen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.