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 +261 -0
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +632 -0
- package/dist/src/amount.d.ts +2 -0
- package/dist/src/amount.js +24 -0
- package/dist/src/backends/explorer.d.ts +15 -0
- package/dist/src/backends/explorer.js +79 -0
- package/dist/src/backends/index.d.ts +15 -0
- package/dist/src/backends/index.js +7 -0
- package/dist/src/backends/node-rpc.d.ts +17 -0
- package/dist/src/backends/node-rpc.js +153 -0
- package/dist/src/client.d.ts +40 -0
- package/dist/src/client.js +66 -0
- package/dist/src/headers.d.ts +20 -0
- package/dist/src/headers.js +74 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +24 -0
- package/dist/src/middleware.d.ts +50 -0
- package/dist/src/middleware.js +84 -0
- package/dist/src/nonce-store.d.ts +16 -0
- package/dist/src/nonce-store.js +40 -0
- package/dist/src/types.d.ts +75 -0
- package/dist/src/types.js +2 -0
- package/dist/src/verifier.d.ts +14 -0
- package/dist/src/verifier.js +110 -0
- package/package.json +65 -0
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.
|