Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/agent-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion apps/agent-client/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
4 changes: 2 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion apps/api/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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) */
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/lib/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import pino from "pino";
import { pino } from "pino";

export const logger = pino({
level: process.env.NODE_ENV === "development" ? "debug" : "info",
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/lib/payment-evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
};
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/lib/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/lib/storage/memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/");
});
});

Expand Down
8 changes: 4 additions & 4 deletions apps/api/src/lib/x402.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
23 changes: 23 additions & 0 deletions apps/api/src/routes/public.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
100 changes: 95 additions & 5 deletions apps/web/src/pages/ControlDeckPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<span className="pay-to-warning-badge" title="No payout address configured!">
<AlertTriangle size={13} style={{ display: "inline-block", marginRight: "4px", verticalAlign: "-2px" }} />
Missing pay-to address
</span>
);
}

return (
<span className="pay-to-info-wrapper">
<strong className="pay-to-address-text" title={address}>
{address.slice(0, 6)}...{address.slice(-6)}
</strong>
{isTestnet && <span className="pay-to-network-badge">Testnet</span>}
<button
onClick={handleCopy}
className="pay-to-copy-button"
title="Copy full public address"
type="button"
>
{copied ? (
<Check size={11} className="copy-icon success" style={{ color: "#37e0af" }} />
) : (
<Copy size={11} className="copy-icon" />
)}
</button>
</span>
);
}

export default function ControlDeckPage() {
const [mode, setMode] = useState<QueryMode>("search");
const [paymentMode, setPaymentMode] = useState<"wallet" | "sponsored">("wallet");
Expand All @@ -69,6 +130,7 @@ export default function ControlDeckPage() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [sponsorshipEnabled, setSponsorshipEnabled] = useState(false);
const [healthDiagnostics, setHealthDiagnostics] = useState<{ network?: string; payToConfigured?: boolean; payToAddress?: string } | null>(null);
const [preview, setPreview] = useState<SponsorshipPreview | null>(null);
const [previewError, setPreviewError] = useState<string | null>(null);
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
Expand Down Expand Up @@ -127,13 +189,28 @@ export default function ControlDeckPage() {

useEffect(() => {
async function bootstrap() {
const [providersResponse, sponsorshipActive] = await Promise.all([
const [providersResponse, healthResponse] = await Promise.all([
fetchJson<{ providers: ProviderDefinition[] }>(`${API_BASE_URL}/api/providers`),
fetchSponsorshipEnabled(API_BASE_URL)
fetchJson<{
sponsorshipEnabled?: boolean;
network?: string;
diagnostics?: {
network: string;
demoMode: boolean;
payToConfigured: boolean;
payToAddress?: string;
sponsorshipEnabled: boolean;
};
}>(`${API_BASE_URL}/health`)
]);
setProviders(providersResponse.providers);
setSelectedProvider(modeDefaultProvider.search);
setSponsorshipEnabled(sponsorshipActive);
setSponsorshipEnabled(healthResponse.sponsorshipEnabled === true);
setHealthDiagnostics({
network: healthResponse.network ?? healthResponse.diagnostics?.network,
payToConfigured: healthResponse.diagnostics?.payToConfigured ?? false,
payToAddress: healthResponse.diagnostics?.payToAddress
});
await refreshMetrics();
}

Expand Down Expand Up @@ -455,7 +532,12 @@ export default function ControlDeckPage() {
units)
</p>
<p className="action-label">
Pay-to: <strong>dynamic via x402</strong>
Pay-to:{" "}
<PayToAddressDisplay
address={healthDiagnostics?.payToAddress}
network={healthDiagnostics?.network}
configured={healthDiagnostics?.payToConfigured}
/>
</p>
</div>
<button
Expand Down Expand Up @@ -518,6 +600,14 @@ export default function ControlDeckPage() {
<div className="trace-box">
<p>payment-response: {result.payment.paymentResponseHeader ?? "<none>"}</p>
<p>network: {result.payment.network}</p>
<p style={{ display: "flex", alignItems: "center", gap: "4px" }}>
pay-to:{" "}
<PayToAddressDisplay
address={result.payment.evidence?.payTo}
network={result.payment.network}
configured={!!result.payment.evidence?.payTo && result.payment.evidence.payTo !== "not_available"}
/>
</p>
{result.payment.evidence?.proofLinks && (
<div className="proof-links">
<p>
Expand Down
69 changes: 69 additions & 0 deletions apps/web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2229,3 +2229,72 @@ body {
height: 52px;
}
}

/* Pay-to address display & copy styles */
.pay-to-warning-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: rgba(255, 178, 54, 0.12);
border: 1px solid rgba(255, 178, 54, 0.35);
color: #ffca65;
padding: 0.25rem 0.5rem;
border-radius: 6px;
font-size: 0.72rem;
font-weight: 600;
}

.pay-to-info-wrapper {
display: inline-flex;
align-items: center;
gap: 0.4rem;
vertical-align: middle;
}

.pay-to-address-text {
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
color: #edf3ff;
font-weight: 600;
}

.pay-to-network-badge {
display: inline-block;
background: rgba(90, 220, 255, 0.15);
border: 1px solid rgba(90, 220, 255, 0.35);
color: #8fe6ff;
padding: 0.05rem 0.35rem;
border-radius: 4px;
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
}

.pay-to-copy-button {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(130, 160, 196, 0.24);
border-radius: 4px;
color: #8ea1bb;
cursor: pointer;
padding: 0.2rem;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 150ms ease;
line-height: 1;
}

.pay-to-copy-button:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(130, 160, 196, 0.45);
color: #eaf3ff;
}

.pay-to-copy-button:active {
transform: scale(0.95);
}

.copy-icon {
display: inline-block;
vertical-align: middle;
}