@signet-base/cli 0.2.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/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Signet CLI
2
+
3
+ Command-line tool for [Signet](https://signet.sebayaki.com) onchain advertising.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # List recent spotlight signatures
9
+ npx signet-cli list
10
+ npx signet-cli list -n 10
11
+
12
+ # Estimate spotlight cost
13
+ npx signet-cli estimate
14
+ npx signet-cli estimate --hours 6
15
+
16
+ # Post a URL to spotlight (requires wallet)
17
+ npx signet-cli post --url https://example.com --hours 0
18
+ npx signet-cli post --url https://example.com --private-key 0x...
19
+ ```
20
+
21
+ ## Environment Variables
22
+
23
+ | Variable | Description |
24
+ |----------|-------------|
25
+ | `PRIVATE_KEY` | Wallet private key for posting (alternative to `--private-key`) |
26
+
27
+ ## How It Works
28
+
29
+ 1. **`list`** — Fetches recent signatures from the Signet API
30
+ 2. **`estimate`** — Queries the estimated USDC cost for a spotlight placement
31
+ 3. **`post`** — Uses the [x402 payment protocol](https://www.x402.org) to pay USDC and place a URL on the spotlight
package/bin/signet.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import { list } from "../src/commands/list.js";
4
+ import { post } from "../src/commands/post.js";
5
+ import { estimate } from "../src/commands/estimate.js";
6
+
7
+ program
8
+ .name("signet")
9
+ .description("Signet — onchain advertising CLI")
10
+ .version("0.2.0");
11
+
12
+ program
13
+ .command("list")
14
+ .description("List recent spotlight signatures")
15
+ .option("-n, --count <number>", "Number of signatures to show", "5")
16
+ .option("--base-url <url>", "Signet API base URL", "https://signet.sebayaki.com")
17
+ .action(list);
18
+
19
+ program
20
+ .command("estimate")
21
+ .description("Estimate USDC cost for spotlight placement")
22
+ .option("-h, --hours <number>", "Guarantee hours (0-24)", "0")
23
+ .option("--simulate", "Simulate mode (same as normal for estimate)")
24
+ .option("--base-url <url>", "Signet API base URL", "https://signet.sebayaki.com")
25
+ .action(estimate);
26
+
27
+ program
28
+ .command("post")
29
+ .description("Place a URL on the Signet spotlight via x402 payment")
30
+ .requiredOption("-u, --url <url>", "URL to advertise")
31
+ .option("-h, --hours <number>", "Guarantee hours (0-24)", "0")
32
+ .option("-k, --private-key <key>", "Wallet private key (or set PRIVATE_KEY env)")
33
+ .option("--simulate", "Simulate: show estimate and payment requirements without posting")
34
+ .option("--base-url <url>", "Signet API base URL", "https://signet.sebayaki.com")
35
+ .action(post);
36
+
37
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@signet-base/cli",
3
+ "version": "0.2.0",
4
+ "description": "CLI for Signet onchain advertising — list and post spotlight ads",
5
+ "type": "module",
6
+ "bin": {
7
+ "signet": "./bin/signet.js",
8
+ "signet-cli": "./bin/signet.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/signet.js",
12
+ "test": "node --test test/*.test.js"
13
+ },
14
+ "dependencies": {
15
+ "@x402/core": "^2.3.0",
16
+ "@x402/evm": "^2.3.0",
17
+ "commander": "^13.0.0",
18
+ "viem": "^2.30.0"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "license": "MIT"
24
+ }
package/src/api.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Signet API client
3
+ */
4
+
5
+ const DEFAULT_BASE_URL = "https://signet.sebayaki.com";
6
+
7
+ export async function fetchEstimate(guaranteeHours = 0, baseUrl = DEFAULT_BASE_URL) {
8
+ const res = await fetch(`${baseUrl}/api/x402/estimate?guaranteeHours=${guaranteeHours}`);
9
+ if (!res.ok) throw new Error(`Estimate failed: ${res.status} ${await res.text()}`);
10
+ return res.json();
11
+ }
12
+
13
+ export async function fetchSignatures(startIndex = 0, endIndex = 5, baseUrl = DEFAULT_BASE_URL) {
14
+ const res = await fetch(
15
+ `${baseUrl}/api/signature/list?startIndex=${startIndex}&endIndex=${endIndex}`
16
+ );
17
+ if (!res.ok) throw new Error(`List failed: ${res.status} ${await res.text()}`);
18
+ return res.json();
19
+ }
20
+
21
+ /**
22
+ * POST to spotlight endpoint. Returns the raw Response for 402 handling.
23
+ */
24
+ export async function postSpotlightRaw({ url, guaranteeHours, headers = {}, baseUrl = DEFAULT_BASE_URL }) {
25
+ return fetch(`${baseUrl}/api/x402/spotlight`, {
26
+ method: "POST",
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ ...headers,
30
+ },
31
+ body: JSON.stringify({ url, guaranteeHours }),
32
+ });
33
+ }
@@ -0,0 +1,24 @@
1
+ import { fetchEstimate } from "../api.js";
2
+
3
+ export async function estimate(opts) {
4
+ const hours = parseInt(opts.hours);
5
+
6
+ try {
7
+ const data = await fetchEstimate(hours, opts.baseUrl);
8
+
9
+ console.log(`\n💰 Signet Spotlight Estimate${opts.simulate ? " (SIMULATE)" : ""}\n`);
10
+ console.log(` Guarantee Hours: ${data.guaranteeHours}`);
11
+ console.log(` Estimated Cost: $${data.estimatedUSDC} USDC`);
12
+ console.log(` Spotlight Available: ${data.spotlightAvailable ? "✅ Yes" : "❌ No"}`);
13
+
14
+ if (data.spotlightRemainingSeconds > 0) {
15
+ const mins = Math.ceil(data.spotlightRemainingSeconds / 60);
16
+ console.log(` Current Guarantee Remaining: ${mins} min`);
17
+ }
18
+
19
+ console.log();
20
+ } catch (err) {
21
+ console.error("❌", err.message);
22
+ process.exit(1);
23
+ }
24
+ }
@@ -0,0 +1,32 @@
1
+ import { fetchSignatures } from "../api.js";
2
+
3
+ export async function list(opts) {
4
+ const count = parseInt(opts.count);
5
+ const endIndex = count;
6
+
7
+ try {
8
+ const { signatures } = await fetchSignatures(0, endIndex, opts.baseUrl);
9
+
10
+ if (!signatures?.length) {
11
+ console.log("No signatures found.");
12
+ return;
13
+ }
14
+
15
+ console.log(`\n📋 Recent Signet Signatures (${signatures.length})\n`);
16
+
17
+ for (const sig of signatures) {
18
+ const date = new Date(sig.timestamp * 1000).toLocaleDateString();
19
+ const hunt = (Number(sig.huntAmount) / 1e18).toFixed(2);
20
+ const title = sig.metadata?.title || "(no title)";
21
+
22
+ console.log(` #${sig.signatureIndex} — ${title}`);
23
+ console.log(` URL: ${sig.url}`);
24
+ console.log(` HUNT: ${hunt} | Views: ${sig.viewCount} | Clicks: ${sig.clickCount}`);
25
+ console.log(` Date: ${date} | Wallet: ${sig.userWallet.slice(0, 8)}...`);
26
+ console.log();
27
+ }
28
+ } catch (err) {
29
+ console.error("❌", err.message);
30
+ process.exit(1);
31
+ }
32
+ }
@@ -0,0 +1,110 @@
1
+ import { x402Client, x402HTTPClient } from "@x402/core/client";
2
+ import { registerExactEvmScheme } from "@x402/evm/exact/client";
3
+ import { privateKeyToAccount } from "viem/accounts";
4
+ import { fetchEstimate, postSpotlightRaw } from "../api.js";
5
+
6
+ export async function post(opts) {
7
+ const privateKey = opts.privateKey || process.env.PRIVATE_KEY;
8
+ if (!privateKey) {
9
+ console.error("❌ Wallet private key required. Use --private-key or set PRIVATE_KEY env.");
10
+ process.exit(1);
11
+ }
12
+
13
+ const hours = parseInt(opts.hours);
14
+ const url = opts.url;
15
+ const simulate = opts.simulate || false;
16
+
17
+ console.log(`\n🎯 Posting to Signet Spotlight${simulate ? " (SIMULATE)" : ""}`);
18
+ console.log(` URL: ${url}`);
19
+ console.log(` Guarantee: ${hours}h\n`);
20
+
21
+ try {
22
+ // Step 0: Get estimate
23
+ console.log("0️⃣ Fetching estimate...");
24
+ const est = await fetchEstimate(hours, opts.baseUrl);
25
+ console.log(` Estimated Cost: $${est.estimatedUSDC} USDC`);
26
+ console.log(` Available: ${est.spotlightAvailable ? "Yes" : "No"}\n`);
27
+
28
+ if (!est.spotlightAvailable) {
29
+ console.log(` ⚠️ Spotlight is currently guaranteed. ${Math.ceil(est.spotlightRemainingSeconds / 60)} min remaining.`);
30
+ }
31
+
32
+ // Step 1: POST without payment to get 402 response
33
+ console.log("1️⃣ Getting payment requirements...");
34
+ const res402 = await postSpotlightRaw({
35
+ url,
36
+ guaranteeHours: hours,
37
+ baseUrl: opts.baseUrl,
38
+ });
39
+
40
+ if (res402.status !== 402) {
41
+ if (res402.ok) {
42
+ console.log("✅ Posted (no payment was required):", await res402.json());
43
+ } else {
44
+ console.error("❌ Unexpected response:", res402.status, await res402.text());
45
+ }
46
+ return;
47
+ }
48
+
49
+ // Parse 402 using x402 HTTP client
50
+ const account = privateKeyToAccount(privateKey);
51
+ const client = new x402Client();
52
+ registerExactEvmScheme(client, { signer: account });
53
+ const httpClient = new x402HTTPClient(client);
54
+
55
+ const body402 = await res402.json();
56
+ const paymentRequired = httpClient.getPaymentRequiredResponse(
57
+ (name) => res402.headers.get(name),
58
+ body402
59
+ );
60
+
61
+ const accepts = paymentRequired.accepts?.[0];
62
+ const displayAmount = accepts?.amount && BigInt(accepts.amount) > 1000000n
63
+ ? (Number(accepts.amount) / 1e6).toFixed(6)
64
+ : accepts?.amount;
65
+ console.log(` Price: $${displayAmount} USDC`);
66
+ console.log(` Pay to: ${accepts?.payTo}`);
67
+ console.log(` Network: ${accepts?.network}\n`);
68
+
69
+ // Step 2: Create x402 payment signature
70
+ console.log("2️⃣ Creating x402 payment signature...");
71
+ console.log(` Signer: ${account.address}`);
72
+
73
+ const paymentPayload = await httpClient.createPaymentPayload(paymentRequired);
74
+ const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
75
+
76
+ if (simulate) {
77
+ console.log("\n🔍 SIMULATE MODE — payment signed successfully!");
78
+ console.log(" Payment payload created and encoded.");
79
+ console.log(" Would submit with payment headers to execute spotlight placement.");
80
+ console.log("\n✅ Simulation complete. No payment submitted.\n");
81
+ return;
82
+ }
83
+
84
+ // Step 3: Submit with payment
85
+ console.log("\n3️⃣ Submitting with payment...");
86
+ const resPost = await postSpotlightRaw({
87
+ url,
88
+ guaranteeHours: hours,
89
+ headers: paymentHeaders,
90
+ baseUrl: opts.baseUrl,
91
+ });
92
+
93
+ if (!resPost.ok) {
94
+ const err = await resPost.json().catch(() => ({ error: `HTTP ${resPost.status}` }));
95
+ console.error("❌ Payment rejected:", err.error || JSON.stringify(err));
96
+ process.exit(1);
97
+ }
98
+
99
+ const result = await resPost.json();
100
+ console.log("\n✅ Spotlight posted!");
101
+ console.log(` TX: ${result.txHash}`);
102
+ console.log(` Block: ${result.blockNumber}`);
103
+ console.log(` USDC Spent: $${result.usdcSpent}`);
104
+ console.log(` URL: ${result.url}`);
105
+ console.log(` Guarantee: ${result.guaranteeHours}h\n`);
106
+ } catch (err) {
107
+ console.error("❌", err.message);
108
+ process.exit(1);
109
+ }
110
+ }
@@ -0,0 +1,96 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { fetchEstimate, fetchSignatures, postSpotlight } from "../src/api.js";
4
+
5
+ describe("api", () => {
6
+ let originalFetch;
7
+
8
+ beforeEach(() => {
9
+ originalFetch = global.fetch;
10
+ });
11
+
12
+ afterEach(() => {
13
+ global.fetch = originalFetch;
14
+ });
15
+
16
+ describe("fetchEstimate", () => {
17
+ it("returns estimate data", async () => {
18
+ const mockData = {
19
+ guaranteeHours: 6,
20
+ estimatedUSDC: "12.28",
21
+ spotlightAvailable: true,
22
+ spotlightRemainingSeconds: 0,
23
+ };
24
+ global.fetch = async (url) => {
25
+ assert.ok(url.includes("/api/x402/estimate?guaranteeHours=6"));
26
+ return { ok: true, json: async () => mockData };
27
+ };
28
+ const result = await fetchEstimate(6);
29
+ assert.deepStrictEqual(result, mockData);
30
+ });
31
+
32
+ it("throws on non-ok response", async () => {
33
+ global.fetch = async () => ({
34
+ ok: false,
35
+ status: 500,
36
+ text: async () => "Server Error",
37
+ });
38
+ await assert.rejects(() => fetchEstimate(0), /Estimate failed: 500/);
39
+ });
40
+ });
41
+
42
+ describe("fetchSignatures", () => {
43
+ it("returns signatures", async () => {
44
+ const mockData = { signatures: [{ signatureIndex: 1 }] };
45
+ global.fetch = async (url) => {
46
+ assert.ok(url.includes("startIndex=0&endIndex=5"));
47
+ return { ok: true, json: async () => mockData };
48
+ };
49
+ const result = await fetchSignatures(0, 5);
50
+ assert.deepStrictEqual(result, mockData);
51
+ });
52
+
53
+ it("throws on error", async () => {
54
+ global.fetch = async () => ({
55
+ ok: false,
56
+ status: 404,
57
+ text: async () => "Not Found",
58
+ });
59
+ await assert.rejects(() => fetchSignatures(), /List failed: 404/);
60
+ });
61
+ });
62
+
63
+ describe("postSpotlight", () => {
64
+ it("returns 402 requirements", async () => {
65
+ const requirements = { x402Version: 2, accepts: [{ amount: "12.28" }] };
66
+ global.fetch = async () => ({
67
+ ok: false,
68
+ status: 402,
69
+ json: async () => requirements,
70
+ });
71
+ const result = await postSpotlight({ url: "https://example.com", guaranteeHours: 0 });
72
+ assert.strictEqual(result.status, 402);
73
+ assert.deepStrictEqual(result.requirements, requirements);
74
+ });
75
+
76
+ it("returns 200 on success", async () => {
77
+ const data = { txHash: "0xabc", url: "https://example.com" };
78
+ global.fetch = async () => ({
79
+ ok: true,
80
+ status: 200,
81
+ json: async () => data,
82
+ });
83
+ const result = await postSpotlight({ url: "https://example.com", guaranteeHours: 0 });
84
+ assert.strictEqual(result.status, 200);
85
+ assert.deepStrictEqual(result.data, data);
86
+ });
87
+
88
+ it("includes payment header when provided", async () => {
89
+ global.fetch = async (url, init) => {
90
+ assert.strictEqual(init.headers["PAYMENT-SIGNATURE"], "test-header");
91
+ return { ok: true, status: 200, json: async () => ({}) };
92
+ };
93
+ await postSpotlight({ url: "https://example.com", guaranteeHours: 0, paymentHeader: "test-header" });
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,58 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { estimate } from "../src/commands/estimate.js";
4
+
5
+ describe("estimate command", () => {
6
+ let originalFetch;
7
+ let output;
8
+
9
+ beforeEach(() => {
10
+ originalFetch = global.fetch;
11
+ output = [];
12
+ const origLog = console.log;
13
+ console.log = (...args) => output.push(args.join(" "));
14
+ // Store for restore
15
+ console._origLog = origLog;
16
+ });
17
+
18
+ afterEach(() => {
19
+ global.fetch = originalFetch;
20
+ console.log = console._origLog;
21
+ });
22
+
23
+ it("displays estimate info", async () => {
24
+ global.fetch = async () => ({
25
+ ok: true,
26
+ json: async () => ({
27
+ guaranteeHours: 6,
28
+ estimatedUSDC: "12.28",
29
+ spotlightAvailable: true,
30
+ spotlightRemainingSeconds: 0,
31
+ }),
32
+ });
33
+
34
+ await estimate({ hours: "6", baseUrl: "https://signet.sebayaki.com" });
35
+
36
+ const text = output.join("\n");
37
+ assert.ok(text.includes("12.28"), "should show USDC cost");
38
+ assert.ok(text.includes("6"), "should show hours");
39
+ assert.ok(text.includes("Yes"), "should show available");
40
+ });
41
+
42
+ it("shows remaining time when applicable", async () => {
43
+ global.fetch = async () => ({
44
+ ok: true,
45
+ json: async () => ({
46
+ guaranteeHours: 0,
47
+ estimatedUSDC: "5.00",
48
+ spotlightAvailable: false,
49
+ spotlightRemainingSeconds: 3600,
50
+ }),
51
+ });
52
+
53
+ await estimate({ hours: "0", baseUrl: "https://signet.sebayaki.com" });
54
+
55
+ const text = output.join("\n");
56
+ assert.ok(text.includes("60 min"), "should show remaining minutes");
57
+ });
58
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { list } from "../src/commands/list.js";
4
+
5
+ describe("list command", () => {
6
+ let originalFetch;
7
+ let output;
8
+
9
+ beforeEach(() => {
10
+ originalFetch = global.fetch;
11
+ output = [];
12
+ const origLog = console.log;
13
+ console.log = (...args) => output.push(args.join(" "));
14
+ console._origLog = origLog;
15
+ });
16
+
17
+ afterEach(() => {
18
+ global.fetch = originalFetch;
19
+ console.log = console._origLog;
20
+ });
21
+
22
+ it("displays signatures", async () => {
23
+ global.fetch = async () => ({
24
+ ok: true,
25
+ json: async () => ({
26
+ signatures: [
27
+ {
28
+ signatureIndex: 42,
29
+ url: "https://example.com",
30
+ huntAmount: "1000000000000000000",
31
+ viewCount: 100,
32
+ clickCount: 10,
33
+ timestamp: 1700000000,
34
+ userWallet: "0x1234567890abcdef1234567890abcdef12345678",
35
+ metadata: { title: "Test Ad" },
36
+ },
37
+ ],
38
+ }),
39
+ });
40
+
41
+ await list({ count: "5", baseUrl: "https://signet.sebayaki.com" });
42
+
43
+ const text = output.join("\n");
44
+ assert.ok(text.includes("#42"), "should show signature index");
45
+ assert.ok(text.includes("Test Ad"), "should show title");
46
+ assert.ok(text.includes("https://example.com"), "should show URL");
47
+ assert.ok(text.includes("1.00"), "should show HUNT amount");
48
+ });
49
+
50
+ it("handles empty list", async () => {
51
+ global.fetch = async () => ({
52
+ ok: true,
53
+ json: async () => ({ signatures: [] }),
54
+ });
55
+
56
+ await list({ count: "5", baseUrl: "https://signet.sebayaki.com" });
57
+
58
+ const text = output.join("\n");
59
+ assert.ok(text.includes("No signatures found"), "should show empty message");
60
+ });
61
+ });
@@ -0,0 +1,153 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { fetchEstimate, postSpotlight } from "../src/api.js";
4
+
5
+ describe("post command simulate flow", () => {
6
+ let originalFetch;
7
+
8
+ beforeEach(() => {
9
+ originalFetch = global.fetch;
10
+ });
11
+
12
+ afterEach(() => {
13
+ global.fetch = originalFetch;
14
+ });
15
+
16
+ it("simulate flow: estimate → 402 → payment requirements extracted", async () => {
17
+ let fetchCalls = [];
18
+
19
+ const paymentRequirements = {
20
+ x402Version: 2,
21
+ accepts: [
22
+ {
23
+ scheme: "exact",
24
+ network: "eip155:8453",
25
+ asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
26
+ amount: "12.28",
27
+ payTo: "0x78981Ca2f04F97975EaA5b2d69Bc1db50459bDe5",
28
+ maxTimeoutSeconds: 300,
29
+ extra: {},
30
+ },
31
+ ],
32
+ resource: {
33
+ url: "/api/x402/spotlight",
34
+ description: "Place URL on Signet spotlight",
35
+ mimeType: "application/json",
36
+ },
37
+ };
38
+
39
+ global.fetch = async (url, init) => {
40
+ fetchCalls.push({ url, method: init?.method || "GET" });
41
+
42
+ if (url.includes("/api/x402/estimate")) {
43
+ return {
44
+ ok: true,
45
+ json: async () => ({
46
+ guaranteeHours: 0,
47
+ estimatedUSDC: "12.28",
48
+ spotlightAvailable: true,
49
+ spotlightRemainingSeconds: 0,
50
+ }),
51
+ };
52
+ }
53
+
54
+ if (url.includes("/api/x402/spotlight")) {
55
+ return {
56
+ ok: false,
57
+ status: 402,
58
+ json: async () => paymentRequirements,
59
+ };
60
+ }
61
+
62
+ throw new Error(`Unexpected fetch: ${url}`);
63
+ };
64
+
65
+ // Step 1: Estimate
66
+ const est = await fetchEstimate(0);
67
+ assert.strictEqual(est.estimatedUSDC, "12.28");
68
+ assert.strictEqual(est.spotlightAvailable, true);
69
+
70
+ // Step 2: Post without payment → 402
71
+ const result = await postSpotlight({ url: "https://example.com", guaranteeHours: 0 });
72
+ assert.strictEqual(result.status, 402);
73
+
74
+ const reqs = result.requirements;
75
+ assert.strictEqual(reqs.x402Version, 2);
76
+ assert.strictEqual(reqs.accepts.length, 1);
77
+ assert.strictEqual(reqs.accepts[0].scheme, "exact");
78
+ assert.strictEqual(reqs.accepts[0].network, "eip155:8453");
79
+ assert.strictEqual(reqs.accepts[0].amount, "12.28");
80
+ assert.strictEqual(reqs.accepts[0].payTo, "0x78981Ca2f04F97975EaA5b2d69Bc1db50459bDe5");
81
+ assert.strictEqual(reqs.accepts[0].asset, "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913");
82
+
83
+ // In simulate mode, we would stop here (no 3rd fetch)
84
+ assert.strictEqual(fetchCalls.length, 2);
85
+ assert.strictEqual(fetchCalls[0].method, "GET");
86
+ assert.strictEqual(fetchCalls[1].method, "POST");
87
+ });
88
+
89
+ it("full flow: estimate → 402 → payment → success", async () => {
90
+ let fetchCalls = [];
91
+
92
+ global.fetch = async (url, init) => {
93
+ fetchCalls.push({ url, method: init?.method || "GET", hasPayment: !!init?.headers?.["PAYMENT-SIGNATURE"] });
94
+
95
+ if (url.includes("/api/x402/estimate")) {
96
+ return {
97
+ ok: true,
98
+ json: async () => ({
99
+ guaranteeHours: 0,
100
+ estimatedUSDC: "12.28",
101
+ spotlightAvailable: true,
102
+ spotlightRemainingSeconds: 0,
103
+ }),
104
+ };
105
+ }
106
+
107
+ if (url.includes("/api/x402/spotlight") && !init?.headers?.["PAYMENT-SIGNATURE"]) {
108
+ return {
109
+ ok: false,
110
+ status: 402,
111
+ json: async () => ({
112
+ x402Version: 2,
113
+ accepts: [{ scheme: "exact", network: "eip155:8453", amount: "12.28", payTo: "0x78981Ca2f04F97975EaA5b2d69Bc1db50459bDe5", asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", maxTimeoutSeconds: 300, extra: {} }],
114
+ }),
115
+ };
116
+ }
117
+
118
+ if (url.includes("/api/x402/spotlight") && init?.headers?.["PAYMENT-SIGNATURE"]) {
119
+ return {
120
+ ok: true,
121
+ status: 200,
122
+ json: async () => ({
123
+ txHash: "0xabc123",
124
+ blockNumber: 12345,
125
+ usdcSpent: "12.28",
126
+ url: "https://example.com",
127
+ guaranteeHours: 0,
128
+ }),
129
+ };
130
+ }
131
+
132
+ throw new Error(`Unexpected fetch: ${url}`);
133
+ };
134
+
135
+ const est = await fetchEstimate(0);
136
+ const initial = await postSpotlight({ url: "https://example.com", guaranteeHours: 0 });
137
+ assert.strictEqual(initial.status, 402);
138
+
139
+ // Simulate creating a payment header
140
+ const paymentHeader = Buffer.from("test-payment").toString("base64");
141
+
142
+ const result = await postSpotlight({
143
+ url: "https://example.com",
144
+ guaranteeHours: 0,
145
+ paymentHeader,
146
+ });
147
+
148
+ assert.strictEqual(result.status, 200);
149
+ assert.strictEqual(result.data.txHash, "0xabc123");
150
+ assert.strictEqual(fetchCalls.length, 3);
151
+ assert.strictEqual(fetchCalls[2].hasPayment, true);
152
+ });
153
+ });