diff --git a/apps/agent-client/package.json b/apps/agent-client/package.json
index 4d670bb..8bc2932 100644
--- a/apps/agent-client/package.json
+++ b/apps/agent-client/package.json
@@ -12,7 +12,7 @@
"validate:real": "tsx src/validate-real.ts",
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
- "test": "vitest run",
+ "test": "vitest run --test-timeout=30000",
"lint": "eslint src",
"format": "prettier --write src",
"format:check": "prettier --check src"
diff --git a/apps/agent-client/src/cli.test.ts b/apps/agent-client/src/cli.test.ts
index 4125076..7cbb875 100644
--- a/apps/agent-client/src/cli.test.ts
+++ b/apps/agent-client/src/cli.test.ts
@@ -5,7 +5,7 @@ import { resolve } from "path";
const execAsync = promisify(exec);
// Workaround for Windows cross-platform testing
-const tsx = process.platform === "win32" ? "npx.cmd tsx" : "npx tsx";
+const tsx = process.platform === "win32" ? "npx.cmd --yes tsx" : "npx --yes tsx";
const cliPath = resolve(__dirname, "cli.ts");
describe("CLI Validation", () => {
diff --git a/apps/api/package.json b/apps/api/package.json
index 72e0010..d89af72 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -10,8 +10,8 @@
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"typecheck": "tsc -p tsconfig.json --noEmit",
- "test": "vitest run",
- "test:coverage": "vitest run --coverage",
+ "test": "vitest run --test-timeout=30000",
+ "test:coverage": "vitest run --coverage --test-timeout=30000",
"test:watch": "vitest",
"migrate:analytics": "tsx src/scripts/migrate-db-json.ts",
"lint": "eslint src",
diff --git a/apps/api/src/lib/config.ts b/apps/api/src/lib/config.ts
index f7dd956..a31a772 100644
--- a/apps/api/src/lib/config.ts
+++ b/apps/api/src/lib/config.ts
@@ -34,7 +34,7 @@ const envSchema = z.object({
STELLAR_RPC_URL: z.string().url().default("https://soroban-testnet.stellar.org"),
X402_FACILITATOR_URL: z.string().url().default("https://channels.openzeppelin.com/x402/testnet"),
X402_FACILITATOR_API_KEY: z.string().optional(),
- X402_PAY_TO_ADDRESS: z.string().min(10, "X402_PAY_TO_ADDRESS is required"),
+ X402_PAY_TO_ADDRESS: z.string().min(10, "X402_PAY_TO_ADDRESS is required").optional(),
API_BASE_URL: z.string().url().default("http://localhost:3001"),
CORS_ORIGINS: z.string().optional(),
DEMO_CLIENT_SECRET_KEY: z.string().optional(),
@@ -93,6 +93,8 @@ export interface ConfigSnapshot {
facilitatorApiKeyConfigured: boolean;
/** Whether a pay-to Stellar address is configured */
payToConfigured: boolean;
+ /** The configured pay-to Stellar public address if available */
+ payToAddress?: string;
/** Whether sponsorship/subsidy mode is enabled */
sponsorshipEnabled: boolean;
/** Whether a sponsorship signing secret is configured (value never exposed) */
@@ -112,6 +114,7 @@ export function getConfigSnapshot(): ConfigSnapshot {
facilitatorConfigured: Boolean(config.X402_FACILITATOR_URL),
facilitatorApiKeyConfigured: Boolean(config.X402_FACILITATOR_API_KEY),
payToConfigured: Boolean(config.X402_PAY_TO_ADDRESS),
+ payToAddress: config.X402_PAY_TO_ADDRESS,
sponsorshipEnabled: config.sponsorshipEnabled,
sponsorshipSigningSecretConfigured: Boolean(config.SPONSORSHIP_SIGNING_SECRET),
anyProviderKeyConfigured: Boolean(
diff --git a/apps/api/src/lib/logger.ts b/apps/api/src/lib/logger.ts
index 65ffc07..d734332 100644
--- a/apps/api/src/lib/logger.ts
+++ b/apps/api/src/lib/logger.ts
@@ -1,4 +1,4 @@
-import pino from "pino";
+import { pino } from "pino";
export const logger = pino({
level: process.env.NODE_ENV === "development" ? "debug" : "info",
diff --git a/apps/api/src/lib/payment-evidence.ts b/apps/api/src/lib/payment-evidence.ts
index 74622c1..9c051f5 100644
--- a/apps/api/src/lib/payment-evidence.ts
+++ b/apps/api/src/lib/payment-evidence.ts
@@ -295,7 +295,7 @@ export function buildDemoPaymentEvidence(req: Request): DemoPaymentEvidence {
providerId,
amountUsd,
network: config.STELLAR_NETWORK,
- payTo: config.X402_PAY_TO_ADDRESS,
+ payTo: config.X402_PAY_TO_ADDRESS ?? "",
facilitatorUrl: config.X402_FACILITATOR_URL,
payer: req.header("x-demo-payer") ?? "demo-agent"
};
diff --git a/apps/api/src/lib/persistence.ts b/apps/api/src/lib/persistence.ts
index 6dcc79c..5a55166 100644
--- a/apps/api/src/lib/persistence.ts
+++ b/apps/api/src/lib/persistence.ts
@@ -49,7 +49,7 @@ function buildPaymentAttempt(
amountUsd: input.priceUsd,
network: config.STELLAR_NETWORK,
payerPublicKey: input.payerPublicKey,
- payToAddress: config.X402_PAY_TO_ADDRESS,
+ payToAddress: config.X402_PAY_TO_ADDRESS ?? "",
facilitatorUrl: config.X402_FACILITATOR_URL,
status: "settled",
transactionHash: input.paymentResponseHeader ?? undefined,
diff --git a/apps/api/src/lib/storage/memory.test.ts b/apps/api/src/lib/storage/memory.test.ts
index 4ef1d91..1e08da5 100644
--- a/apps/api/src/lib/storage/memory.test.ts
+++ b/apps/api/src/lib/storage/memory.test.ts
@@ -6,8 +6,8 @@ import { buildTestPaymentAttempt, buildTestUsageEvent } from "../../test/storage
describe("storage paths", () => {
it("resolves relative data paths from the API package root", () => {
- expect(resolveApiDataPath("data/analytics.db")).toMatch(/apps\/api\/data\/analytics\.db$/);
- expect(resolveApiDataPath("data/analytics.db")).not.toContain("/src/data/");
+ expect(resolveApiDataPath("data/analytics.db").replace(/\\/g, "/")).toMatch(/apps\/api\/data\/analytics\.db$/);
+ expect(resolveApiDataPath("data/analytics.db").replace(/\\/g, "/")).not.toContain("/src/data/");
});
});
diff --git a/apps/api/src/lib/x402.ts b/apps/api/src/lib/x402.ts
index 5bf6315..b8a23fd 100644
--- a/apps/api/src/lib/x402.ts
+++ b/apps/api/src/lib/x402.ts
@@ -105,7 +105,7 @@ function demoMode402Middleware(req: Request, res: Response, next: NextFunction)
scheme: "exact",
network: config.STELLAR_NETWORK,
price,
- payTo: config.X402_PAY_TO_ADDRESS,
+ payTo: config.X402_PAY_TO_ADDRESS ?? "",
facilitator: config.X402_FACILITATOR_URL
},
instructions:
@@ -184,7 +184,7 @@ export function createX402Middleware() {
scheme: "exact",
network,
price: (context: HTTPRequestContext) => resolveRoutePrice(context, "search"),
- payTo: config.X402_PAY_TO_ADDRESS
+ payTo: config.X402_PAY_TO_ADDRESS ?? ""
},
description: "Paid search endpoint on Query402",
settlementFailedResponseBody
@@ -194,7 +194,7 @@ export function createX402Middleware() {
scheme: "exact",
network,
price: (context: HTTPRequestContext) => resolveRoutePrice(context, "news"),
- payTo: config.X402_PAY_TO_ADDRESS
+ payTo: config.X402_PAY_TO_ADDRESS ?? ""
},
description: "Paid news endpoint on Query402",
settlementFailedResponseBody
@@ -204,7 +204,7 @@ export function createX402Middleware() {
scheme: "exact",
network,
price: (context: HTTPRequestContext) => resolveRoutePrice(context, "scrape"),
- payTo: config.X402_PAY_TO_ADDRESS
+ payTo: config.X402_PAY_TO_ADDRESS ?? ""
},
description: "Paid scrape endpoint on Query402",
settlementFailedResponseBody
diff --git a/apps/api/src/routes/public.test.ts b/apps/api/src/routes/public.test.ts
index a9262cf..b2b6d31 100644
--- a/apps/api/src/routes/public.test.ts
+++ b/apps/api/src/routes/public.test.ts
@@ -78,6 +78,29 @@ describe("public routes", () => {
expect(response.body.diagnostics.payToConfigured).toBe(true); // TEST_WALLET is set by applySponsorshipTestEnv
});
+ it("health diagnostics exposes payToAddress when configured", async () => {
+ applyApiTestEnv({ X402_PAY_TO_ADDRESS: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF" });
+ const app = await createPublicApp();
+ const response = await request(app).get("/health");
+
+ expect(response.status).toBe(200);
+ expect(response.body.diagnostics.payToConfigured).toBe(true);
+ expect(response.body.diagnostics.payToAddress).toBe("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF");
+ });
+
+ it("health diagnostics handles missing/empty payToAddress safely", async () => {
+ delete process.env.X402_PAY_TO_ADDRESS;
+ applyApiTestEnv({ X402_PAY_TO_ADDRESS: "" });
+ delete process.env.X402_PAY_TO_ADDRESS;
+
+ const app = await createPublicApp();
+ const response = await request(app).get("/health");
+
+ expect(response.status).toBe(200);
+ expect(response.body.diagnostics.payToConfigured).toBe(false);
+ expect(response.body.diagnostics.payToAddress).toBeUndefined();
+ });
+
describe("health diagnostics — secret redaction", () => {
it("never exposes raw secret values in health response body", async () => {
// Set all secret-like env vars to recognisable sentinel values,
diff --git a/apps/web/src/pages/ControlDeckPage.tsx b/apps/web/src/pages/ControlDeckPage.tsx
index 78144f6..e39d5cd 100644
--- a/apps/web/src/pages/ControlDeckPage.tsx
+++ b/apps/web/src/pages/ControlDeckPage.tsx
@@ -12,7 +12,10 @@ import {
ShieldCheck,
Sparkles,
TerminalSquare,
- XCircle
+ XCircle,
+ Copy,
+ Check,
+ AlertTriangle
} from "lucide-react";
import { Link } from "react-router-dom";
import type { AnalyticsResponse, PaidQueryResponse } from "../types.js";
@@ -45,6 +48,64 @@ function toTokenBaseUnits(amountUsd: number) {
return normalizedAmount.replace(".", "").replace(/^0+/, "") || "0";
}
+function PayToAddressDisplay({
+ address,
+ network,
+ configured
+}: {
+ address?: string;
+ network?: string;
+ configured?: boolean;
+}) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = async () => {
+ if (!address) return;
+ try {
+ await navigator.clipboard.writeText(address);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error("Failed to copy address", err);
+ }
+ };
+
+ const isTestnet =
+ network?.toLowerCase().includes("testnet") ||
+ network?.toLowerCase().includes("test sdf network") ||
+ network?.toLowerCase().includes("september 2015");
+
+ if (!configured || !address) {
+ return (
+
+
- Pay-to: dynamic via x402
+ Pay-to:{" "}
+