@bsvkey/x402-bsv-client 0.1.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/LICENSE +21 -0
- package/README.md +58 -0
- package/example.js +37 -0
- package/index.js +122 -0
- package/package.json +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Embryo Space Inc. (DBA BSVKey)
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @bsvkey/x402-bsv-client
|
|
2
|
+
|
|
3
|
+
Pay an [x402](https://x402.org) `402 Payment Required` **in BSV**, in one line.
|
|
4
|
+
|
|
5
|
+
x402 ships payment builders for EVM (EIP-3009) and Solana. This is the missing
|
|
6
|
+
one for the **`bsv-p2pkh`** scheme — so an AI agent can pay a BSV-settled x402
|
|
7
|
+
endpoint (like `inference.bsvkey.com`) with true sub-cent, per-call micropayments.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @bsvkey/x402-bsv-client @bsv/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use it
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { fetchWithX402, readSettlement } from '@bsvkey/x402-bsv-client';
|
|
17
|
+
|
|
18
|
+
const res = await fetchWithX402(
|
|
19
|
+
'https://inference.bsvkey.com/v1/x402/chat/completions',
|
|
20
|
+
{
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'content-type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({ model: 'grok-4.3', messages: [{ role: 'user', content: 'Say hi in 3 words' }] }),
|
|
24
|
+
},
|
|
25
|
+
{ wif: process.env.BSV_WIF } // a funded BSV private key (WIF)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
console.log(data.choices[0].message.content); // the answer
|
|
30
|
+
console.log(readSettlement(res)); // { success, transaction (on-chain txid), network, ... }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's the whole x402 handshake, handled for you:
|
|
34
|
+
|
|
35
|
+
1. `POST` the request → server replies **402** with the quoted price.
|
|
36
|
+
2. This lib builds + signs a BSV tx paying the quoted amount to the server's `payTo`.
|
|
37
|
+
3. Retries with the `X-PAYMENT` header → server settles on-chain and returns **200** + the answer.
|
|
38
|
+
|
|
39
|
+
## Lower level
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
import { buildX402Payment } from '@bsvkey/x402-bsv-client';
|
|
43
|
+
|
|
44
|
+
// Given an x402 402 response body, produce the base64 X-PAYMENT header value:
|
|
45
|
+
const xPayment = await buildX402Payment(body402, wif);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Notes
|
|
49
|
+
|
|
50
|
+
- **Funding:** the WIF's address must hold enough BSV to cover the quoted amount + a
|
|
51
|
+
small fee. Fund it like any BSV address (the `payTo`/QR flow on inference.bsvkey.com works).
|
|
52
|
+
- **Back-to-back calls are safe:** the client chains each payment off its own change,
|
|
53
|
+
so a loop of paid calls won't double-spend while the mempool catches up.
|
|
54
|
+
- **Non-custodial:** your key never leaves the process; the signed transaction pays the
|
|
55
|
+
server's address directly.
|
|
56
|
+
- **Networks:** `bsv` (mainnet) and `bsv-testnet`. Testnet lets you dry-run for free.
|
|
57
|
+
|
|
58
|
+
MIT.
|
package/example.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Runnable example: pay for one inference in BSV via x402.
|
|
2
|
+
// BSV_WIF=<funded WIF> node example.js "your prompt here"
|
|
3
|
+
//
|
|
4
|
+
// Needs @bsv/sdk installed and a funded mainnet key. Without BSV_WIF it stops at
|
|
5
|
+
// the 402 and just prints the quote (safe, no payment).
|
|
6
|
+
|
|
7
|
+
import { fetchWithX402, buildX402Payment, readSettlement } from './index.js';
|
|
8
|
+
|
|
9
|
+
const URL = process.env.X402_URL || 'https://inference.bsvkey.com/v1/x402/chat/completions';
|
|
10
|
+
const prompt = process.argv[2] || 'In one sentence, why do AI agents want per-token billing?';
|
|
11
|
+
const wif = process.env.BSV_WIF;
|
|
12
|
+
|
|
13
|
+
const init = {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { 'content-type': 'application/json' },
|
|
16
|
+
body: JSON.stringify({ model: process.env.X402_MODEL || 'grok-4.3', messages: [{ role: 'user', content: prompt }], max_tokens: 256 }),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (!wif) {
|
|
20
|
+
// No key: just fetch the 402 quote so you can see the price without paying.
|
|
21
|
+
const r = await fetch(URL, init);
|
|
22
|
+
const body = await r.json();
|
|
23
|
+
const req = (body.accepts || [])[0];
|
|
24
|
+
console.log(`Quote: ${req?.maxAmountRequired} sat to ${req?.payTo} on ${req?.network} (model ${req?.extra?.model}).`);
|
|
25
|
+
console.log('Set BSV_WIF=<funded key> to actually pay and get the answer.');
|
|
26
|
+
} else {
|
|
27
|
+
console.log('Paying and running…');
|
|
28
|
+
const res = await fetchWithX402(URL, init, { wif });
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
console.error('failed:', res.status, await res.text());
|
|
31
|
+
} else {
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
console.log('\nAnswer:\n' + (data.choices?.[0]?.message?.content || '(none)'));
|
|
34
|
+
console.log('\nSettlement:', readSettlement(res));
|
|
35
|
+
console.log('Paid:', data.x_bsv?.paidSats, 'sat →', data.x_bsv?.payTo);
|
|
36
|
+
}
|
|
37
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// @bsvkey/x402-bsv-client — pay an x402 "402 Payment Required" in BSV.
|
|
2
|
+
//
|
|
3
|
+
// x402 ships EVM (EIP-3009) and Solana payment builders; this is the missing BSV
|
|
4
|
+
// one for the `bsv-p2pkh` scheme. It turns an agent's paid API call into one line:
|
|
5
|
+
//
|
|
6
|
+
// import { fetchWithX402 } from '@bsvkey/x402-bsv-client';
|
|
7
|
+
// const res = await fetchWithX402('https://inference.bsvkey.com/v1/x402/chat/completions',
|
|
8
|
+
// { method:'POST', headers:{'content-type':'application/json'},
|
|
9
|
+
// body: JSON.stringify({ model:'grok-4.3', messages:[{role:'user',content:'hi'}] }) },
|
|
10
|
+
// { wif: process.env.BSV_WIF });
|
|
11
|
+
// const answer = (await res.json()).choices[0].message.content;
|
|
12
|
+
//
|
|
13
|
+
// It does the whole handshake: request -> 402 -> build+sign a BSV payment paying the
|
|
14
|
+
// quoted amount to payTo -> retry with the X-PAYMENT header -> return the 200 response
|
|
15
|
+
// (whose X-PAYMENT-RESPONSE header carries the on-chain settlement txid).
|
|
16
|
+
//
|
|
17
|
+
// Peer dependency: @bsv/sdk (v1). Node 18+ (global fetch).
|
|
18
|
+
|
|
19
|
+
import { PrivateKey, P2PKH, Transaction, Utils } from '@bsv/sdk';
|
|
20
|
+
|
|
21
|
+
const NET = { bsv: 'main', 'bsv-testnet': 'test', 'bsv-dev': 'main' };
|
|
22
|
+
const PREFIX = { main: [0x00], test: [0x6f] };
|
|
23
|
+
|
|
24
|
+
// --- session UTXO tracker: chain each payment off our own change so back-to-back
|
|
25
|
+
// paid calls don't double-spend while WhatsOnChain's unspent list catches up. ---
|
|
26
|
+
const _sessions = new Map(); // addr -> { spent:Set, change:[{tx,vout,satoshis}] }
|
|
27
|
+
function session(addr) {
|
|
28
|
+
let s = _sessions.get(addr);
|
|
29
|
+
if (!s) { s = { spent: new Set(), change: [] }; _sessions.set(addr, s); }
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pickRequirement(body) {
|
|
34
|
+
const accepts = (body && body.accepts) || [];
|
|
35
|
+
return accepts.find((a) => a.scheme === 'bsv-p2pkh') || accepts[0] || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build the base64 X-PAYMENT header value for a given x402 402 body, paying in BSV.
|
|
39
|
+
export async function buildX402Payment(body402, wif, { wocBase } = {}) {
|
|
40
|
+
const req = pickRequirement(body402);
|
|
41
|
+
if (!req) throw new Error('no bsv-p2pkh payment requirement in the 402 response');
|
|
42
|
+
const amount = Number(req.maxAmountRequired || req.amount || 0);
|
|
43
|
+
if (!(amount > 0)) throw new Error('invalid amount in payment requirement');
|
|
44
|
+
const net = NET[req.network] || 'main';
|
|
45
|
+
const prefix = PREFIX[net];
|
|
46
|
+
const base = (wocBase || 'https://api.whatsonchain.com/v1/bsv') + '/' + net;
|
|
47
|
+
|
|
48
|
+
const priv = PrivateKey.fromWif(wif);
|
|
49
|
+
const pub = priv.toPublicKey();
|
|
50
|
+
const addr = pub.toAddress(prefix);
|
|
51
|
+
const sess = session(addr);
|
|
52
|
+
|
|
53
|
+
const tx = new Transaction();
|
|
54
|
+
tx.addOutput({ lockingScript: new P2PKH().lock(Utils.fromBase58Check(req.payTo).data), satoshis: amount });
|
|
55
|
+
|
|
56
|
+
const target = amount + 500; // amount + fee headroom
|
|
57
|
+
let got = 0;
|
|
58
|
+
const usedChange = [];
|
|
59
|
+
|
|
60
|
+
// spend our own unconfirmed change first
|
|
61
|
+
for (const c of sess.change) {
|
|
62
|
+
tx.addInput({ sourceTransaction: c.tx, sourceOutputIndex: c.vout, unlockingScriptTemplate: new P2PKH().unlock(priv) });
|
|
63
|
+
got += c.satoshis; usedChange.push(c);
|
|
64
|
+
if (got >= target) break;
|
|
65
|
+
}
|
|
66
|
+
// then confirmed UTXOs, skipping anything already spent this session
|
|
67
|
+
if (got < target) {
|
|
68
|
+
const unspent = await (await fetch(`${base}/address/${addr}/unspent`)).json();
|
|
69
|
+
if ((!unspent || !unspent.length) && !usedChange.length) throw new Error('wallet is unfunded: ' + addr);
|
|
70
|
+
for (const u of (unspent || []).sort((a, b) => b.value - a.value)) {
|
|
71
|
+
if (sess.spent.has(u.tx_hash + ':' + u.tx_pos)) continue;
|
|
72
|
+
let src;
|
|
73
|
+
try { const beef = await (await fetch(`${base}/tx/${u.tx_hash}/beef`)).text(); src = Transaction.fromBEEF(Utils.toArray(beef.trim(), 'hex')); }
|
|
74
|
+
catch { const hex = await (await fetch(`${base}/tx/${u.tx_hash}/hex`)).text(); src = Transaction.fromHex(hex.trim()); }
|
|
75
|
+
tx.addInput({ sourceTransaction: src, sourceOutputIndex: u.tx_pos, unlockingScriptTemplate: new P2PKH().unlock(priv) });
|
|
76
|
+
got += u.value; if (got >= target) break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (got < target) throw new Error(`wallet balance too low: have ${got}, need ${target} (${addr})`);
|
|
80
|
+
|
|
81
|
+
const changeIndex = tx.outputs.length;
|
|
82
|
+
tx.addOutput({ lockingScript: new P2PKH().lock(pub.toHash()), change: true });
|
|
83
|
+
await tx.fee();
|
|
84
|
+
await tx.sign();
|
|
85
|
+
|
|
86
|
+
// commit tracker (the payment is about to be submitted by the resource server)
|
|
87
|
+
for (const inp of tx.inputs) sess.spent.add((inp.sourceTransaction ? inp.sourceTransaction.id('hex') : inp.sourceTXID) + ':' + inp.sourceOutputIndex);
|
|
88
|
+
sess.change = sess.change.filter((c) => !usedChange.includes(c));
|
|
89
|
+
const chg = tx.outputs[changeIndex];
|
|
90
|
+
if (chg && chg.satoshis > 0) sess.change.push({ tx, vout: changeIndex, satoshis: chg.satoshis });
|
|
91
|
+
|
|
92
|
+
const envelope = {
|
|
93
|
+
x402Version: body402.x402Version || 1,
|
|
94
|
+
scheme: 'bsv-p2pkh',
|
|
95
|
+
network: req.network,
|
|
96
|
+
payload: { transaction: Utils.toHex(tx.toBEEF()), payer: addr },
|
|
97
|
+
};
|
|
98
|
+
return Buffer.from(JSON.stringify(envelope)).toString('base64');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// One-call paid fetch: run the request, auto-pay a 402 in BSV, return the final Response.
|
|
102
|
+
// `paymentBuilder` is injectable for testing; defaults to buildX402Payment.
|
|
103
|
+
export async function fetchWithX402(url, init = {}, { wif, wocBase, paymentBuilder } = {}) {
|
|
104
|
+
const first = await fetch(url, init);
|
|
105
|
+
if (first.status !== 402) return first;
|
|
106
|
+
if (!wif) throw new Error('402 received but no `wif` provided to pay it');
|
|
107
|
+
|
|
108
|
+
const body402 = await first.clone().json().catch(() => null);
|
|
109
|
+
if (!body402) throw new Error('402 response was not JSON x402');
|
|
110
|
+
const build = paymentBuilder || buildX402Payment;
|
|
111
|
+
const xpayment = await build(body402, wif, { wocBase });
|
|
112
|
+
|
|
113
|
+
const headers = Object.assign({}, init.headers || {}, { 'X-PAYMENT': xpayment });
|
|
114
|
+
return fetch(url, Object.assign({}, init, { headers }));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Decode the settlement result from a paid 200 response (the on-chain txid, etc.).
|
|
118
|
+
export function readSettlement(res) {
|
|
119
|
+
const h = res.headers.get('x-payment-response');
|
|
120
|
+
if (!h) return null;
|
|
121
|
+
try { return JSON.parse(Buffer.from(h, 'base64').toString('utf8')); } catch { return null; }
|
|
122
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bsvkey/x402-bsv-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pay an x402 HTTP 402 in BSV — the missing bsv-p2pkh payment builder for x402 agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": { ".": "./index.js" },
|
|
8
|
+
"files": ["index.js", "example.js", "README.md", "LICENSE"],
|
|
9
|
+
"author": "Embryo Space Inc. (DBA BSVKey)",
|
|
10
|
+
"homepage": "https://inference.bsvkey.com/v1/x402",
|
|
11
|
+
"engines": { "node": ">=18" },
|
|
12
|
+
"peerDependencies": { "@bsv/sdk": "^1" },
|
|
13
|
+
"keywords": ["x402", "bsv", "micropayments", "ai-agents", "402", "payments", "http-402"],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": { "type": "git", "url": "https://github.com/BSVKey/x402-bsv-client" }
|
|
16
|
+
}
|