Skip to content

Commit bcd25b9

Browse files
committed
fix(dashboard,docs): ICP round 3 - new endpoints, doc fixes, deploy
REST API: - Add GET /api/scores/:mint for ReputationScoreV3 queries - Add includeReputation=true on GET /api/agents for bulk reputation - Deploy all pending fixes (stats, pagination, compressedAddress, nulls, order, POST outcome/message) Documentation: - Remove false @coral-xyz/anchor peer dep from SDK README - Fix nonTransferable default to false in SDK reference - Add feedbackCacheTtlMs, transactionConfig, reputationScoreSchema, credential to SDK reference - Add POST /api/feedback, scores endpoint, includeReputation to SKILL.md - Add endpointTypes case-sensitivity and clientAddress/counterparty mapping notes - Add ReputationScoreV3 cost (~0.002 SOL) to SKILL.md - Add incremental sync guidance for scoring providers - Fix getAddressDecoder import source in SKILL.md - Link REST API docs to GitHub URL instead of file path
1 parent bf70916 commit bcd25b9

5 files changed

Lines changed: 133 additions & 6 deletions

File tree

apps/dashboard/src/worker/identity-api.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
SATI_PROGRAM_ADDRESS,
2727
SATI_CHAIN_IDS,
2828
type AgentIdentity,
29+
parseReputationScoreContent,
2930
} from "@cascade-fyi/sati-sdk";
3031
import type { ServiceDefinition, TrustMechanism } from "@cascade-fyi/sati-sdk";
3132
import bs58 from "bs58";
@@ -263,6 +264,7 @@ export function createIdentityApi(env: Env) {
263264
const limit = Math.min(Math.max(limitParam, 1), 50);
264265
const offset = Math.max(offsetParam, 0);
265266

267+
const includeReputation = c.req.query("includeReputation") === "true";
266268
const endpointTypes = endpointTypesParam?.split(",").filter(Boolean) ?? [];
267269
const hasFilters = !!(nameFilter || endpointTypes.length > 0);
268270

@@ -331,6 +333,45 @@ export function createIdentityApi(env: Env) {
331333
// Apply pagination to filtered results
332334
const paginated = hasFilters ? filtered.slice(offset, offset + limit) : filtered.slice(0, limit);
333335

336+
// Optionally compute reputation for each agent in the page
337+
if (includeReputation) {
338+
const networkConfig = getNetworkConfig(sati);
339+
const feedbackSchemas = [networkConfig.feedbackSchema, networkConfig.feedbackPublicSchema].filter(Boolean);
340+
341+
await Promise.all(
342+
paginated.map(async (agent) => {
343+
let feedbackCount = 0;
344+
let totalValue = 0;
345+
let valueCount = 0;
346+
347+
for (const schema of feedbackSchemas) {
348+
try {
349+
const feedbacks = await sati.listFeedbacks({
350+
sasSchema: schema as Address,
351+
agentMint: agent.mint as Address,
352+
});
353+
for (const fb of feedbacks.items) {
354+
const parsed = parseFeedbackContent(fb.data.content, fb.data.contentType);
355+
feedbackCount++;
356+
if (parsed?.value !== undefined) {
357+
totalValue += parsed.value;
358+
valueCount++;
359+
}
360+
}
361+
} catch {
362+
// Skip schema errors for individual agents
363+
}
364+
}
365+
366+
agent.reputation = {
367+
count: feedbackCount,
368+
summaryValue: valueCount > 0 ? Math.round(totalValue / valueCount) : 0,
369+
summaryValueDecimals: 0,
370+
};
371+
}),
372+
);
373+
}
374+
334375
return c.json({ agents: paginated, count: paginated.length, totalAgents: Number(stats.totalAgents) });
335376
} catch (error) {
336377
console.error("[agents] ERROR:", error);
@@ -686,6 +727,46 @@ export function createIdentityApi(env: Env) {
686727
}
687728
});
688729

730+
// ---------------------------------------------------------------------------
731+
// GET /api/scores/:mint - List reputation scores from scoring providers
732+
// ---------------------------------------------------------------------------
733+
734+
app.get("/api/scores/:mint", async (c) => {
735+
const mint = c.req.param("mint");
736+
const network = getNetwork(c.req.query("network"));
737+
738+
if (!isAddress(mint)) {
739+
return c.json({ error: "Invalid mint address" }, 400);
740+
}
741+
742+
try {
743+
const sati = createSatiClient(network, env);
744+
const networkConfig = getNetworkConfig(sati);
745+
746+
if (!networkConfig.reputationScoreSchema) {
747+
return c.json({ error: "ReputationScore schema not configured for network" }, 500);
748+
}
749+
750+
const scores = await sati.listReputationScores(mint as Address, networkConfig.reputationScoreSchema as Address);
751+
752+
return c.json({
753+
scores: scores.map((s) => {
754+
const parsed = parseReputationScoreContent(s.content, s.contentType);
755+
return {
756+
provider: s.counterparty,
757+
agentMint: s.agentMint,
758+
outcome: s.outcome,
759+
content: parsed,
760+
};
761+
}),
762+
count: scores.length,
763+
});
764+
} catch (error) {
765+
console.error("[scores] ERROR:", error);
766+
return c.json({ error: error instanceof Error ? error.message : "Failed to list scores" }, 500);
767+
}
768+
});
769+
689770
// ---------------------------------------------------------------------------
690771
// POST /api/feedback - Give feedback (free, server signs as counterparty)
691772
// ---------------------------------------------------------------------------

