whitemagic-pay 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 +105 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +328 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WhiteMagic Labs
|
|
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,105 @@
|
|
|
1
|
+
# whitemagic-pay (TypeScript)
|
|
2
|
+
|
|
3
|
+
Autonomous machine payment client, session lease caching, and cryptographic
|
|
4
|
+
Proof-of-Context notarization for AI agents. A drop-in `fetch` wrapper that
|
|
5
|
+
negotiates HTTP 402 challenges, signs EIP-3009 gasless permits on Base, caches
|
|
6
|
+
session leases, and retries — no API key, no account.
|
|
7
|
+
|
|
8
|
+
- Zero mandatory dependencies (global `fetch`); optional [`viem`](https://viem.sh) enables the paid signing path.
|
|
9
|
+
- Session pass caching: one payment covers every call until the lease expires.
|
|
10
|
+
- Faucet support: ephemeral agents can claim a free 10-minute pass with no wallet.
|
|
11
|
+
- Works in Node.js 18+, Bun, Deno, and browsers (paid signing requires a key-capable runtime).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install whitemagic-pay
|
|
17
|
+
# optional, for the paid path:
|
|
18
|
+
npm install viem
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
### 1. Free onboarding (no wallet, no API key)
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { X402Client } from "whitemagic-pay";
|
|
27
|
+
|
|
28
|
+
const client = new X402Client();
|
|
29
|
+
const faucet = await client.claimFaucet("https://mcp.whitemagic.dev");
|
|
30
|
+
console.log("session pass acquired:", client.pass.tier);
|
|
31
|
+
|
|
32
|
+
const { status, data } = await client.request(
|
|
33
|
+
"POST",
|
|
34
|
+
"https://mcp.whitemagic.dev/mcp",
|
|
35
|
+
{
|
|
36
|
+
jsonrpc: "2.0",
|
|
37
|
+
id: 1,
|
|
38
|
+
method: "tools/call",
|
|
39
|
+
params: { name: "memory.search", arguments: { query: "context continuity" } },
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
console.log(status, data);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. Autonomous multi-tier micro-leases (x402 on Base)
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { X402Client } from "whitemagic-pay";
|
|
49
|
+
|
|
50
|
+
const client = new X402Client({
|
|
51
|
+
privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
|
|
52
|
+
preferredTier: "task",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Negotiates the 402 challenge, signs EIP-3009, settles on Base, caches the
|
|
56
|
+
// lease, and retries the call in one step.
|
|
57
|
+
const { status, data } = await client.request("POST", "https://mcp.whitemagic.dev/mcp", { /* ... */ });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or wrap `fetch` directly:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const res = await client.fetch("https://mcp.whitemagic.dev/mcp", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
body: JSON.stringify({ /* ... */ }),
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Tier menu
|
|
70
|
+
|
|
71
|
+
| Tier | Price (USDC) | Atomic units | Duration | Best for |
|
|
72
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
73
|
+
| `call` | $0.002 | 2,000 | 60 s | Single tool call, health probe |
|
|
74
|
+
| `task` | $0.010 | 10,000 | 300 s | Standard multi-step task |
|
|
75
|
+
| `swarm` | $0.050 | 50,000 | 900 s | Multi-agent coordination |
|
|
76
|
+
| `day` | $0.500 | 500,000 | 86,400 s | Continuous production worker |
|
|
77
|
+
|
|
78
|
+
### 3. Proof-of-Context notarization
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const receipt = await client.notarizeContext({
|
|
82
|
+
contextDigest: "sha256:b10b001a...",
|
|
83
|
+
action: "agent.decision",
|
|
84
|
+
model: "gemini-2.5-flash",
|
|
85
|
+
metadata: { epoch: 1 },
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## API
|
|
90
|
+
|
|
91
|
+
- `new X402Client({ privateKey?, preferredTier?, autoLease? })`
|
|
92
|
+
- `client.claimFaucet(baseUrl?)` — claim a free 10-minute trial pass.
|
|
93
|
+
- `client.request(method, url, data?, headers?, timeoutMs?)` — full negotiation + pass reuse.
|
|
94
|
+
- `client.fetch(input, init?)` — `fetch`-compatible wrapper.
|
|
95
|
+
- `client.notarizeContext(input, baseUrl?)` — signed Proof-of-Context notarization.
|
|
96
|
+
- `client.isPassActive()` / `client.pass` — lease state.
|
|
97
|
+
- `client.address()` — payer address (async; resolves after signer load).
|
|
98
|
+
|
|
99
|
+
## Links
|
|
100
|
+
|
|
101
|
+
- Hosted lanes and x402 quickstart: https://www.whitemagic.dev/hosted
|
|
102
|
+
- Receipt spec and verifiers: https://www.whitemagic.dev/receipts
|
|
103
|
+
- Python package: https://pypi.org/project/whitemagic-pay/
|
|
104
|
+
|
|
105
|
+
MIT — WhiteMagic Labs.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whitemagic-pay (TypeScript) — autonomous x402 payment client, session lease
|
|
3
|
+
* caching, and Proof-of-Context notarization for AI agents.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the Python client's semantics: HTTP 402 challenge negotiation,
|
|
6
|
+
* EIP-3009 TransferWithAuthorization signing (via the optional `viem`
|
|
7
|
+
* dependency), session pass reuse, faucet claims, and context notarization.
|
|
8
|
+
*/
|
|
9
|
+
export type Tier = "call" | "task" | "swarm" | "day";
|
|
10
|
+
export interface X402Accept {
|
|
11
|
+
network: string;
|
|
12
|
+
asset: string;
|
|
13
|
+
amount: string | number;
|
|
14
|
+
payTo: string;
|
|
15
|
+
extra?: {
|
|
16
|
+
name?: string;
|
|
17
|
+
version?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface X402Challenge {
|
|
21
|
+
x402Version?: number;
|
|
22
|
+
resource?: string;
|
|
23
|
+
accepts: X402Accept[];
|
|
24
|
+
extensions?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
export interface X402ClientOptions {
|
|
27
|
+
privateKey?: string;
|
|
28
|
+
preferredTier?: Tier;
|
|
29
|
+
autoLease?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface X402Response {
|
|
32
|
+
status: number;
|
|
33
|
+
data: Record<string, unknown>;
|
|
34
|
+
headers: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
export interface NotarizeInput {
|
|
37
|
+
contextDigest?: string;
|
|
38
|
+
promptHash?: string;
|
|
39
|
+
action?: string;
|
|
40
|
+
model?: string;
|
|
41
|
+
metadata?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
export declare const TIER_PRICES: Record<Tier, number>;
|
|
44
|
+
export declare class X402Client {
|
|
45
|
+
private signerPromise;
|
|
46
|
+
private preferredTier;
|
|
47
|
+
private autoLease;
|
|
48
|
+
private cachedPass;
|
|
49
|
+
private passExpiresAt;
|
|
50
|
+
private passTier;
|
|
51
|
+
constructor(options?: X402ClientOptions);
|
|
52
|
+
/** The payer address, when a private key was provided. */
|
|
53
|
+
address(): Promise<string | null>;
|
|
54
|
+
isPassActive(): boolean;
|
|
55
|
+
get pass(): {
|
|
56
|
+
token: string | null;
|
|
57
|
+
tier: string;
|
|
58
|
+
expiresAt: number;
|
|
59
|
+
};
|
|
60
|
+
/** Claim a 10-minute trial session pass from the hosted faucet (no wallet). */
|
|
61
|
+
claimFaucet(baseUrl?: string): Promise<Record<string, unknown>>;
|
|
62
|
+
/** Request a Proof-of-Context notarization from the hosted API lane. */
|
|
63
|
+
notarizeContext(input: NotarizeInput, baseUrl?: string): Promise<Record<string, unknown>>;
|
|
64
|
+
/** Build a signed EIP-3009 payment payload from an x402 challenge. */
|
|
65
|
+
createPaymentPayload(challenge: X402Challenge, tier?: Tier): Promise<{
|
|
66
|
+
payment: Record<string, unknown>;
|
|
67
|
+
header: string;
|
|
68
|
+
}>;
|
|
69
|
+
/** Execute a request with automatic 402 negotiation and session pass reuse. */
|
|
70
|
+
request(method: string, url: string, data?: unknown, headers?: Record<string, string>, timeoutMs?: number): Promise<X402Response>;
|
|
71
|
+
/**
|
|
72
|
+
* fetch()-style wrapper: same negotiation semantics for callers that want
|
|
73
|
+
* a drop-in replacement for global fetch.
|
|
74
|
+
*/
|
|
75
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
76
|
+
private raw;
|
|
77
|
+
private extractChallenge;
|
|
78
|
+
private extractChallengeFromResponse;
|
|
79
|
+
private cachePassFromHeaders;
|
|
80
|
+
}
|
|
81
|
+
/** Convenience helper mirroring the Python package's `wrap_session`. */
|
|
82
|
+
export declare function createX402Client(options?: X402ClientOptions): X402Client;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whitemagic-pay (TypeScript) — autonomous x402 payment client, session lease
|
|
3
|
+
* caching, and Proof-of-Context notarization for AI agents.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the Python client's semantics: HTTP 402 challenge negotiation,
|
|
6
|
+
* EIP-3009 TransferWithAuthorization signing (via the optional `viem`
|
|
7
|
+
* dependency), session pass reuse, faucet claims, and context notarization.
|
|
8
|
+
*/
|
|
9
|
+
const CHAIN_MAP = {
|
|
10
|
+
"eip155:8453": 8453,
|
|
11
|
+
"eip155:84532": 84532,
|
|
12
|
+
"eip155:1": 1,
|
|
13
|
+
};
|
|
14
|
+
export const TIER_PRICES = {
|
|
15
|
+
call: 2000,
|
|
16
|
+
task: 10000,
|
|
17
|
+
swarm: 50000,
|
|
18
|
+
day: 500000,
|
|
19
|
+
};
|
|
20
|
+
const EIP3009_TYPES = {
|
|
21
|
+
TransferWithAuthorization: [
|
|
22
|
+
{ name: "from", type: "address" },
|
|
23
|
+
{ name: "to", type: "address" },
|
|
24
|
+
{ name: "value", type: "uint256" },
|
|
25
|
+
{ name: "validAfter", type: "uint256" },
|
|
26
|
+
{ name: "validBefore", type: "uint256" },
|
|
27
|
+
{ name: "nonce", type: "bytes32" },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
function b64encode(text) {
|
|
31
|
+
const g = globalThis;
|
|
32
|
+
if (typeof g.btoa === "function")
|
|
33
|
+
return g.btoa(text);
|
|
34
|
+
if (g.Buffer)
|
|
35
|
+
return g.Buffer.from(text, "utf8").toString("base64");
|
|
36
|
+
throw new Error("No base64 encoder available in this runtime.");
|
|
37
|
+
}
|
|
38
|
+
function b64decode(text) {
|
|
39
|
+
const g = globalThis;
|
|
40
|
+
if (typeof g.atob === "function")
|
|
41
|
+
return g.atob(text);
|
|
42
|
+
if (g.Buffer)
|
|
43
|
+
return g.Buffer.from(text, "base64").toString("utf8");
|
|
44
|
+
throw new Error("No base64 decoder available in this runtime.");
|
|
45
|
+
}
|
|
46
|
+
function randomNonce() {
|
|
47
|
+
const bytes = new Uint8Array(32);
|
|
48
|
+
const cryptoObj = globalThis.crypto;
|
|
49
|
+
if (cryptoObj?.getRandomValues) {
|
|
50
|
+
cryptoObj.getRandomValues(bytes);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
for (let i = 0; i < bytes.length; i++)
|
|
54
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
55
|
+
}
|
|
56
|
+
return "0x" + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
57
|
+
}
|
|
58
|
+
async function loadSigner(privateKey) {
|
|
59
|
+
let accounts;
|
|
60
|
+
try {
|
|
61
|
+
accounts = (await import("viem/accounts"));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error("The paid path requires the optional `viem` dependency. Install it with: npm install viem");
|
|
65
|
+
}
|
|
66
|
+
return accounts.privateKeyToAccount(privateKey);
|
|
67
|
+
}
|
|
68
|
+
export class X402Client {
|
|
69
|
+
signerPromise = null;
|
|
70
|
+
preferredTier;
|
|
71
|
+
autoLease;
|
|
72
|
+
cachedPass = null;
|
|
73
|
+
passExpiresAt = 0;
|
|
74
|
+
passTier = "";
|
|
75
|
+
constructor(options = {}) {
|
|
76
|
+
this.preferredTier = options.preferredTier ?? "task";
|
|
77
|
+
this.autoLease = options.autoLease !== false;
|
|
78
|
+
if (options.privateKey) {
|
|
79
|
+
this.signerPromise = loadSigner(options.privateKey);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** The payer address, when a private key was provided. */
|
|
83
|
+
async address() {
|
|
84
|
+
if (!this.signerPromise)
|
|
85
|
+
return null;
|
|
86
|
+
return (await this.signerPromise).address;
|
|
87
|
+
}
|
|
88
|
+
isPassActive() {
|
|
89
|
+
return Boolean(this.cachedPass && Date.now() < this.passExpiresAt - 5000);
|
|
90
|
+
}
|
|
91
|
+
get pass() {
|
|
92
|
+
return { token: this.cachedPass, tier: this.passTier, expiresAt: this.passExpiresAt };
|
|
93
|
+
}
|
|
94
|
+
/** Claim a 10-minute trial session pass from the hosted faucet (no wallet). */
|
|
95
|
+
async claimFaucet(baseUrl = "https://mcp.whitemagic.dev") {
|
|
96
|
+
const address = await this.address();
|
|
97
|
+
const res = await this.raw("POST", `${baseUrl.replace(/\/$/, "")}/faucet`, {
|
|
98
|
+
payer: address ?? "agent",
|
|
99
|
+
});
|
|
100
|
+
const data = res.data;
|
|
101
|
+
if (data.session_pass) {
|
|
102
|
+
this.cachedPass = data.session_pass;
|
|
103
|
+
this.passExpiresAt = Date.now() + Number(data.expires_in ?? 600) * 1000;
|
|
104
|
+
this.passTier = "faucet";
|
|
105
|
+
}
|
|
106
|
+
return res.data;
|
|
107
|
+
}
|
|
108
|
+
/** Request a Proof-of-Context notarization from the hosted API lane. */
|
|
109
|
+
async notarizeContext(input, baseUrl = "https://api.whitemagic.dev") {
|
|
110
|
+
const payload = {};
|
|
111
|
+
if (input.contextDigest)
|
|
112
|
+
payload.context_digest = input.contextDigest;
|
|
113
|
+
if (input.promptHash)
|
|
114
|
+
payload.prompt_hash = input.promptHash;
|
|
115
|
+
if (input.action)
|
|
116
|
+
payload.action = input.action;
|
|
117
|
+
if (input.model)
|
|
118
|
+
payload.model = input.model;
|
|
119
|
+
if (input.metadata)
|
|
120
|
+
payload.metadata = input.metadata;
|
|
121
|
+
const address = await this.address();
|
|
122
|
+
if (address)
|
|
123
|
+
payload.agent_id = address;
|
|
124
|
+
const res = await this.request("POST", `${baseUrl.replace(/\/$/, "")}/notarize`, payload);
|
|
125
|
+
if (res.status !== 200) {
|
|
126
|
+
throw new Error(`Notarization failed with status ${res.status}: ${JSON.stringify(res.data)}`);
|
|
127
|
+
}
|
|
128
|
+
return res.data;
|
|
129
|
+
}
|
|
130
|
+
/** Build a signed EIP-3009 payment payload from an x402 challenge. */
|
|
131
|
+
async createPaymentPayload(challenge, tier) {
|
|
132
|
+
if (!this.signerPromise) {
|
|
133
|
+
throw new Error("No private key provided to X402Client to sign the payment challenge.");
|
|
134
|
+
}
|
|
135
|
+
const signer = await this.signerPromise;
|
|
136
|
+
const accepts = challenge.accepts ?? [];
|
|
137
|
+
if (accepts.length === 0) {
|
|
138
|
+
throw new Error("Challenge contains no 'accepts' requirements.");
|
|
139
|
+
}
|
|
140
|
+
const chosenTier = tier ?? this.preferredTier;
|
|
141
|
+
const targetAmount = TIER_PRICES[chosenTier];
|
|
142
|
+
let req = accepts.find((c) => Number(c.amount) === targetAmount);
|
|
143
|
+
if (!req) {
|
|
144
|
+
req = accepts.reduce((min, c) => (Number(c.amount) < Number(min.amount) ? c : min));
|
|
145
|
+
}
|
|
146
|
+
const network = req.network ?? "eip155:8453";
|
|
147
|
+
const chainId = CHAIN_MAP[network];
|
|
148
|
+
if (chainId === undefined) {
|
|
149
|
+
throw new Error(`Unsupported network rail: ${network}`);
|
|
150
|
+
}
|
|
151
|
+
const extra = req.extra ?? {};
|
|
152
|
+
const now = Math.floor(Date.now() / 1000);
|
|
153
|
+
const authorization = {
|
|
154
|
+
from: signer.address,
|
|
155
|
+
to: req.payTo,
|
|
156
|
+
value: Number(req.amount),
|
|
157
|
+
validAfter: now - 60,
|
|
158
|
+
validBefore: now + 600,
|
|
159
|
+
nonce: randomNonce(),
|
|
160
|
+
};
|
|
161
|
+
const domain = {
|
|
162
|
+
name: extra.name ?? "USD Coin",
|
|
163
|
+
version: String(extra.version ?? "2"),
|
|
164
|
+
chainId,
|
|
165
|
+
verifyingContract: req.asset,
|
|
166
|
+
};
|
|
167
|
+
const signature = await signer.signTypedData({
|
|
168
|
+
domain,
|
|
169
|
+
types: EIP3009_TYPES,
|
|
170
|
+
primaryType: "TransferWithAuthorization",
|
|
171
|
+
message: authorization,
|
|
172
|
+
});
|
|
173
|
+
const payloadAuthorization = {
|
|
174
|
+
from: authorization.from,
|
|
175
|
+
to: authorization.to,
|
|
176
|
+
value: String(authorization.value),
|
|
177
|
+
validAfter: String(authorization.validAfter),
|
|
178
|
+
validBefore: String(authorization.validBefore),
|
|
179
|
+
nonce: authorization.nonce,
|
|
180
|
+
};
|
|
181
|
+
const payment = {
|
|
182
|
+
x402Version: challenge.x402Version ?? 2,
|
|
183
|
+
resource: challenge.resource,
|
|
184
|
+
accepted: req,
|
|
185
|
+
payload: { signature, authorization: payloadAuthorization },
|
|
186
|
+
extensions: challenge.extensions ?? {},
|
|
187
|
+
};
|
|
188
|
+
return { payment, header: b64encode(JSON.stringify(payment)) };
|
|
189
|
+
}
|
|
190
|
+
/** Execute a request with automatic 402 negotiation and session pass reuse. */
|
|
191
|
+
async request(method, url, data, headers, timeoutMs = 30000) {
|
|
192
|
+
const reqHeaders = { "Content-Type": "application/json", ...(headers ?? {}) };
|
|
193
|
+
if (this.isPassActive() && this.cachedPass) {
|
|
194
|
+
reqHeaders["Authorization"] = `Bearer ${this.cachedPass}`;
|
|
195
|
+
reqHeaders["X-Session-Pass"] = this.cachedPass;
|
|
196
|
+
}
|
|
197
|
+
const body = data === undefined
|
|
198
|
+
? undefined
|
|
199
|
+
: typeof data === "string"
|
|
200
|
+
? data
|
|
201
|
+
: JSON.stringify(data);
|
|
202
|
+
let res = await this.raw(method, url, body, reqHeaders, timeoutMs);
|
|
203
|
+
if (res.status === 402 && this.signerPromise) {
|
|
204
|
+
const challenge = await this.extractChallenge(res);
|
|
205
|
+
if (challenge && Array.isArray(challenge.accepts) && challenge.accepts.length > 0) {
|
|
206
|
+
const { header } = await this.createPaymentPayload(challenge);
|
|
207
|
+
const retryHeaders = {
|
|
208
|
+
...reqHeaders,
|
|
209
|
+
"PAYMENT-SIGNATURE": header,
|
|
210
|
+
"X-Lease-Tier": this.preferredTier,
|
|
211
|
+
};
|
|
212
|
+
res = await this.raw(method, url, body, retryHeaders, timeoutMs);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this.cachePassFromHeaders(res.headers);
|
|
216
|
+
return res;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* fetch()-style wrapper: same negotiation semantics for callers that want
|
|
220
|
+
* a drop-in replacement for global fetch.
|
|
221
|
+
*/
|
|
222
|
+
async fetch(input, init) {
|
|
223
|
+
const headers = new Headers(init?.headers ?? {});
|
|
224
|
+
if (this.isPassActive() && this.cachedPass) {
|
|
225
|
+
headers.set("Authorization", `Bearer ${this.cachedPass}`);
|
|
226
|
+
headers.set("X-Session-Pass", this.cachedPass);
|
|
227
|
+
}
|
|
228
|
+
let response = await globalThis.fetch(input, { ...init, headers });
|
|
229
|
+
if (response.status === 402 && this.signerPromise) {
|
|
230
|
+
const challenge = await this.extractChallengeFromResponse(response);
|
|
231
|
+
if (challenge && Array.isArray(challenge.accepts) && challenge.accepts.length > 0) {
|
|
232
|
+
const { header } = await this.createPaymentPayload(challenge);
|
|
233
|
+
const retryHeaders = new Headers(headers);
|
|
234
|
+
retryHeaders.set("PAYMENT-SIGNATURE", header);
|
|
235
|
+
retryHeaders.set("X-Lease-Tier", this.preferredTier);
|
|
236
|
+
response = await globalThis.fetch(input, { ...init, headers: retryHeaders });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const headerRecord = {};
|
|
240
|
+
response.headers.forEach((value, key) => {
|
|
241
|
+
headerRecord[key] = value;
|
|
242
|
+
});
|
|
243
|
+
this.cachePassFromHeaders(headerRecord);
|
|
244
|
+
return response;
|
|
245
|
+
}
|
|
246
|
+
async raw(method, url, body, headers, timeoutMs = 30000) {
|
|
247
|
+
const controller = new AbortController();
|
|
248
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
249
|
+
const payload = body === undefined ? undefined : typeof body === "string" ? body : JSON.stringify(body);
|
|
250
|
+
try {
|
|
251
|
+
const response = await globalThis.fetch(url, {
|
|
252
|
+
method,
|
|
253
|
+
headers: { "Content-Type": "application/json", ...(headers ?? {}) },
|
|
254
|
+
body: payload,
|
|
255
|
+
signal: controller.signal,
|
|
256
|
+
});
|
|
257
|
+
const headerRecord = {};
|
|
258
|
+
response.headers.forEach((value, key) => {
|
|
259
|
+
headerRecord[key] = value;
|
|
260
|
+
});
|
|
261
|
+
const text = await response.text();
|
|
262
|
+
let data = {};
|
|
263
|
+
try {
|
|
264
|
+
data = JSON.parse(text);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
data = { raw: text };
|
|
268
|
+
}
|
|
269
|
+
return { status: response.status, data, headers: headerRecord };
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
clearTimeout(timer);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async extractChallenge(res) {
|
|
276
|
+
const body = res.data;
|
|
277
|
+
if (body && Array.isArray(body.accepts))
|
|
278
|
+
return body;
|
|
279
|
+
const header = res.headers["payment-required"] ?? res.headers["PAYMENT-REQUIRED"];
|
|
280
|
+
if (header) {
|
|
281
|
+
try {
|
|
282
|
+
return JSON.parse(b64decode(header));
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
async extractChallengeFromResponse(response) {
|
|
291
|
+
const header = response.headers.get("PAYMENT-REQUIRED");
|
|
292
|
+
if (header) {
|
|
293
|
+
try {
|
|
294
|
+
return JSON.parse(b64decode(header));
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// fall through to the body
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const clone = response.clone();
|
|
302
|
+
const body = (await clone.json());
|
|
303
|
+
if (body && Array.isArray(body.accepts))
|
|
304
|
+
return body;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// not JSON
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
cachePassFromHeaders(headers) {
|
|
312
|
+
const sessionPass = headers["x-session-pass"];
|
|
313
|
+
const expiresIn = headers["x-session-expires-in"];
|
|
314
|
+
const tier = headers["x-session-tier"];
|
|
315
|
+
if (sessionPass && expiresIn) {
|
|
316
|
+
const secs = Number(expiresIn);
|
|
317
|
+
if (!Number.isNaN(secs)) {
|
|
318
|
+
this.cachedPass = sessionPass;
|
|
319
|
+
this.passExpiresAt = Date.now() + secs * 1000;
|
|
320
|
+
this.passTier = tier ?? this.preferredTier;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** Convenience helper mirroring the Python package's `wrap_session`. */
|
|
326
|
+
export function createX402Client(options = {}) {
|
|
327
|
+
return new X402Client(options);
|
|
328
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "whitemagic-pay",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Autonomous x402 payment client, session lease caching, and Proof-of-Context notarization for AI agents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"x402",
|
|
25
|
+
"agent",
|
|
26
|
+
"payments",
|
|
27
|
+
"usdc",
|
|
28
|
+
"base",
|
|
29
|
+
"eip-3009",
|
|
30
|
+
"notarization",
|
|
31
|
+
"whitemagic",
|
|
32
|
+
"mcp"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/lbailey94/whitemagic-pay.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://www.whitemagic.dev/hosted",
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"viem": ">=2"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"viem": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"typescript": "^5.5.0",
|
|
49
|
+
"viem": "^2.21.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc -p tsconfig.json",
|
|
53
|
+
"smoke": "node scripts/smoke.mjs"
|
|
54
|
+
}
|
|
55
|
+
}
|