diff --git a/agent/llm/skill-adapter.ts b/agent/llm/skill-adapter.ts index ebc0062..1d967d9 100644 --- a/agent/llm/skill-adapter.ts +++ b/agent/llm/skill-adapter.ts @@ -18,6 +18,7 @@ import { listGrants, upsertFinding } from "@agent/store"; import { pricingForSeverity } from "@agent/scanner"; import { isTargetInScope } from "@onchain/scope-grant"; import { notify } from "@agent/notify"; +import { triggerPayment, hasX402Configured } from "@onchain/x402"; const SEVERITY_MAP: Record = { CRITICAL: "critical", @@ -144,6 +145,37 @@ export async function runSkill( upsertFinding(finding); await notify(finding).catch(() => {}); + // Auto-trigger x402 payment — no user intervention required. + let paymentId: string | null = null; + let txHash: string | null = null; + let explorerUrl: string | null = null; + const amountUsdc = finding.billing.amountUsdc; + + if (hasX402Configured() && parseFloat(amountUsdc) > 0) { + try { + const payment = await triggerPayment({ + findingId: finding.findingId, + amountUsdc, + payerWallet: (process.env.AGENT_ADDRESS ?? "0x0000000000000000000000000000000000000000") as `0x${string}`, + receivingWallet: (process.env.GOATX402_RECEIVING_WALLET ?? "0x0000000000000000000000000000000000000000") as `0x${string}`, + description: `ShieldClaw: ${finding.title} at ${targetUrl}` + }); + paymentId = payment.paymentId; + txHash = payment.txHash; + explorerUrl = payment.explorerUrl; + upsertFinding({ + ...finding, + billing: { + ...finding.billing, + x402PaymentId: payment.paymentId, + status: payment.status === "succeeded" ? "paid" : "pending" + } + }); + } catch { + // Payment failure never blocks the finding report + } + } + return { ok: true, attackType: result.attackType, @@ -151,7 +183,15 @@ export async function runSkill( findingId: finding.findingId, severity: finding.severity, title: finding.title, - amountUsdc: finding.billing.amountUsdc + charged: amountUsdc, + paymentId, + txHash, + explorerUrl, + note: parseFloat(amountUsdc) === 0 + ? "Info-level finding — no charge." + : paymentId + ? `Charged ${amountUsdc} USDC automatically via x402.` + : "x402 merchant not configured — charge will fire when credentials are set." }; } diff --git a/agent/llm/tools.ts b/agent/llm/tools.ts index 8dc3e6f..40041ac 100644 --- a/agent/llm/tools.ts +++ b/agent/llm/tools.ts @@ -123,6 +123,12 @@ export const TOOL_SCHEMAS: ToolSchema[] = [ properties: { targetUrl: { type: "string" as const } }, required: ["targetUrl"] } + }, + { + name: "get_dashboard_metrics", + description: + "Return live aggregate metrics from the agent store: finding counts by severity/category/attack-vector, USDC paid vs pending, patch rate, active scope grants, and per-skill breakdowns. Use this to answer questions about posture, coverage, top threats, or billing summary.", + input_schema: { type: "object" as const, properties: {} } } ]; @@ -139,19 +145,19 @@ const HANDLERS: Record = { get_pricing: async () => ({ currency: "USDC", - model: "hybrid", - subscription: { - pricePerAssetMonthly: "50.00", - includes: "continuous monitoring + info/low/medium findings" - }, - urgencyPremium: { - critical: "20.00", - high: "5.00", - note: "Fires as an immediate x402 micropayment when the finding lands mid-period." + model: "pay-per-finding", + guarantee: "You pay nothing if no vulnerability is found. Zero scan fee. Zero monitoring fee.", + perFinding: { + info: "0.000", + low: "0.001", + medium: "0.002", + high: "0.003", + critical: "0.005" }, disputeWindowDays: 7, + slashing: "Disputed findings are not billed and slash agent on-chain reputation.", explanation: - "Two billing primitives via x402: a recurring monthly subscription per asset under watch, plus event-driven urgency premiums for critical/high findings. The hybrid is predictable for the customer and event-aligned for serious issues." + "Pure pay-per-finding via x402. If the agent finds nothing, you owe nothing. When a vulnerability is confirmed, an x402 USDC micropayment fires automatically — no invoice, no human approval. Pricing scales with severity so the cost always reflects the risk." }), list_scope_grants: async () => { @@ -337,7 +343,74 @@ const HANDLERS: Record = { port_scan: async ({ targetUrl }: { targetUrl: string }) => runSkill("port_scan", targetUrl), ssl_check: async ({ targetUrl }: { targetUrl: string }) => runSkill("ssl_check", targetUrl), credential_exposure: async ({ targetUrl }: { targetUrl: string }) => runSkill("credential_exposure", targetUrl), - wallet_auth_bypass: async ({ targetUrl }: { targetUrl: string }) => runSkill("wallet_auth_bypass", targetUrl) + wallet_auth_bypass: async ({ targetUrl }: { targetUrl: string }) => runSkill("wallet_auth_bypass", targetUrl), + + get_dashboard_metrics: async () => { + const findings = listFindings(); + const grants = listGrants(); + + const bySeverity = { critical: 0, high: 0, medium: 0, low: 0, info: 0 }; + const byCategory: Record = {}; + const byAttackType: Record = { sql_injection: 0, credential_exposure: 0, port_scan: 0, ssl_check: 0, wallet_auth: 0, other: 0 }; + let paidUsdc = 0; + let pendingUsdc = 0; + let paidCount = 0; + let disputedCount = 0; + + for (const f of findings) { + bySeverity[f.severity] = (bySeverity[f.severity] ?? 0) + 1; + byCategory[f.category] = (byCategory[f.category] ?? 0) + 1; + + const t = f.title.toLowerCase(); + if (t.includes("sql")) byAttackType.sql_injection++; + else if (t.includes("credential") || t.includes("secret") || t.includes("env") || t.includes("wallet")) byAttackType.credential_exposure++; + else if (t.includes("port")) byAttackType.port_scan++; + else if (t.includes("tls") || t.includes("ssl") || t.includes("https")) byAttackType.ssl_check++; + else if (t.includes("idor") || t.includes("auth bypass")) byAttackType.wallet_auth++; + else byAttackType.other++; + + const amt = Number(f.billing.amountUsdc); + if (f.billing.status === "paid") { paidUsdc += amt; paidCount++; } + else if (f.billing.status === "pending") pendingUsdc += amt; + else if (f.billing.status === "disputed") disputedCount++; + } + + const criticalCount = bySeverity.critical; + const totalFindings = findings.length; + const patchRate = totalFindings > 0 ? Math.round((paidCount / totalFindings) * 100) : 0; + const activeGrants = grants.filter(g => Date.now() / 1000 >= g.notBefore && Date.now() / 1000 <= g.notAfter).length; + + return { + summary: { + totalFindings, + bySeverity, + byCategory, + byAttackType, + activeGrants, + paidUsdc: paidUsdc.toFixed(2), + pendingUsdc: pendingUsdc.toFixed(2), + paidCount, + disputedCount, + patchRate: `${patchRate}%` + }, + posture: { + criticalUnpatched: criticalCount - paidCount > 0 ? criticalCount - paidCount : 0, + tlsIssues: byAttackType.ssl_check, + secretLeaks: byAttackType.credential_exposure, + sqlVulns: byAttackType.sql_injection, + openPorts: byAttackType.port_scan, + authBypasses: byAttackType.wallet_auth + }, + attackVectors: [ + { name: "SQL Injection", count: byAttackType.sql_injection, skill: "sql_injection_attack" }, + { name: "Credential Exposure", count: byAttackType.credential_exposure, skill: "credential_exposure" }, + { name: "Port Surface", count: byAttackType.port_scan, skill: "port_scan" }, + { name: "SSL / TLS", count: byAttackType.ssl_check, skill: "ssl_check" }, + { name: "Wallet Auth Bypass", count: byAttackType.wallet_auth, skill: "wallet_auth_bypass" } + ], + note: "Live data from the agent store. Run scans with sql_injection_attack / port_scan / ssl_check / credential_exposure / wallet_auth_bypass to populate." + }; + } }; export async function callTool(name: string, input: unknown): Promise { diff --git a/agent/orchestrator.ts b/agent/orchestrator.ts index b2be779..41c4de8 100644 --- a/agent/orchestrator.ts +++ b/agent/orchestrator.ts @@ -132,12 +132,12 @@ export async function runOrchestratedScan(targetUrl: string, chatId?: number | s ...(refused.length > 0 ? [`⛔ *Refused (no scope grant): ${refused.length}*`, ...refusalLines, ``] : []), found.length > 0 ? [ - `💰 *Production list: ${productionUsdc} USDC*`, - ` ⚡ *On-chain charge (demo-scaled): ${chargedUsdc} USDC*`, - paymentId ? ` x402 order: \`${paymentId}\`` : null, - txHash ? ` tx: [${txHash.slice(0, 16)}…](${explorerUrl})` : null + `💰 *Total charged: ${chargedUsdc} USDC (auto via x402)*`, + paymentId ? ` ✅ Payment fired automatically` : ` ⚠️ x402 credentials not set — charge skipped`, + paymentId ? ` Order: \`${paymentId}\`` : null, + txHash ? ` Tx: [${txHash.slice(0, 16)}…](${explorerUrl})` : null ].filter(Boolean).join("\n") - : `💰 No charge — no vulnerabilities found.`, + : `💰 *No charge — no vulnerabilities found.*`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, ].join("\n"); diff --git a/agent/scanner.ts b/agent/scanner.ts index 30b8b1a..c437989 100644 --- a/agent/scanner.ts +++ b/agent/scanner.ts @@ -132,13 +132,14 @@ export function nucleiResultToFinding( } export function pricingForSeverity(severity: Severity): string { + // Pay-per-finding: $0 if no vuln found, price scales with severity. return ( { - info: "0.10", - low: "0.25", - medium: "1.00", - high: "5.00", - critical: "20.00" + info: "0.000", + low: "0.001", + medium: "0.002", + high: "0.003", + critical: "0.005" } satisfies Record )[severity]; } diff --git a/components/activity-terminal.tsx b/components/activity-terminal.tsx index a3c4f78..5f73ca6 100644 --- a/components/activity-terminal.tsx +++ b/components/activity-terminal.tsx @@ -28,10 +28,12 @@ function ts() { } export function ActivityTerminal() { - const [lines, setLines] = useState(() => SEED.slice(0, 8).map((s) => `[${ts()}] ${s}`)); + const [lines, setLines] = useState([]); const ref = useRef(null); useEffect(() => { + setLines(SEED.slice(0, 8).map((s) => `[${ts()}] ${s}`)); + const id = setInterval(() => { setLines((curr) => { const next = SEED[Math.floor(Math.random() * SEED.length)]; diff --git a/components/billing-model.tsx b/components/billing-model.tsx index 7e4411d..fc24dc8 100644 --- a/components/billing-model.tsx +++ b/components/billing-model.tsx @@ -1,7 +1,15 @@ "use client"; import { AnimatedCounter } from "./animated-counter"; -import { Coins, Zap, AlertTriangle, Repeat } from "lucide-react"; +import { Coins, Zap, AlertTriangle, ShieldCheck } from "lucide-react"; + +const TIERS = [ + { label: "Info", price: "0.000", color: "text-slate-400", border: "border-slate-400/20", bg: "bg-slate-400/5" }, + { label: "Low", price: "0.001", color: "text-cyan-300", border: "border-cyan-400/25", bg: "bg-cyan-400/5" }, + { label: "Medium", price: "0.002", color: "text-amber-300", border: "border-amber-400/25", bg: "bg-amber-400/5" }, + { label: "High", price: "0.003", color: "text-orange-300", border: "border-orange-400/25",bg: "bg-orange-400/5"}, + { label: "Critical", price: "0.005", color: "text-red-300", border: "border-red-400/25", bg: "bg-red-400/5" }, +]; export function BillingModel() { return ( @@ -9,73 +17,48 @@ export function BillingModel() {
Billing Model
-
Two x402 primitives. One protocol.
+
Pay per finding. Nothing if we find nothing.
- +
-
total revenue · 30d
+
scan fee · always
-
- {/* Subscription column */} -
-
-
- -
-
-
Monthly subscription
-
recurring · per asset under watch
-
-
- $50 / mo · asset -
-
-
- - - - -
-
- Auto-settles via x402 at month boundary. Includes continuous monitoring + info/low/medium findings. + {/* Guarantee banner */} +
+ +
+
Zero-cost guarantee
+
+ If ShieldClaw finds no vulnerabilities, you pay nothing. No scan fee, no monitoring fee, no invoice. Ever.
+
- {/* Urgency column */} -
-
-
- -
-
-
Urgency premium
-
event-driven · critical & high only
-
-
- $20 / $5 + {/* Per-severity pricing */} +
+
Per-finding price · paid via x402 on confirmation
+
+ {TIERS.map((t) => ( +
+
{t.label}
+
+ {t.price === "0.000" ? free : `$${t.price}`} +
+
USDC
-
-
- - - - -
-
- Immediate x402 micropayment when a critical or high finding lands. Aligns incentives without breaking the subscription model. -
+ ))}
-
-