docs/reference/rest-api.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ All endpoints accept a `?network=mainnet|devnet` query parameter (defaults to ma
1111
### List agents
1212

1313
```
14-
GET /api/agents?network=mainnet&limit=20&offset=0&order=newest&name=search&owner=ADDRESS&endpointTypes=MCP,A2A
14+
GET /api/agents?network=mainnet&limit=20&offset=0&order=newest&name=search&owner=ADDRESS&endpointTypes=MCP,A2A&includeReputation=true
1515
```
1616

1717
| Param | Type | Description |
@@ -22,7 +22,8 @@ GET /api/agents?network=mainnet&limit=20&offset=0&order=newest&name=search&owner
2222
| `order` | `newest` \| `oldest` | Sort order (default `newest`) |
2323
| `name` | string | Filter by name (case-insensitive substring). Searches all agents, not just the current page. |
2424
| `owner` | string | Filter by owner address |
25-
| `endpointTypes` | string | Comma-separated service types: `MCP`, `A2A`, `OASF`. Searches all agents. |
25+
| `endpointTypes` | string | Comma-separated service types: `MCP`, `A2A`, `OASF` (case-sensitive). Searches all agents. |
26+
| `includeReputation` | `true` | Include `reputation` object per agent (slower - extra RPC calls per agent) |
2627

2728
**Response:**
2829

@@ -115,6 +116,36 @@ GET /api/stats?network=mainnet
115116
}
116117
```
117118

119+
### Reputation scores (from providers)
120+
121+
```
122+
GET /api/scores/:mint?network=mainnet
123+
```
124+
125+
Returns ReputationScoreV3 attestations published by scoring providers for this agent.
126+
127+
**Response:**
128+
129+
```json
130+
{
131+
"scores": [
132+
{
133+
"provider": "Provider...",
134+
"agentMint": "Agent...",
135+
"outcome": 2,
136+
"content": {
137+
"score": 85,
138+
"methodology": "weighted_average",
139+
"feedbackCount": 42
140+
}
141+
}
142+
],
143+
"count": 1
144+
}
145+
```
146+
147+
`content` is the parsed JSON from the scoring provider's attestation. Structure varies by provider but typically includes `score`, `methodology`, and `feedbackCount`.
148+
118149
### Reputation badge
119150

120151
```
@@ -239,3 +270,5 @@ Server acts as counterparty and pays transaction fees. Rate limited per IP.
239270
- `createdAt` is a Unix timestamp (seconds), approximate based on Solana slot times (~400ms/slot)
240271
- `schema` field indicates whether feedback is blind (`FeedbackV1`) or public (`FeedbackPublicV1`)
241272
- `compressedAddress` is a base58-encoded string, usable as a stable identifier for feedback entries
273+
- `clientAddress` in the REST API corresponds to `counterparty` in the SDK (the reviewer's wallet address)
274+
- `endpointTypes` filter is case-sensitive - use `MCP`, `A2A`, `OASF` (not lowercase)

docs/reference/sati-sdk.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const sati = new Sati({
4040
| `wsUrl` | `string` | No | Custom WebSocket URL for subscriptions |
4141
| `photonRpcUrl` | `string` | No | Photon RPC URL for Light Protocol queries (defaults to hosted proxy) |
4242
| `onWarning` | `(warning: SatiWarning) => void` | No | Non-fatal warning callback (parse errors, RPC failures) |
43+
| `feedbackCacheTtlMs` | `number` | No | Feedback cache TTL in ms (default 30000, 0 to disable) |
44+
| `transactionConfig` | `object` | No | `{ priorityFeeMicroLamports?, computeUnitLimit?, maxRetries? }` |
4345

4446
### Properties
4547

@@ -51,6 +53,8 @@ const sati = new Sati({
5153
| `feedbackPublicSchema` | `Address \| undefined` | FeedbackPublic schema address |
5254
| `feedbackSchema` | `Address \| undefined` | Feedback schema address |
5355
| `validationSchema` | `Address \| undefined` | Validation schema address |
56+
| `reputationScoreSchema` | `Address \| undefined` | ReputationScoreV3 schema address |
57+
| `credential` | `Address \| undefined` | SATI credential address (needed for reputation scores) |
5458
| `lookupTable` | `Address \| undefined` | Address Lookup Table address |
5559

5660
---
@@ -122,7 +126,7 @@ const result = await builder.register({
122126
const result = await builder.register({
123127
payer: signer,
124128
uploader: createPinataUploader(process.env.PINATA_JWT!),
125-
nonTransferable: true, // soulbound (default)
129+
nonTransferable: false, // default: false. Set true for soulbound.
126130
});
127131

128132
console.log(result.mint); // Agent mint address

packages/sdk/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pnpm add @cascade-fyi/sati-sdk
1010

1111
**Peer dependencies:**
1212
```bash
13-
pnpm add @solana/kit @solana-program/token-2022 @coral-xyz/anchor
13+
pnpm add @solana/kit @solana-program/token-2022
1414
```
1515

1616
## Quick Start

skills/sati/SKILL.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,9 @@ for await (const page of sati.listAllFeedbacks({ agentMint: address("Agent...")
302302
for (const item of page.items) {
303303
// item.data: { taskRef, agentMint, counterparty, dataHash, outcome, contentType, content }
304304
// item.raw.slotCreated (bigint) for on-chain slot
305-
// item.address is Uint8Array - decode with getAddressDecoder().read(item.address, 0)
305+
// item.address is Uint8Array - decode with getAddressDecoder() from @solana/kit:
306+
// import { getAddressDecoder } from "@solana/kit";
307+
// const [address] = getAddressDecoder().read(item.address, 0);
306308
const parsed = parseFeedbackContent(item.data.content, item.data.contentType);
307309
// parsed: { value, valueDecimals, tag1, tag2, m (message), endpoint, reviewer, feedbackURI, feedbackHash }
308310
}
@@ -314,6 +316,8 @@ for await (const page of sati.listAllFeedbacks()) { /* ... */ }
314316

315317
Note: `searchFeedback`/`searchAllFeedback` return `ParsedFeedback[]` (fully parsed). `listAllFeedbacks` returns raw `ParsedAttestation` pages where content is still bytes - use `parseFeedbackContent(item.data.content, item.data.contentType)` to extract fields. `createdAt` timestamps in `ParsedFeedback` are approximate - derived from Solana slot numbers using ~400ms/slot estimate.
316318

319+
**Incremental sync (scoring providers):** There is no `sinceSlot` filter - Photon RPC does not support slot-range queries on compressed accounts. For incremental updates, track `item.raw.slotCreated` locally and skip items below your last-processed slot on each full fetch. At current volumes this is efficient; for higher scale, use a Solana transaction log indexer (Helius webhooks, Yellowstone gRPC) to stream new attestation events.
320+
317321
### 4. Reputation Summary
318322

319323
```typescript
@@ -487,15 +491,19 @@ const score = await sati.getReputationScore(
487491

488492
### REST API
489493

490-
The dashboard at `sati.cascade.fyi` exposes a public REST API. See `docs/reference/rest-api.md` for full endpoint documentation. Key endpoints:
494+
The dashboard at `sati.cascade.fyi` exposes a public REST API. See the [REST API reference](https://github.com/cascade-protocol/sati/blob/main/docs/reference/rest-api.md) for full endpoint documentation. Key endpoints:
491495

492496
- `GET /api/agents` - list/search agents (supports `name`, `owner`, `endpointTypes`, `order`, pagination)
493497
- `GET /api/agents/:mint` - single agent with reputation summary
494498
- `GET /api/feedback/:mint` - feedback for an agent (paginated with `limit`/`offset`)
495499
- `GET /api/feedback` - global feedback across all agents (paginated)
496500
- `GET /api/reputation/:mint` - reputation summary with tag/reviewer filters
497501
- `GET /api/stats` - registry statistics (`totalAgents`, `groupMint`, etc.)
502+
- `GET /api/scores/:mint` - reputation scores from scoring providers (ReputationScoreV3)
498503
- `GET /api/badge/:mint` - SVG reputation badge for README embedding
504+
- `POST /api/feedback` - submit feedback without a wallet (server acts as counterparty, rate limited per IP)
505+
506+
The agents list supports `includeReputation=true` to get reputation inline per agent (slower but avoids N+1 requests). The REST API uses `clientAddress` for the reviewer field (the SDK calls this `counterparty`). Filter params like `endpointTypes` are case-sensitive (use `MCP`, not `mcp`).
499507

500508
### Configuration
501509

@@ -551,6 +559,7 @@ try {
551559
| Agent registration | ~0.003 SOL |
552560
| Agent transfer | ~0.0005 SOL |
553561
| Feedback attestation | ~0.00001 SOL (compressed) |
562+
| Reputation score (ReputationScoreV3) | ~0.002 SOL (regular SAS, not compressed) |
554563
| Devnet | Free (auto-funded faucet) |
555564

556565
## Common Issues

0 commit comments

Comments
 (0)