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
15 changes: 15 additions & 0 deletions electron/ipc/said.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ipcMain } from 'electron'
import { ipcHandler } from '../services/IpcHandlerFactory'
import * as SaidProtocolService from '../services/SaidProtocolService'

export function registerSaidHandlers() {
ipcMain.handle('said:get-identity', ipcHandler(async (_event, wallet: string) => {
if (typeof wallet !== 'string' || !wallet) throw new Error('Invalid wallet address')
return SaidProtocolService.getIdentity(wallet.trim())
}))

ipcMain.handle('said:get-trust', ipcHandler(async (_event, wallet: string) => {
if (typeof wallet !== 'string' || !wallet) throw new Error('Invalid wallet address')
return SaidProtocolService.getTrustScore(wallet.trim())
}))
}
2 changes: 2 additions & 0 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { registerLaunchHandlers } from '../ipc/launch'
import { registerDashboardHandlers } from '../ipc/dashboard'
import { registerForensicsHandlers } from '../ipc/forensics'
import { registerRegistryHandlers } from '../ipc/registry'
import { registerSaidHandlers } from '../ipc/said'
import { registerColosseumHandlers } from '../ipc/colosseum'
import { registerIdleHandlers } from '../ipc/idle'
import { registerMeterflowHandlers } from '../ipc/meterflow'
Expand Down Expand Up @@ -325,6 +326,7 @@ function registerAllIpc() {
registerDashboardHandlers()
registerForensicsHandlers()
registerRegistryHandlers()
registerSaidHandlers()
registerColosseumHandlers()
registerIdleHandlers()
registerMeterflowHandlers()
Expand Down
5 changes: 5 additions & 0 deletions electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,11 @@ contextBridge.exposeInMainWorld('daemon', {
importToken: (mint: string, walletId: string) => ipcRenderer.invoke('dashboard:import-token', mint, walletId),
},

said: {
getIdentity: (wallet: string) => ipcRenderer.invoke('said:get-identity', wallet),
getTrust: (wallet: string) => ipcRenderer.invoke('said:get-trust', wallet),
},

forensics: {
scan: (input: object) => ipcRenderer.invoke('forensics:scan', input),
expand: (input: object) => ipcRenderer.invoke('forensics:expand', input),
Expand Down
148 changes: 148 additions & 0 deletions electron/services/SaidProtocolService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { PublicKey } from '@solana/web3.js'
import type { SaidIdentity, SaidTrustScore } from '../shared/types'

// SAID Protocol — on-chain identity, verification, and trust for AI agents on Solana.
// DAEMON only consumes the public read API here. Registration/verification/staking sign
// transactions and must be added behind SignerGuardService + a transaction preview.
const SAID_API_BASE = process.env.DAEMON_SAID_API_URL || 'https://api.saidprotocol.com'
const REQUEST_TIMEOUT_MS = 10_000

// Program IDs are docs-sourced. Verify on a Solana explorer before wiring any signing flow.
export const SAID_PROGRAM_ID = {
mainnet: '5dpw6KEQPn248pnkkaYyWfHwu2nfb3LUMbTucb6LaA8G',
devnet: 'ESPreFucjVwtDmZbhtL3JLJ9VxCethNEYtosMQhkcurv',
} as const

function isValidWallet(address: string): boolean {
try {
new PublicKey(address)
return address.length >= 32 && address.length <= 44
} catch {
return false
}
}

async function fetchJson<T>(path: string): Promise<{ status: number; body: T | null }> {
const url = `${SAID_API_BASE}${path}`
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
try {
const response = await fetch(url, {
method: 'GET',
headers: { Accept: 'application/json' },
signal: controller.signal,
})
if (response.status === 404) return { status: 404, body: null }
if (!response.ok) {
const text = await response.text().catch(() => '')
throw new Error(`SAID request failed (${response.status}): ${text.slice(0, 180)}`)
}
const body = (await response.json()) as T
return { status: response.status, body }
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
throw new Error('SAID request timed out')
}
throw err
} finally {
clearTimeout(timer)
}
}

// The numeric trust score lives on /api/agents/:wallet under trustScore.score.
// /api/trust/:wallet only returns a coarse tier + registered/verified booleans, and
// /api/agents/:wallet already carries verification, reputation, and the economic
// breakdown — so a single agents read is the source of truth for both calls.
interface RawTrustScore {
score?: number
economic?: number
badges?: string[]
}

interface RawAgent {
wallet?: string
pda?: string
owner?: string
name?: string
description?: string
isVerified?: boolean
image?: string
twitter?: string
website?: string
serviceTypes?: string[]
skills?: string[]
reputationScore?: number
feedbackCount?: number
trustScore?: number | RawTrustScore
passportMint?: string
}

function readScore(value: number | RawTrustScore | undefined): number | null {
if (typeof value === 'number') return value
if (value && typeof value.score === 'number') return value.score
return null
}

function isStaked(trust: number | RawTrustScore | undefined): boolean {
if (typeof trust !== 'object' || !trust) return false
if (Array.isArray(trust.badges) && trust.badges.includes('staked')) return true
return typeof trust.economic === 'number' && trust.economic > 0
}

export async function getTrustScore(wallet: string): Promise<SaidTrustScore> {
if (!isValidWallet(wallet)) throw new Error('Invalid Solana wallet address')
const { status, body } = await fetchJson<RawAgent>(`/api/agents/${wallet}`)
if (status === 404 || !body || !body.wallet) {
return { score: 0, verified: false, staked: false, reputation: null }
}
const score = readScore(body.trustScore) ?? 0
return {
score: Math.max(0, Math.min(100, Math.round(score))),
verified: Boolean(body.isVerified),
staked: isStaked(body.trustScore),
reputation: body.reputationScore ?? null,
}
}

export async function getIdentity(wallet: string): Promise<SaidIdentity> {
if (!isValidWallet(wallet)) throw new Error('Invalid Solana wallet address')
const { status, body } = await fetchJson<RawAgent>(`/api/agents/${wallet}`)
if (status === 404 || !body || !body.wallet) {
return {
wallet,
pda: null,
owner: null,
name: null,
description: null,
isVerified: false,
image: null,
twitter: null,
website: null,
serviceTypes: [],
skills: [],
reputationScore: null,
feedbackCount: null,
trustScore: null,
passportMint: null,
registered: false,
}
}
return {
wallet: body.wallet,
pda: body.pda ?? null,
owner: body.owner ?? null,
name: body.name ?? null,
description: body.description ?? null,
isVerified: Boolean(body.isVerified),
image: body.image ?? null,
twitter: body.twitter ?? null,
website: body.website ?? null,
serviceTypes: Array.isArray(body.serviceTypes) ? body.serviceTypes : [],
skills: Array.isArray(body.skills) ? body.skills : [],
reputationScore: body.reputationScore ?? null,
feedbackCount: body.feedbackCount ?? null,
trustScore: readScore(body.trustScore),
passportMint: body.passportMint ?? null,
registered: true,
}
}
26 changes: 26 additions & 0 deletions electron/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,32 @@ export interface RicoMapsEmbedStatus {
error: string | null
}

export interface SaidTrustScore {
score: number
verified: boolean
staked: boolean
reputation: number | null
}

export interface SaidIdentity {
wallet: string
pda: string | null
owner: string | null
name: string | null
description: string | null
isVerified: boolean
image: string | null
twitter: string | null
website: string | null
serviceTypes: string[]
skills: string[]
reputationScore: number | null
feedbackCount: number | null
trustScore: number | null
passportMint: string | null
registered: boolean
}

export type SolanaTransactionPreviewKind = 'send-sol' | 'send-token' | 'swap' | 'launch'

export interface SolanaTransactionPreviewInput {
Expand Down
65 changes: 65 additions & 0 deletions src/panels/IntegrationCommandCenter/actionRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,71 @@ export async function runIntegrationAction(actionId: string, context: Integratio
}
}

if (actionId === 'check-said-identity') {
const wallet = context.defaultWallet
if (!wallet) {
return {
title: 'No wallet selected',
status: 'warning',
detail: 'Set a default wallet in the Wallet panel before checking its SAID identity.',
}
}

const identity = await daemon.said.getIdentity(wallet.address)
if (!identity.ok || !identity.data) {
return {
title: 'SAID lookup failed',
status: 'error',
detail: identity.error ?? 'DAEMON could not reach the SAID directory.',
items: [wallet.address],
}
}

if (!identity.data.registered) {
return {
title: 'Not registered on SAID',
status: 'info',
detail: `${wallet.name} has no SAID identity yet. Register it to earn a verifiable trust score and appear in the directory.`,
items: [wallet.address],
}
}

const trustRes = await daemon.said.getTrust(wallet.address)
const score = trustRes.ok && trustRes.data ? trustRes.data.score : identity.data.trustScore
const badges = [
identity.data.isVerified ? 'verified' : 'unverified',
trustRes.ok && trustRes.data?.staked ? 'staked' : 'no stake',
]
return {
title: identity.data.name ? `SAID: ${identity.data.name}` : 'SAID identity found',
status: 'success',
detail: `Trust score ${score ?? 'n/a'}/100 (${badges.join(', ')}).`,
items: [
wallet.address,
...(identity.data.pda ? [`PDA ${identity.data.pda}`] : []),
...(typeof identity.data.feedbackCount === 'number' ? [`${identity.data.feedbackCount} feedback`] : []),
],
}
}

if (actionId === 'open-said-directory') {
void daemon.shell.openExternal('https://www.saidprotocol.com/agents')
return {
title: 'Opening SAID directory',
status: 'success',
detail: 'Launched the SAID public agent directory in your browser.',
}
}

if (actionId === 'open-said-docs') {
void daemon.shell.openExternal('https://www.saidprotocol.com/docs')
return {
title: 'Opening SAID docs',
status: 'success',
detail: 'Launched the SAID docs in your browser.',
}
}

return {
title: 'Preview only',
status: 'info',
Expand Down
21 changes: 21 additions & 0 deletions src/panels/IntegrationCommandCenter/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,27 @@ export const INTEGRATION_REGISTRY: IntegrationDefinition[] = [
{ id: 'preview-squads-vault', label: 'Preview vault', description: 'Planned: multisig and vault preview before proposal creation or execution.', kind: 'planned', risk: 'read-only' },
],
},
{
id: 'said-protocol',
name: 'SAID Protocol',
tagline: 'On-chain identity + trust for AI agents',
description: 'Give agents a verifiable Solana identity with a verification badge, a 0–100 trust score, and on-chain reputation. Resolve agents by wallet/name/DID across 10 chains and discover them in a public directory. Complements the DAEMON work registry: SAID proves who an agent is, the registry proves what it did.',
category: 'agent',
docsUrl: 'https://www.saidprotocol.com/docs',
installCommand: 'pnpm add @said-protocol/agent said-sdk',
recommendedFor: ['agent identity', 'agent verification', 'trust scores', 'agent reputation', 'agent discovery', 'A2A messaging'],
requirements: [
{ type: 'package', key: '@said-protocol/agent', label: '@said-protocol/agent package', optional: true },
{ type: 'wallet', key: 'default-wallet', label: 'Default DAEMON wallet (for register/verify signing)' },
{ type: 'external-url', key: 'https://www.saidprotocol.com/agents', label: 'SAID agent directory' },
],
actions: [
{ id: 'check-said-identity', label: 'Check identity', description: 'Look up the default wallet on SAID and show its agent identity, verification badge, and trust score.', kind: 'safe-check', risk: 'read-only' },
{ id: 'open-said-directory', label: 'Browse directory', description: 'Open the SAID public agent directory in your browser.', kind: 'setup', risk: 'read-only' },
{ id: 'open-said-docs', label: 'Open docs', description: 'Open the SAID docs in your browser.', kind: 'setup', risk: 'read-only' },
{ id: 'preview-said-register', label: 'Preview registration', description: 'Review the register → verify flow and on-chain costs before any signing is enabled.', kind: 'planned', risk: 'requires-confirmation' },
],
},
]

export function getIntegration(id: string): IntegrationDefinition | undefined {
Expand Down
10 changes: 10 additions & 0 deletions src/types/daemon.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import type {
ForensicsGraphData,
ForensicsGraphNode,
RicoMapsEmbedStatus,
SaidIdentity,
SaidTrustScore,
JupiterTokenSearchResult,
ClaudeMdData,
ClaudeConnection,
Expand Down Expand Up @@ -178,6 +180,8 @@ export type {
ForensicsGraphData,
ForensicsGraphNode,
RicoMapsEmbedStatus,
SaidIdentity,
SaidTrustScore,
JupiterTokenSearchResult,
ClaudeMdData,
ClaudeConnection,
Expand Down Expand Up @@ -1607,6 +1611,7 @@ declare global {
launch: DaemonLaunch
dashboard: DaemonDashboard
forensics: DaemonForensics
said: DaemonSaid
registry: DaemonRegistry
colosseum: DaemonColosseum
idle: DaemonIdle
Expand Down Expand Up @@ -1747,6 +1752,11 @@ declare global {
startRicoMaps: () => Promise<IpcResponse<RicoMapsEmbedStatus>>
}

interface DaemonSaid {
getIdentity: (wallet: string) => Promise<IpcResponse<SaidIdentity>>
getTrust: (wallet: string) => Promise<IpcResponse<SaidTrustScore>>
}

interface DaemonBrowser {
navigate: (url: string) => Promise<IpcResponse<{ pageId: string; url: string; title: string; status: number; contentLength: number }>>
capture: (pageId: string, url: string, title: string, content: string) => Promise<IpcResponse<void>>
Expand Down
Loading