Skip to content
Merged
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
42 changes: 41 additions & 1 deletion agent/llm/skill-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AttackResult["severity"], Severity> = {
CRITICAL: "critical",
Expand Down Expand Up @@ -144,14 +145,53 @@ 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,
vulnerable: true,
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."
};
}

Expand Down
95 changes: 84 additions & 11 deletions agent/llm/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {} }
}
];

Expand All @@ -139,19 +145,19 @@ const HANDLERS: Record<string, ToolHandler> = {

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 () => {
Expand Down Expand Up @@ -337,7 +343,74 @@ const HANDLERS: Record<string, ToolHandler> = {
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<string, number> = {};
const byAttackType: Record<string, number> = { 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<string> {
Expand Down
10 changes: 5 additions & 5 deletions agent/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
11 changes: 6 additions & 5 deletions agent/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, string>
)[severity];
}
4 changes: 3 additions & 1 deletion components/activity-terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ function ts() {
}

export function ActivityTerminal() {
const [lines, setLines] = useState<string[]>(() => SEED.slice(0, 8).map((s) => `[${ts()}] ${s}`));
const [lines, setLines] = useState<string[]>([]);
const ref = useRef<HTMLDivElement>(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)];
Expand Down
91 changes: 37 additions & 54 deletions components/billing-model.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,64 @@
"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 (
<section className="rounded-xl border border-border/80 bg-panel/60 p-5">
<div className="mb-5 flex items-center justify-between">
<div>
<div className="text-[10px] uppercase tracking-[0.25em] text-cyan-300">Billing Model</div>
<div className="mt-1 text-lg font-semibold">Two x402 primitives. One protocol.</div>
<div className="mt-1 text-lg font-semibold">Pay per finding. Nothing if we find nothing.</div>
</div>
<div className="text-right">
<div className="font-mono text-2xl text-glow text-emerald-300">
<AnimatedCounter target={29_730.5} prefix="$" decimals={2} liveIncrement={0.3} />
<AnimatedCounter target={0} prefix="$" decimals={2} />
</div>
<div className="text-[10px] uppercase tracking-widest text-muted">total revenue · 30d</div>
<div className="text-[10px] uppercase tracking-widest text-muted">scan fee · always</div>
</div>
</div>

<div className="grid gap-4 md:grid-cols-2">
{/* Subscription column */}
<div className="rounded-lg border border-emerald-400/25 bg-emerald-400/5 p-4">
<div className="mb-3 flex items-center gap-2">
<div className="grid h-8 w-8 place-items-center rounded-md bg-emerald-400/15 text-emerald-300">
<Repeat size={14} />
</div>
<div>
<div className="text-xs font-medium text-emerald-200">Monthly subscription</div>
<div className="text-[10px] text-emerald-300/70">recurring · per asset under watch</div>
</div>
<div className="ml-auto rounded-full border border-emerald-400/30 bg-bg/40 px-2 py-0.5 font-mono text-[10px] text-emerald-300">
$50 / mo · asset
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<Stat label="Assets watched" value={468} accent="text-emerald-300" />
<Stat label="Active customers" value={47} accent="text-emerald-300" />
<Stat label="MRR" value={23_400} prefix="$" accent="text-emerald-300" />
<Stat label="Renewal rate" value={94} suffix="%" decimals={0} accent="text-emerald-300" />
</div>
<div className="mt-3 rounded-md bg-bg/40 px-3 py-2 text-[11px] leading-relaxed text-emerald-200/80">
Auto-settles via x402 at month boundary. Includes continuous monitoring + info/low/medium findings.
{/* Guarantee banner */}
<div className="mb-4 flex items-center gap-3 rounded-lg border border-emerald-400/30 bg-emerald-400/5 px-4 py-3">
<ShieldCheck size={18} className="shrink-0 text-emerald-300" />
<div>
<div className="text-sm font-medium text-emerald-200">Zero-cost guarantee</div>
<div className="text-[11px] text-emerald-300/70">
If ShieldClaw finds no vulnerabilities, you pay nothing. No scan fee, no monitoring fee, no invoice. Ever.
</div>
</div>
</div>

{/* Urgency column */}
<div className="rounded-lg border border-red-400/25 bg-red-400/5 p-4">
<div className="mb-3 flex items-center gap-2">
<div className="grid h-8 w-8 place-items-center rounded-md bg-red-400/15 text-red-300">
<Zap size={14} />
</div>
<div>
<div className="text-xs font-medium text-red-200">Urgency premium</div>
<div className="text-[10px] text-red-300/70">event-driven · critical &amp; high only</div>
</div>
<div className="ml-auto rounded-full border border-red-400/30 bg-bg/40 px-2 py-0.5 font-mono text-[10px] text-red-300">
$20 / $5
{/* Per-severity pricing */}
<div className="mb-4">
<div className="mb-2 text-[10px] uppercase tracking-[0.2em] text-muted">Per-finding price · paid via x402 on confirmation</div>
<div className="grid grid-cols-5 gap-2">
{TIERS.map((t) => (
<div key={t.label} className={`rounded-lg border ${t.border} ${t.bg} p-3 text-center`}>
<div className={`text-[10px] uppercase tracking-widest ${t.color}`}>{t.label}</div>
<div className={`mt-1 font-mono text-xl font-semibold ${t.color}`}>
{t.price === "0.000" ? <span className="text-base text-muted">free</span> : `$${t.price}`}
</div>
<div className="mt-0.5 text-[9px] text-muted">USDC</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<Stat label="Critical fires" value={117} accent="text-red-300" />
<Stat label="High fires" value={384} accent="text-orange-300" />
<Stat label="Premium rev" value={4_260} prefix="$" accent="text-red-300" />
<Stat label="Avg time-to-fire" value={2.3} suffix="s" decimals={1} accent="text-red-300" />
</div>
<div className="mt-3 rounded-md bg-bg/40 px-3 py-2 text-[11px] leading-relaxed text-red-200/80">
Immediate x402 micropayment when a critical or high finding lands. Aligns incentives without breaking the subscription model.
</div>
))}
</div>
</div>

<div className="mt-4 grid grid-cols-2 gap-3 rounded-md border border-cyan-400/15 bg-cyan-400/5 px-4 py-3 md:grid-cols-4">
<Footer label="Combined per-customer" value="$632 / mo · avg" icon={<Coins size={12} className="text-cyan-300" />} />
<Footer label="Dispute rate" value="2.4% · 7d window" icon={<AlertTriangle size={12} className="text-cyan-300" />} />
<Footer label="On-chain receipts" value="501 · 30d" icon={<Zap size={12} className="text-cyan-300" />} />
<Footer label="Reputation stake" value="Active · 0 slashings" icon={<Repeat size={12} className="text-cyan-300" />} />
<div className="grid grid-cols-2 gap-3 rounded-md border border-cyan-400/15 bg-cyan-400/5 px-4 py-3 md:grid-cols-4">
<Footer label="Payment method" value="x402 · instant USDC" icon={<Zap size={12} className="text-cyan-300" />} />
<Footer label="Dispute window" value="7 days · on-chain" icon={<AlertTriangle size={12} className="text-cyan-300" />} />
<Footer label="False positive" value="0 charge · rep slashed" icon={<ShieldCheck size={12} className="text-cyan-300" />} />
<Footer label="Scan fee" value="$0.00 · always" icon={<Coins size={12} className="text-cyan-300" />} />
</div>
</section>
);
Expand Down
26 changes: 14 additions & 12 deletions components/geo-attack-feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,22 @@ function rand(min: number, max: number) {
}

export function GeoAttackFeed() {
const [attacks, setAttacks] = useState<Attack[]>(() =>
Array.from({ length: 8 }, (_, i) => {
const o = ORIGINS[i % ORIGINS.length];
return {
origin: o.o,
region: `${o.flag} ${o.r}`,
vector: VECTORS[Math.floor(rand(0, VECTORS.length))],
blocked: Math.random() > 0.15,
time: nowStr()
};
})
);
const [attacks, setAttacks] = useState<Attack[]>([]);

useEffect(() => {
setAttacks(
Array.from({ length: 8 }, (_, i) => {
const o = ORIGINS[i % ORIGINS.length];
return {
origin: o.o,
region: `${o.flag} ${o.r}`,
vector: VECTORS[Math.floor(rand(0, VECTORS.length))],
blocked: Math.random() > 0.15,
time: nowStr()
};
})
);

const id = setInterval(() => {
setAttacks((curr) => {
const o = ORIGINS[Math.floor(Math.random() * ORIGINS.length)];
Expand Down
Loading