@retrodeck/x402 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/dist/client.d.ts +39 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +162 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +191 -0
- package/dist/index.js.map +1 -0
- package/package.json +24 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LocalStore } from '@rdk/core';
|
|
2
|
+
export declare const AUTONOMOUS_THRESHOLD_USDC = 0.05;
|
|
3
|
+
export declare const BATCH_INTERVAL_MS: number;
|
|
4
|
+
export declare const BATCH_TRIGGER_USDC = 1;
|
|
5
|
+
export interface TipConfig {
|
|
6
|
+
privateKey: string;
|
|
7
|
+
centralApiUrl: string;
|
|
8
|
+
centralApiKey: string;
|
|
9
|
+
defaultChain: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class X402Client {
|
|
12
|
+
private config;
|
|
13
|
+
private store;
|
|
14
|
+
private settling;
|
|
15
|
+
constructor(config: TipConfig, store: LocalStore);
|
|
16
|
+
/** Queue a tip after a successful network retrieval */
|
|
17
|
+
payForRetrieval(opts: {
|
|
18
|
+
chunkId: string;
|
|
19
|
+
providerNodeId: string;
|
|
20
|
+
amountUsdc: number;
|
|
21
|
+
chain?: string;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
queued: boolean;
|
|
24
|
+
txHash?: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** Batch-settle all pending tips. Called hourly or when threshold crossed. */
|
|
27
|
+
settleBatch(): Promise<{
|
|
28
|
+
settled: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
totalUsdc: number;
|
|
31
|
+
}>;
|
|
32
|
+
/** Start hourly background settlement loop */
|
|
33
|
+
startSettlementLoop(): ReturnType<typeof setInterval>;
|
|
34
|
+
private sendUsdc;
|
|
35
|
+
private getProviderWallet;
|
|
36
|
+
private recordOnCentral;
|
|
37
|
+
private loadEthers;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,WAAW,CAAC;AAE3D,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAC9C,eAAO,MAAM,iBAAiB,QAAiB,CAAC;AAChD,eAAO,MAAM,kBAAkB,IAAO,CAAC;AAmBvC,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU;IAKhD,uDAAuD;IACjD,eAAe,CAAC,IAAI,EAAE;QAC1B,OAAO,EAAE,MAAM,CAAC;QAChB,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAmBjD,8EAA8E;IACxE,WAAW,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAkDpF,8CAA8C;IAC9C,mBAAmB,IAAI,UAAU,CAAC,OAAO,WAAW,CAAC;YAUvC,QAAQ;YAgCR,iBAAiB;YAcjB,eAAe;YAiBf,UAAU;CAWzB"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// packages/rdk-x402/src/client.ts
|
|
2
|
+
// On-chain USDC micropayment tips for RDK knowledge retrieval.
|
|
3
|
+
// ethers.js is an optional dep — imported lazily on first tip settlement only.
|
|
4
|
+
// Tier 4: only loaded when user runs `rdk tips:enable`.
|
|
5
|
+
export const AUTONOMOUS_THRESHOLD_USDC = 0.05;
|
|
6
|
+
export const BATCH_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
7
|
+
export const BATCH_TRIGGER_USDC = 1.00;
|
|
8
|
+
const USDC_CONTRACTS = {
|
|
9
|
+
base: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
10
|
+
ethereum: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
11
|
+
polygon: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
|
|
12
|
+
};
|
|
13
|
+
const ERC20_ABI = [
|
|
14
|
+
'function transfer(address to, uint256 amount) returns (bool)',
|
|
15
|
+
'function balanceOf(address owner) view returns (uint256)',
|
|
16
|
+
];
|
|
17
|
+
const RPC_URLS = {
|
|
18
|
+
base: 'https://mainnet.base.org',
|
|
19
|
+
ethereum: 'https://cloudflare-eth.com',
|
|
20
|
+
polygon: 'https://polygon-rpc.com',
|
|
21
|
+
};
|
|
22
|
+
export class X402Client {
|
|
23
|
+
config;
|
|
24
|
+
store;
|
|
25
|
+
settling = false;
|
|
26
|
+
constructor(config, store) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.store = store;
|
|
29
|
+
}
|
|
30
|
+
/** Queue a tip after a successful network retrieval */
|
|
31
|
+
async payForRetrieval(opts) {
|
|
32
|
+
const chain = opts.chain ?? this.config.defaultChain;
|
|
33
|
+
this.store.enqueueTip({
|
|
34
|
+
chunkId: opts.chunkId,
|
|
35
|
+
providerNodeId: opts.providerNodeId,
|
|
36
|
+
amountUsdc: opts.amountUsdc,
|
|
37
|
+
chain,
|
|
38
|
+
});
|
|
39
|
+
// Trigger immediate batch if we've crossed the $1.00 threshold
|
|
40
|
+
const pendingTotal = this.store.getPendingTipTotal();
|
|
41
|
+
if (pendingTotal >= BATCH_TRIGGER_USDC) {
|
|
42
|
+
setImmediate(() => this.settleBatch().catch(console.error));
|
|
43
|
+
}
|
|
44
|
+
return { queued: true };
|
|
45
|
+
}
|
|
46
|
+
/** Batch-settle all pending tips. Called hourly or when threshold crossed. */
|
|
47
|
+
async settleBatch() {
|
|
48
|
+
if (this.settling)
|
|
49
|
+
return { settled: 0, failed: 0, totalUsdc: 0 };
|
|
50
|
+
this.settling = true;
|
|
51
|
+
let settled = 0;
|
|
52
|
+
let failed = 0;
|
|
53
|
+
let totalUsdc = 0;
|
|
54
|
+
try {
|
|
55
|
+
const pending = this.store.getPendingTips();
|
|
56
|
+
if (pending.length === 0)
|
|
57
|
+
return { settled: 0, failed: 0, totalUsdc: 0 };
|
|
58
|
+
// Group by providerNodeId + chain to minimize on-chain transactions
|
|
59
|
+
const groups = new Map();
|
|
60
|
+
for (const tip of pending) {
|
|
61
|
+
const key = `${tip.providerNodeId}:${tip.chain}`;
|
|
62
|
+
const group = groups.get(key) ?? [];
|
|
63
|
+
group.push(tip);
|
|
64
|
+
groups.set(key, group);
|
|
65
|
+
}
|
|
66
|
+
for (const [, tips] of groups) {
|
|
67
|
+
const batchAmount = tips.reduce((s, t) => s + t.amountUsdc, 0);
|
|
68
|
+
if (batchAmount < 0.001)
|
|
69
|
+
continue; // skip micro-batches below gas cost
|
|
70
|
+
const chain = tips[0].chain;
|
|
71
|
+
const providerNodeId = tips[0].providerNodeId;
|
|
72
|
+
try {
|
|
73
|
+
const txHash = await this.sendUsdc(providerNodeId, batchAmount, chain);
|
|
74
|
+
if (txHash) {
|
|
75
|
+
for (const tip of tips) {
|
|
76
|
+
this.store.settleTip(tip.id, txHash);
|
|
77
|
+
await this.recordOnCentral(tip.chunkId, txHash, tip.amountUsdc, chain);
|
|
78
|
+
}
|
|
79
|
+
settled += tips.length;
|
|
80
|
+
totalUsdc += batchAmount;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
for (const tip of tips)
|
|
85
|
+
this.store.failTip(tip.id);
|
|
86
|
+
failed += tips.length;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
this.settling = false;
|
|
92
|
+
}
|
|
93
|
+
return { settled, failed, totalUsdc };
|
|
94
|
+
}
|
|
95
|
+
/** Start hourly background settlement loop */
|
|
96
|
+
startSettlementLoop() {
|
|
97
|
+
return setInterval(() => {
|
|
98
|
+
if (this.store.getPendingTipTotal() > 0) {
|
|
99
|
+
this.settleBatch().catch(console.error);
|
|
100
|
+
}
|
|
101
|
+
}, BATCH_INTERVAL_MS);
|
|
102
|
+
}
|
|
103
|
+
// ── Private: on-chain settlement ──────────────────────────────────────────
|
|
104
|
+
async sendUsdc(providerNodeId, amountUsdc, chain) {
|
|
105
|
+
const toAddress = await this.getProviderWallet(providerNodeId);
|
|
106
|
+
if (!toAddress)
|
|
107
|
+
return null;
|
|
108
|
+
const usdcAddress = USDC_CONTRACTS[chain];
|
|
109
|
+
if (!usdcAddress)
|
|
110
|
+
throw new Error(`Chain not supported: ${chain}`);
|
|
111
|
+
if (!this.config.privateKey) {
|
|
112
|
+
throw new Error('No wallet private key. Set RDK_WALLET_PRIVATE_KEY env var.');
|
|
113
|
+
}
|
|
114
|
+
// Lazy-load ethers — only runs if tips are actually being settled
|
|
115
|
+
const ethers = await this.loadEthers();
|
|
116
|
+
const rpcUrl = process.env[`${chain.toUpperCase()}_RPC_URL`] ?? RPC_URLS[chain];
|
|
117
|
+
if (!rpcUrl)
|
|
118
|
+
throw new Error(`No RPC URL for chain: ${chain}`);
|
|
119
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
120
|
+
const wallet = new ethers.Wallet(this.config.privateKey, provider);
|
|
121
|
+
const usdc = new ethers.Contract(usdcAddress, ERC20_ABI, wallet);
|
|
122
|
+
// USDC: 6 decimals
|
|
123
|
+
const amount = BigInt(Math.round(amountUsdc * 1_000_000));
|
|
124
|
+
const tx = await usdc['transfer'](toAddress, amount);
|
|
125
|
+
const receipt = await tx.wait();
|
|
126
|
+
return receipt?.hash ?? null;
|
|
127
|
+
}
|
|
128
|
+
async getProviderWallet(providerNodeId) {
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(`${this.config.centralApiUrl}/api/v1/nodes/${providerNodeId}/wallet`, { headers: { Authorization: `Bearer ${this.config.centralApiKey}` } });
|
|
131
|
+
if (!res.ok)
|
|
132
|
+
return null;
|
|
133
|
+
const data = await res.json();
|
|
134
|
+
return data.walletAddress ?? null;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async recordOnCentral(chunkId, txHash, amountUsdc, chain) {
|
|
141
|
+
await fetch(`${this.config.centralApiUrl}/api/v1/tips/record`, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: `Bearer ${this.config.centralApiKey}`,
|
|
145
|
+
'Content-Type': 'application/json',
|
|
146
|
+
},
|
|
147
|
+
body: JSON.stringify({ queryId: '', chunkId, txHash, amountUsdc, chain }),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// Lazy ethers loader — returns the ethers namespace, not { ethers: ... }
|
|
151
|
+
async loadEthers() {
|
|
152
|
+
try {
|
|
153
|
+
return await import('ethers');
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
throw new Error('ethers not installed.\n' +
|
|
157
|
+
'Run: rdk tips:enable (installs automatically)\n' +
|
|
158
|
+
'Or: npm install ethers');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AUTONOMOUS_THRESHOLD_USDC: () => AUTONOMOUS_THRESHOLD_USDC,
|
|
34
|
+
BATCH_INTERVAL_MS: () => BATCH_INTERVAL_MS,
|
|
35
|
+
BATCH_TRIGGER_USDC: () => BATCH_TRIGGER_USDC,
|
|
36
|
+
X402Client: () => X402Client
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/client.ts
|
|
41
|
+
var AUTONOMOUS_THRESHOLD_USDC = 0.05;
|
|
42
|
+
var BATCH_INTERVAL_MS = 60 * 60 * 1e3;
|
|
43
|
+
var BATCH_TRIGGER_USDC = 1;
|
|
44
|
+
var USDC_CONTRACTS = {
|
|
45
|
+
base: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
46
|
+
ethereum: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
47
|
+
polygon: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
|
|
48
|
+
};
|
|
49
|
+
var ERC20_ABI = [
|
|
50
|
+
"function transfer(address to, uint256 amount) returns (bool)",
|
|
51
|
+
"function balanceOf(address owner) view returns (uint256)"
|
|
52
|
+
];
|
|
53
|
+
var RPC_URLS = {
|
|
54
|
+
base: "https://mainnet.base.org",
|
|
55
|
+
ethereum: "https://cloudflare-eth.com",
|
|
56
|
+
polygon: "https://polygon-rpc.com"
|
|
57
|
+
};
|
|
58
|
+
var X402Client = class {
|
|
59
|
+
config;
|
|
60
|
+
store;
|
|
61
|
+
settling = false;
|
|
62
|
+
constructor(config, store) {
|
|
63
|
+
this.config = config;
|
|
64
|
+
this.store = store;
|
|
65
|
+
}
|
|
66
|
+
/** Queue a tip after a successful network retrieval */
|
|
67
|
+
async payForRetrieval(opts) {
|
|
68
|
+
const chain = opts.chain ?? this.config.defaultChain;
|
|
69
|
+
this.store.enqueueTip({
|
|
70
|
+
chunkId: opts.chunkId,
|
|
71
|
+
providerNodeId: opts.providerNodeId,
|
|
72
|
+
amountUsdc: opts.amountUsdc,
|
|
73
|
+
chain
|
|
74
|
+
});
|
|
75
|
+
const pendingTotal = this.store.getPendingTipTotal();
|
|
76
|
+
if (pendingTotal >= BATCH_TRIGGER_USDC) {
|
|
77
|
+
setImmediate(() => this.settleBatch().catch(console.error));
|
|
78
|
+
}
|
|
79
|
+
return { queued: true };
|
|
80
|
+
}
|
|
81
|
+
/** Batch-settle all pending tips. Called hourly or when threshold crossed. */
|
|
82
|
+
async settleBatch() {
|
|
83
|
+
if (this.settling) return { settled: 0, failed: 0, totalUsdc: 0 };
|
|
84
|
+
this.settling = true;
|
|
85
|
+
let settled = 0;
|
|
86
|
+
let failed = 0;
|
|
87
|
+
let totalUsdc = 0;
|
|
88
|
+
try {
|
|
89
|
+
const pending = this.store.getPendingTips();
|
|
90
|
+
if (pending.length === 0) return { settled: 0, failed: 0, totalUsdc: 0 };
|
|
91
|
+
const groups = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const tip of pending) {
|
|
93
|
+
const key = `${tip.providerNodeId}:${tip.chain}`;
|
|
94
|
+
const group = groups.get(key) ?? [];
|
|
95
|
+
group.push(tip);
|
|
96
|
+
groups.set(key, group);
|
|
97
|
+
}
|
|
98
|
+
for (const [, tips] of groups) {
|
|
99
|
+
const batchAmount = tips.reduce((s, t) => s + t.amountUsdc, 0);
|
|
100
|
+
if (batchAmount < 1e-3) continue;
|
|
101
|
+
const chain = tips[0].chain;
|
|
102
|
+
const providerNodeId = tips[0].providerNodeId;
|
|
103
|
+
try {
|
|
104
|
+
const txHash = await this.sendUsdc(providerNodeId, batchAmount, chain);
|
|
105
|
+
if (txHash) {
|
|
106
|
+
for (const tip of tips) {
|
|
107
|
+
this.store.settleTip(tip.id, txHash);
|
|
108
|
+
await this.recordOnCentral(tip.chunkId, txHash, tip.amountUsdc, chain);
|
|
109
|
+
}
|
|
110
|
+
settled += tips.length;
|
|
111
|
+
totalUsdc += batchAmount;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
for (const tip of tips) this.store.failTip(tip.id);
|
|
115
|
+
failed += tips.length;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
this.settling = false;
|
|
120
|
+
}
|
|
121
|
+
return { settled, failed, totalUsdc };
|
|
122
|
+
}
|
|
123
|
+
/** Start hourly background settlement loop */
|
|
124
|
+
startSettlementLoop() {
|
|
125
|
+
return setInterval(() => {
|
|
126
|
+
if (this.store.getPendingTipTotal() > 0) {
|
|
127
|
+
this.settleBatch().catch(console.error);
|
|
128
|
+
}
|
|
129
|
+
}, BATCH_INTERVAL_MS);
|
|
130
|
+
}
|
|
131
|
+
// ── Private: on-chain settlement ──────────────────────────────────────────
|
|
132
|
+
async sendUsdc(providerNodeId, amountUsdc, chain) {
|
|
133
|
+
const toAddress = await this.getProviderWallet(providerNodeId);
|
|
134
|
+
if (!toAddress) return null;
|
|
135
|
+
const usdcAddress = USDC_CONTRACTS[chain];
|
|
136
|
+
if (!usdcAddress) throw new Error(`Chain not supported: ${chain}`);
|
|
137
|
+
if (!this.config.privateKey) {
|
|
138
|
+
throw new Error("No wallet private key. Set RDK_WALLET_PRIVATE_KEY env var.");
|
|
139
|
+
}
|
|
140
|
+
const ethers = await this.loadEthers();
|
|
141
|
+
const rpcUrl = process.env[`${chain.toUpperCase()}_RPC_URL`] ?? RPC_URLS[chain];
|
|
142
|
+
if (!rpcUrl) throw new Error(`No RPC URL for chain: ${chain}`);
|
|
143
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
144
|
+
const wallet = new ethers.Wallet(this.config.privateKey, provider);
|
|
145
|
+
const usdc = new ethers.Contract(usdcAddress, ERC20_ABI, wallet);
|
|
146
|
+
const amount = BigInt(Math.round(amountUsdc * 1e6));
|
|
147
|
+
const tx = await usdc["transfer"](toAddress, amount);
|
|
148
|
+
const receipt = await tx.wait();
|
|
149
|
+
return receipt?.hash ?? null;
|
|
150
|
+
}
|
|
151
|
+
async getProviderWallet(providerNodeId) {
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch(
|
|
154
|
+
`${this.config.centralApiUrl}/api/v1/nodes/${providerNodeId}/wallet`,
|
|
155
|
+
{ headers: { Authorization: `Bearer ${this.config.centralApiKey}` } }
|
|
156
|
+
);
|
|
157
|
+
if (!res.ok) return null;
|
|
158
|
+
const data = await res.json();
|
|
159
|
+
return data.walletAddress ?? null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async recordOnCentral(chunkId, txHash, amountUsdc, chain) {
|
|
165
|
+
await fetch(`${this.config.centralApiUrl}/api/v1/tips/record`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
Authorization: `Bearer ${this.config.centralApiKey}`,
|
|
169
|
+
"Content-Type": "application/json"
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify({ queryId: "", chunkId, txHash, amountUsdc, chain })
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Lazy ethers loader — returns the ethers namespace, not { ethers: ... }
|
|
175
|
+
async loadEthers() {
|
|
176
|
+
try {
|
|
177
|
+
return await import("ethers");
|
|
178
|
+
} catch {
|
|
179
|
+
throw new Error(
|
|
180
|
+
"ethers not installed.\nRun: rdk tips:enable (installs automatically)\nOr: npm install ethers"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
186
|
+
0 && (module.exports = {
|
|
187
|
+
AUTONOMOUS_THRESHOLD_USDC,
|
|
188
|
+
BATCH_INTERVAL_MS,
|
|
189
|
+
BATCH_TRIGGER_USDC,
|
|
190
|
+
X402Client
|
|
191
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,OAAO,EAAE,UAAU,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@retrodeck/x402",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "RDK x402 micropayment tip protocol — on-chain USDC tips for knowledge retrieval",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"optionalDependencies": {
|
|
7
|
+
"ethers": "^6.14.1"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@types/node": "^22.15.21",
|
|
11
|
+
"esbuild": "^0.28.0",
|
|
12
|
+
"typescript": "^5.8.3",
|
|
13
|
+
"@rdk/core": "1.0.0"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/",
|
|
17
|
+
"package.json"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=cjs --target=node20 --outfile=dist/index.js --external:ethers --external:better-sqlite3 --external:@xenova/transformers",
|
|
21
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
22
|
+
"clean": "rm -rf dist"
|
|
23
|
+
}
|
|
24
|
+
}
|