Skip to content

Commit 7909fd7

Browse files
committed
feat(dashboard): add AI prediction demo with Kalshi markets
- Add Predict page with Claude AI-powered market predictions - Integrate Kalshi API for real-time prediction market data - Create ValidationV1 attestations for prediction confidence checks - Add mainnet support with Dexter facilitator (x402.dexter.cash) - Improve error logging with request IDs and detailed catch blocks - Add SDK error handling layer (SatiError, transaction error parsing) - Add Badge and Collapsible UI components - Support multi-network x402 payments (devnet + mainnet)
1 parent 0a644a8 commit 7909fd7

22 files changed

Lines changed: 2163 additions & 1118 deletions

apps/dashboard/package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
"clean": "rm -rf dist"
1818
},
1919
"dependencies": {
20+
"@ai-sdk/anthropic": "^3.0.12",
2021
"@cascade-fyi/sati-sdk": "workspace:*",
22+
"@radix-ui/react-collapsible": "^1.1.12",
2123
"@radix-ui/react-dialog": "^1.1.15",
2224
"@radix-ui/react-dropdown-menu": "^2.1.16",
2325
"@radix-ui/react-label": "^2.1.8",
@@ -34,10 +36,11 @@
3436
"@solana/react-hooks": "^1.1.1",
3537
"@tailwindcss/vite": "^4.1.17",
3638
"@tanstack/react-query": "^5.90.11",
37-
"@x402/core": "^2.1.0",
38-
"@x402/fetch": "^2.1.0",
39-
"@x402/hono": "^2.1.0",
40-
"@x402/svm": "^2.1.0",
39+
"@x402/core": "^2.2.0",
40+
"@x402/fetch": "^2.2.0",
41+
"@x402/hono": "^2.2.0",
42+
"@x402/svm": "^2.2.0",
43+
"ai": "^6.0.33",
4144
"bs58": "^6.0.0",
4245
"class-variance-authority": "^0.7.1",
4346
"clsx": "^2.1.1",

apps/dashboard/src/env.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,26 @@ export type Network = "devnet" | "mainnet";
44
export const DEMO_AGENT_MINT_DEVNET = "J7b9Ks4TNBDN1nMoPfSYnD39oCBL2hVSp1FAoiwdHyoC";
55
export const DEMO_AGENT_MINT_MAINNET: string | undefined = undefined; // Not yet registered
66

7+
// Prediction agent (separate identity for Kalshi predictions)
8+
// Same mint address on both networks for consistency
9+
export const PREDICTION_AGENT_MINT_DEVNET = "2JoPSg3XkK77dyftS4L1A8GZvtKqiUrYtQtE6Xvagent";
10+
export const PREDICTION_AGENT_MINT_MAINNET = "2JoPSg3XkK77dyftS4L1A8GZvtKqiUrYtQtE6Xvagent";
11+
712
export const parse = (env: Record<string, unknown>) => {
813
const key = (env.VITE_HELIUS_API_KEY as string) ?? "";
914

1015
return {
1116
VITE_HELIUS_API_KEY: env.VITE_HELIUS_API_KEY as string | undefined,
1217
SATI_AGENT_SIGNER_KEY: env.SATI_AGENT_SIGNER_KEY as string | undefined,
18+
SATI_DEMO_VALIDATOR_SIGNER_KEY: env.SATI_DEMO_VALIDATOR_SIGNER_KEY as string | undefined,
19+
KALSHI_API_KEY_ID: env.KALSHI_API_KEY_ID as string | undefined,
20+
KALSHI_API_KEY_RSA_SECRET: env.KALSHI_API_KEY_RSA_SECRET as string | undefined,
21+
// Anthropic API key for AI predictions
22+
ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY as string | undefined,
1323
DEMO_AGENT_MINT_DEVNET,
1424
DEMO_AGENT_MINT_MAINNET,
25+
PREDICTION_AGENT_MINT_DEVNET,
26+
PREDICTION_AGENT_MINT_MAINNET,
1527
RPC_URLS: {
1628
devnet: {
1729
rpc: `https://devnet.helius-rpc.com/?api-key=${key}`,

apps/dashboard/src/react-app/components/GiveFeedbackDialog.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ import { Textarea } from "@/components/ui/textarea";
4040
import { Slider } from "@/components/ui/slider";
4141
import { Loader2, X } from "lucide-react";
4242

43-
// Get deployed FeedbackPublic schema address (SingleSigner mode - no counterparty sig needed)
44-
const deployedConfig = loadDeployedConfig(getNetwork());
45-
const FEEDBACK_SCHEMA_ADDRESS = deployedConfig?.schemas?.feedbackPublic as Address | undefined;
46-
4743
interface GiveFeedbackDialogProps {
4844
/** Agent mint address */
4945
agentMint: Address;
@@ -65,7 +61,7 @@ interface SubmitFeedbackRequest {
6561
outcome: number;
6662
counterparty: string;
6763
agentSignature: string; // User's SIWS signature (hex)
68-
agentAddress: string; // User's wallet address
64+
agentOwner: string; // User's wallet address
6965
counterpartyMessage: string; // SIWS message bytes (hex) - triggers server-paid mode
7066
content?: string;
7167
contentType?: number;
@@ -137,7 +133,12 @@ export function GiveFeedbackDialog({ agentMint, agentName, children, onSuccess }
137133
const feedbackMutation = useMutation({
138134
mutationFn: async (selectedOutcome: Outcome) => {
139135
if (!session) throw new Error("Wallet not connected");
140-
if (!FEEDBACK_SCHEMA_ADDRESS) throw new Error("Feedback schema not configured");
136+
137+
// Load schema for current network (not cached at module level)
138+
const network = getNetwork();
139+
const deployedConfig = loadDeployedConfig(network);
140+
const feedbackSchemaAddress = deployedConfig?.schemas?.feedbackPublic as Address | undefined;
141+
if (!feedbackSchemaAddress) throw new Error("Feedback schema not configured");
141142

142143
const toastId = toast.loading("Preparing feedback...");
143144

@@ -190,15 +191,15 @@ export function GiveFeedbackDialog({ agentMint, agentName, children, onSuccess }
190191
toast.loading("Submitting feedback...", { id: toastId });
191192

192193
const request: SubmitFeedbackRequest = {
193-
network: getNetwork(),
194-
sasSchema: FEEDBACK_SCHEMA_ADDRESS,
194+
network,
195+
sasSchema: feedbackSchemaAddress,
195196
taskRef: bytesToHex(taskRef),
196197
agentMint: agentMint,
197198
dataHash: bytesToHex(dataHash),
198199
outcome: selectedOutcome,
199200
counterparty: session.account.address,
200201
agentSignature: bytesToHex(signature), // User's SIWS signature
201-
agentAddress: session.account.address, // User's wallet address
202+
agentOwner: session.account.address, // User's wallet address
202203
counterpartyMessage: bytesToHex(new Uint8Array(siwsMessage.messageBytes)), // Triggers server-paid mode
203204
...(contentJson && { content: contentJson, contentType: 1 }),
204205
};

apps/dashboard/src/react-app/components/Header.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,20 @@ export function Header() {
4545
<NavLink to="/" end className={({ isActive }) => getNavLinkClasses(isActive)}>
4646
Explore
4747
</NavLink>
48+
<NavLink to="/predict" className={({ isActive }) => getNavLinkClasses(isActive)}>
49+
Predict
50+
</NavLink>
4851
<NavLink to="/profile" className={({ isActive }) => getNavLinkClasses(isActive)}>
4952
My Profile
5053
</NavLink>
54+
<a
55+
href="https://cascade-protocol.github.io/sati/"
56+
target="_blank"
57+
rel="noopener noreferrer"
58+
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
59+
>
60+
Docs
61+
</a>
5162
</nav>
5263
</div>
5364

@@ -90,9 +101,20 @@ export function Header() {
90101
<NavLink to="/" end className={({ isActive }) => getMobileNavLinkClasses(isActive)}>
91102
Explore
92103
</NavLink>
104+
<NavLink to="/predict" className={({ isActive }) => getMobileNavLinkClasses(isActive)}>
105+
Predict
106+
</NavLink>
93107
<NavLink to="/profile" className={({ isActive }) => getMobileNavLinkClasses(isActive)}>
94108
My Profile
95109
</NavLink>
110+
<a
111+
href="https://cascade-protocol.github.io/sati/"
112+
target="_blank"
113+
rel="noopener noreferrer"
114+
className="block rounded-md px-3 py-2 text-base text-muted-foreground transition-colors hover:text-foreground"
115+
>
116+
Docs
117+
</a>
96118
</nav>
97119
<Separator />
98120
<div className="flex items-center gap-4">

apps/dashboard/src/react-app/components/VerifiedFeedbackDialog.tsx

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* VerifiedFeedbackDialog - DualSignature feedback demonstrating blind feedback model
33
*
44
* 2-Step Flow:
5-
* 1. INTERACT: User pays $0.01 USDC → Agent signs BLINDLY (doesn't know outcome)
5+
* 1. INTERACT: User pays $0.001 USDC → Agent signs BLINDLY (doesn't know outcome)
66
* 2. RATE & SUBMIT: User chooses outcome → Signs SIWS → Server submits transaction
77
*
88
* This demonstrates SATI's core innovation: unforgeable feedback where the agent
@@ -21,8 +21,9 @@ import {
2121
buildCounterpartyMessage,
2222
serializeFeedback,
2323
type FeedbackData,
24+
handleTransactionError,
2425
} from "@cascade-fyi/sati-sdk";
25-
import { getNetwork, getSolscanUrl, getRpcUrl } from "@/lib/network";
26+
import { getChain, getNetwork, getSolscanUrl, getRpcUrl } from "@/lib/network";
2627
import { createPaymentFetch } from "@/lib/x402";
2728

2829
import { Button } from "@/components/ui/button";
@@ -43,11 +44,24 @@ import { Loader2, CheckCircle2 } from "lucide-react";
4344
const deployedConfig = loadDeployedConfig(getNetwork());
4445
const FEEDBACK_SCHEMA_ADDRESS = deployedConfig?.schemas?.feedback as Address | undefined;
4546

47+
// Pre-filled agent signature data (for skipping Step 1)
48+
interface PrefilledSignatureData {
49+
taskRef: string; // hex
50+
dataHash: string; // hex
51+
agentSignature: string; // hex
52+
agentOwner: string;
53+
}
54+
4655
interface VerifiedFeedbackDialogProps {
4756
agentMint: Address;
4857
agentName: string;
49-
children: ReactNode;
58+
children?: ReactNode;
5059
onSuccess?: () => void;
60+
// Controlled mode props
61+
open?: boolean;
62+
onOpenChange?: (open: boolean) => void;
63+
// Pre-filled data to skip Step 1 (agent already signed)
64+
prefilledData?: PrefilledSignatureData;
5165
}
5266

5367
// API types
@@ -61,7 +75,7 @@ interface EchoRequest {
6175
interface EchoResponse {
6276
success: boolean;
6377
data?: {
64-
agentAddress: string;
78+
agentOwner: string;
6579
interactionHash: string;
6680
signature: string;
6781
signatureBase58: string;
@@ -78,7 +92,7 @@ interface SubmitFeedbackRequest {
7892
outcome: number;
7993
counterparty: string;
8094
agentSignature: string;
81-
agentAddress: string;
95+
agentOwner: string;
8296
counterpartySignature: string;
8397
counterpartyMessage: string;
8498
content?: string;
@@ -117,18 +131,40 @@ interface AgentSignatureData {
117131
taskRef: string; // hex
118132
dataHash: string; // hex
119133
signature: string; // hex
120-
agentAddress: string;
134+
agentOwner: string;
121135
}
122136

123137
type Step = "interact" | "rate";
124138

125-
export function VerifiedFeedbackDialog({ agentMint, agentName, children, onSuccess }: VerifiedFeedbackDialogProps) {
126-
const [open, setOpen] = useState(false);
139+
export function VerifiedFeedbackDialog({
140+
agentMint,
141+
agentName,
142+
children,
143+
onSuccess,
144+
open: controlledOpen,
145+
onOpenChange: controlledOnOpenChange,
146+
prefilledData,
147+
}: VerifiedFeedbackDialogProps) {
148+
// Support controlled mode
149+
const [internalOpen, setInternalOpen] = useState(false);
150+
const isControlled = controlledOpen !== undefined;
151+
const open = isControlled ? controlledOpen : internalOpen;
152+
153+
// Initialize step based on prefilledData
154+
const initialStep: Step = prefilledData ? "rate" : "interact";
155+
const initialAgentData: AgentSignatureData | null = prefilledData
156+
? {
157+
taskRef: prefilledData.taskRef,
158+
dataHash: prefilledData.dataHash,
159+
signature: prefilledData.agentSignature,
160+
agentOwner: prefilledData.agentOwner,
161+
}
162+
: null;
127163

128164
// Step state
129-
const [step, setStep] = useState<Step>("interact");
165+
const [step, setStep] = useState<Step>(initialStep);
130166
const [isInteracting, setIsInteracting] = useState(false);
131-
const [agentSignatureData, setAgentSignatureData] = useState<AgentSignatureData | null>(null);
167+
const [agentSignatureData, setAgentSignatureData] = useState<AgentSignatureData | null>(initialAgentData);
132168

133169
// Rating state (Step 2)
134170
const [outcome, setOutcome] = useState<string>("2"); // Default to Positive
@@ -139,10 +175,15 @@ export function VerifiedFeedbackDialog({ agentMint, agentName, children, onSucce
139175

140176
// Reset state when dialog closes
141177
const handleOpenChange = (isOpen: boolean) => {
142-
setOpen(isOpen);
178+
if (isControlled) {
179+
controlledOnOpenChange?.(isOpen);
180+
} else {
181+
setInternalOpen(isOpen);
182+
}
143183
if (!isOpen) {
144-
setStep("interact");
145-
setAgentSignatureData(null);
184+
// Reset to initial state (which depends on prefilledData)
185+
setStep(initialStep);
186+
setAgentSignatureData(initialAgentData);
146187
setOutcome("2");
147188
setMessage("");
148189
setIsInteracting(false);
@@ -177,9 +218,9 @@ export function VerifiedFeedbackDialog({ agentMint, agentName, children, onSucce
177218
const taskRef = bytesToHex(new Uint8Array(taskRefDigest));
178219
const dataHash = bytesToHex(new Uint8Array(dataHashDigest));
179220

180-
// Create x402 payment-enabled fetch
181-
toast.loading("Approve payment ($0.01 USDC)...", { id: toastId });
182-
const rpcUrl = getRpcUrl(getNetwork());
221+
// Create x402 payment-enabled fetch using user's network
222+
toast.loading("Approve payment ($0.001 USDC)...", { id: toastId });
223+
const rpcUrl = getRpcUrl(getChain());
183224
const paymentFetch = createPaymentFetch(session, rpcUrl);
184225

185226
// Call /api/echo with x402 payment - agent signs WITHOUT knowing outcome
@@ -211,7 +252,7 @@ export function VerifiedFeedbackDialog({ agentMint, agentName, children, onSucce
211252
taskRef,
212253
dataHash,
213254
signature: echoResult.data.signature,
214-
agentAddress: echoResult.data.agentAddress,
255+
agentOwner: echoResult.data.agentOwner,
215256
});
216257

217258
toast.success("Agent signed! Now choose your rating.", { id: toastId });
@@ -289,7 +330,7 @@ export function VerifiedFeedbackDialog({ agentMint, agentName, children, onSucce
289330
outcome: selectedOutcome,
290331
counterparty: session.account.address,
291332
agentSignature: agentSignatureData.signature,
292-
agentAddress: agentSignatureData.agentAddress,
333+
agentOwner: agentSignatureData.agentOwner,
293334
counterpartySignature: bytesToHex(counterpartySig),
294335
counterpartyMessage: bytesToHex(new Uint8Array(siwsMessage.messageBytes)),
295336
...(contentJson && { content: contentJson, contentType: 1 }),
@@ -329,8 +370,18 @@ export function VerifiedFeedbackDialog({ agentMint, agentName, children, onSucce
329370
};
330371
} catch (error) {
331372
toast.dismiss(toastId);
332-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
333-
toast.error(`Failed: ${errorMessage}`);
373+
374+
// Use SDK error handler for consistent error messages
375+
const result = handleTransactionError(error);
376+
377+
if (result.reason === "duplicate_attestation") {
378+
toast.error(result.message, {
379+
description: "Each prediction can only receive one feedback per user.",
380+
duration: 6000,
381+
});
382+
} else {
383+
toast.error(result.message);
384+
}
334385
throw error;
335386
}
336387
},
@@ -358,7 +409,7 @@ export function VerifiedFeedbackDialog({ agentMint, agentName, children, onSucce
358409

359410
return (
360411
<Dialog open={open} onOpenChange={handleOpenChange}>
361-
<DialogTrigger asChild>{children}</DialogTrigger>
412+
{children && <DialogTrigger asChild>{children}</DialogTrigger>}
362413
<DialogContent className="sm:max-w-md">
363414
<DialogHeader>
364415
<DialogTitle>Verified Feedback</DialogTitle>
@@ -396,9 +447,9 @@ export function VerifiedFeedbackDialog({ agentMint, agentName, children, onSucce
396447
{step === "interact" && (
397448
<div className="space-y-4 py-4">
398449
<p className="text-sm text-muted-foreground">
399-
Pay <span className="font-medium text-foreground">$0.01 USDC</span> to request a signature from the agent.
400-
The agent signs <span className="font-medium text-foreground">without knowing</span> what feedback you'll
401-
give.
450+
Pay <span className="font-medium text-foreground">$0.001 USDC</span> to request a signature from the
451+
agent. The agent signs <span className="font-medium text-foreground">without knowing</span> what feedback
452+
you'll give.
402453
</p>
403454

404455
<div className="p-3 rounded-lg bg-muted/50 border text-sm">
@@ -421,7 +472,7 @@ export function VerifiedFeedbackDialog({ agentMint, agentName, children, onSucce
421472
Processing...
422473
</>
423474
) : (
424-
"Pay & Get Signature ($0.01)"
475+
"Pay & Get Signature ($0.001)"
425476
)}
426477
</Button>
427478

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from "react";
2+
import { cva, type VariantProps } from "class-variance-authority";
3+
4+
import { cn } from "@/lib/utils";
5+
6+
const badgeVariants = cva(
7+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8+
{
9+
variants: {
10+
variant: {
11+
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12+
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
13+
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
14+
outline: "text-foreground",
15+
},
16+
},
17+
defaultVariants: {
18+
variant: "default",
19+
},
20+
},
21+
);
22+
23+
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
24+
25+
function Badge({ className, variant, ...props }: BadgeProps) {
26+
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
27+
}
28+
29+
export { Badge, badgeVariants };

0 commit comments

Comments
 (0)