Skip to content

Commit b2b8426

Browse files
committed
feat(sdk): outcome filter, batch listAllAgents; REST API: global feedback, pagination, missing fields
SDK: - Add outcome filter to FeedbackSearchOptions - listAllAgents uses batched loadAgents internally (fixes N+1 RPC calls) REST API (dashboard): - GET /api/feedback (global) - feedback across all agents - GET /api/agents: offset pagination, endpointTypes filter, totalAgents, nonTransferable - GET /api/feedback/:mint: add message, createdAt, compressedAddress, schema, outcome filter SKILL.md: - Document reputation scoring, listAllFeedbacks, browser wallet flow, schema discrimination - Clarify searchFeedback vs searchAllFeedback, platform ownership model - Add REST API reference, Outcome enum values, various warnings
1 parent 19de9cf commit b2b8426

7 files changed

Lines changed: 264 additions & 63 deletions

File tree

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

Lines changed: 104 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,17 @@ export function createIdentityApi(env: Env) {
254254
const network = getNetwork(c.req.query("network"));
255255
const nameFilter = c.req.query("name")?.toLowerCase();
256256
const ownerFilter = c.req.query("owner");
257+
const endpointTypesParam = c.req.query("endpointTypes");
257258
const limitParam = Number.parseInt(c.req.query("limit") ?? "20", 10);
259+
const offsetParam = Number.parseInt(c.req.query("offset") ?? "0", 10);
258260
const limit = Math.min(Math.max(limitParam, 1), 50);
261+
const offset = Math.max(offsetParam, 0);
262+
263+
const endpointTypes = endpointTypesParam?.split(",").filter(Boolean) ?? [];
259264

260265
try {
261266
const sati = createSatiClient(network, env);
267+
const stats = await sati.getRegistryStats();
262268

263269
let agents: AgentIdentity[];
264270
if (ownerFilter) {
@@ -267,11 +273,11 @@ export function createIdentityApi(env: Env) {
267273
}
268274
agents = await sati.listAgentsByOwner(ownerFilter as Address);
269275
} else {
270-
const result = await sati.listAllAgents({ limit });
271-
agents = result.agents;
276+
const result = await sati.listAllAgents({ limit: limit + offset, offset: 0 });
277+
agents = result.agents.slice(offset);
272278
}
273279

274-
// Fetch registration files and apply name filter
280+
// Fetch registration files and apply filters
275281
const results = [];
276282
for (const agent of agents) {
277283
if (results.length >= limit) break;
@@ -283,6 +289,11 @@ export function createIdentityApi(env: Env) {
283289
if (!agentName.includes(nameFilter)) continue;
284290
}
285291

292+
if (endpointTypes.length > 0) {
293+
const serviceNames = (regFile?.services ?? []).map((s) => s.name);
294+
if (!endpointTypes.some((t) => serviceNames.includes(t))) continue;
295+
}
296+
286297
results.push({
287298
mint: agent.mint,
288299
agentId: `${CAIP2_CHAINS[network]}:${agent.mint}`,
@@ -292,14 +303,15 @@ export function createIdentityApi(env: Env) {
292303
image: regFile?.image ?? "",
293304
uri: agent.uri,
294305
memberNumber: Number(agent.memberNumber),
306+
nonTransferable: agent.nonTransferable,
295307
active: regFile?.active ?? true,
296308
services: regFile?.services ?? [],
297309
supportedTrust: regFile?.supportedTrust ?? [],
298310
x402Support: regFile?.x402Support ?? false,
299311
});
300312
}
301313

302-
return c.json({ agents: results, count: results.length });
314+
return c.json({ agents: results, count: results.length, totalAgents: Number(stats.totalAgents) });
303315
} catch (error) {
304316
console.error("[agents] ERROR:", error);
305317
return c.json({ error: error instanceof Error ? error.message : "Failed to list agents" }, 500);
@@ -511,57 +523,111 @@ export function createIdentityApi(env: Env) {
511523
// GET /api/feedback/:mint - List feedback for agent
512524
// ---------------------------------------------------------------------------
513525

526+
// Shared feedback query logic for both per-agent and global endpoints
527+
async function queryFeedback(
528+
sati: Sati,
529+
network: "devnet" | "mainnet",
530+
agentMint?: Address,
531+
filters?: { clientAddress?: string; tag1?: string; tag2?: string; outcome?: string },
532+
) {
533+
const networkConfig = getNetworkConfig(sati);
534+
const feedbackSchemas = [networkConfig.feedbackSchema, networkConfig.feedbackPublicSchema].filter(Boolean);
535+
536+
const { rpc: rpcUrl } = env.RPC_URLS[network];
537+
const slotResp = await fetch(rpcUrl, {
538+
method: "POST",
539+
headers: { "Content-Type": "application/json" },
540+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getSlot", params: [{ commitment: "confirmed" }] }),
541+
});
542+
const slotJson = (await slotResp.json()) as { result: number };
543+
const currentSlot = slotJson.result;
544+
const nowSec = Math.floor(Date.now() / 1000);
545+
546+
const feedbackItems: Array<Record<string, unknown>> = [];
547+
let feedbackIndex = 0;
548+
549+
for (const schema of feedbackSchemas) {
550+
const filter: Record<string, unknown> = { sasSchema: schema as Address };
551+
if (agentMint) filter.agentMint = agentMint;
552+
553+
const feedbacks = await sati.listFeedbacks(filter as { sasSchema: Address; agentMint?: Address });
554+
555+
for (const fb of feedbacks.items) {
556+
const parsed = parseFeedbackContent(fb.data.content, fb.data.contentType);
557+
558+
if (filters?.clientAddress && fb.data.counterparty !== filters.clientAddress) continue;
559+
if (filters?.tag1 && parsed?.tag1 !== filters.tag1) continue;
560+
if (filters?.tag2 && parsed?.tag2 !== filters.tag2) continue;
561+
if (filters?.outcome !== undefined && String(fb.data.outcome) !== filters.outcome) continue;
562+
563+
const slotDiff = Number(BigInt(currentSlot) - fb.raw.slotCreated);
564+
const createdAt = nowSec - Math.floor(slotDiff * 0.4);
565+
566+
feedbackItems.push({
567+
compressedAddress: fb.address,
568+
clientAddress: fb.data.counterparty,
569+
agentMint: fb.data.agentMint,
570+
feedbackIndex: feedbackIndex++,
571+
value: parsed?.value ?? 0,
572+
valueDecimals: parsed?.valueDecimals ?? 0,
573+
tag1: parsed?.tag1 ?? "",
574+
tag2: parsed?.tag2 ?? "",
575+
message: parsed?.m ?? "",
576+
endpoint: parsed?.endpoint ?? "",
577+
outcome: fb.data.outcome,
578+
createdAt,
579+
schema: schema === networkConfig.feedbackSchema ? "FeedbackV1" : "FeedbackPublicV1",
580+
isRevoked: false,
581+
});
582+
}
583+
}
584+
585+
return feedbackItems;
586+
}
587+
514588
app.get("/api/feedback/:mint", async (c) => {
515589
const mint = c.req.param("mint");
516590
const network = getNetwork(c.req.query("network"));
517-
const clientAddressFilter = c.req.query("clientAddress");
518-
const tag1Filter = c.req.query("tag1");
519-
const tag2Filter = c.req.query("tag2");
520591

521592
if (!isAddress(mint)) {
522593
return c.json({ error: "Invalid mint address" }, 400);
523594
}
524595

525596
try {
526597
const sati = createSatiClient(network, env);
527-
const networkConfig = getNetworkConfig(sati);
528-
const feedbackSchemas = [networkConfig.feedbackSchema, networkConfig.feedbackPublicSchema].filter(Boolean);
529-
530-
const feedbackItems: Array<Record<string, unknown>> = [];
531-
let feedbackIndex = 0;
598+
const feedbackItems = await queryFeedback(sati, network, mint as Address, {
599+
clientAddress: c.req.query("clientAddress"),
600+
tag1: c.req.query("tag1"),
601+
tag2: c.req.query("tag2"),
602+
outcome: c.req.query("outcome"),
603+
});
532604

533-
for (const schema of feedbackSchemas) {
534-
const feedbacks = await sati.listFeedbacks({
535-
sasSchema: schema as Address,
536-
agentMint: mint as Address,
537-
});
605+
return c.json({ feedbacks: feedbackItems, count: feedbackItems.length });
606+
} catch (error) {
607+
console.error("[feedback list] ERROR:", error);
608+
return c.json({ error: error instanceof Error ? error.message : "Failed to list feedback" }, 500);
609+
}
610+
});
538611

539-
for (const fb of feedbacks.items) {
540-
const parsed = parseFeedbackContent(fb.data.content, fb.data.contentType);
612+
// ---------------------------------------------------------------------------
613+
// GET /api/feedback - List ALL feedback across all agents
614+
// ---------------------------------------------------------------------------
541615

542-
// Apply filters
543-
if (clientAddressFilter && fb.data.counterparty !== clientAddressFilter) continue;
544-
if (tag1Filter && parsed?.tag1 !== tag1Filter) continue;
545-
if (tag2Filter && parsed?.tag2 !== tag2Filter) continue;
616+
app.get("/api/feedback", async (c) => {
617+
const network = getNetwork(c.req.query("network"));
546618

547-
feedbackItems.push({
548-
clientAddress: fb.data.counterparty,
549-
feedbackIndex: feedbackIndex++,
550-
value: parsed?.value ?? 0,
551-
valueDecimals: parsed?.valueDecimals ?? 0,
552-
tag1: parsed?.tag1 ?? "",
553-
tag2: parsed?.tag2 ?? "",
554-
endpoint: parsed?.endpoint ?? "",
555-
reviewer: parsed?.reviewer ?? "",
556-
outcome: fb.data.outcome,
557-
isRevoked: false,
558-
});
559-
}
560-
}
619+
try {
620+
const sati = createSatiClient(network, env);
621+
const feedbackItems = await queryFeedback(sati, network, undefined, {
622+
clientAddress: c.req.query("clientAddress"),
623+
tag1: c.req.query("tag1"),
624+
tag2: c.req.query("tag2"),
625+
outcome: c.req.query("outcome"),
626+
});
561627

562628
return c.json({ feedbacks: feedbackItems, count: feedbackItems.length });
563629
} catch (error) {
564-
console.error("[feedback list] ERROR:", error);
630+
console.error("[feedback global] ERROR:", error);
565631
return c.json({ error: error instanceof Error ? error.message : "Failed to list feedback" }, 500);
566632
}
567633
});

docs/reference/rest-api.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ 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&name=search&owner=ADDRESS
14+
GET /api/agents?network=mainnet&limit=20&offset=0&name=search&owner=ADDRESS&endpointTypes=MCP,A2A
1515
```
1616

1717
| Param | Type | Description |
1818
|-------|------|-------------|
1919
| `network` | `mainnet` \| `devnet` | Network to query |
2020
| `limit` | number | Max results (1-50, default 20) |
21+
| `offset` | number | Skip first N agents (default 0) |
2122
| `name` | string | Filter by name (case-insensitive substring) |
2223
| `owner` | string | Filter by owner address |
24+
| `endpointTypes` | string | Comma-separated service types: `MCP`, `A2A`, `OASF` |
2325

2426
**Response:**
2527

@@ -35,13 +37,15 @@ GET /api/agents?network=mainnet&limit=20&name=search&owner=ADDRESS
3537
"image": "https://...",
3638
"uri": "ipfs://Qm...",
3739
"memberNumber": 1,
40+
"nonTransferable": false,
3841
"active": true,
3942
"services": [{"name": "MCP", "endpoint": "https://..."}],
4043
"supportedTrust": ["reputation"],
4144
"x402Support": false
4245
}
4346
],
44-
"count": 1
47+
"count": 1,
48+
"totalAgents": 80
4549
}
4650
```
4751

@@ -102,34 +106,53 @@ Returns an SVG badge (shields.io style) showing the agent's reputation score. Em
102106

103107
Badge shows score/100 with review count, color-coded: green (70+), yellow (40-69), red (<40), gray (no reviews). Cached for 5 minutes.
104108

105-
### List feedback
109+
### List feedback (per agent)
106110

107111
```
108-
GET /api/feedback/:mint?network=mainnet&clientAddress=ADDR&tag1=starred&tag2=chat
112+
GET /api/feedback/:mint?network=mainnet&clientAddress=ADDR&tag1=starred&tag2=chat&outcome=2
109113
```
110114

115+
| Param | Type | Description |
116+
|-------|------|-------------|
117+
| `clientAddress` | string | Filter by reviewer address |
118+
| `tag1` | string | Filter by primary tag |
119+
| `tag2` | string | Filter by secondary tag |
120+
| `outcome` | number | Filter by outcome: 0 = Negative, 1 = Neutral, 2 = Positive |
121+
111122
**Response:**
112123

113124
```json
114125
{
115126
"feedbacks": [
116127
{
128+
"compressedAddress": "Attest...",
117129
"clientAddress": "Reviewer...",
130+
"agentMint": "Agent...",
118131
"feedbackIndex": 0,
119132
"value": 87,
120133
"valueDecimals": 0,
121134
"tag1": "starred",
122135
"tag2": "chat",
136+
"message": "Great response time",
123137
"endpoint": "https://...",
124-
"reviewer": "",
125138
"outcome": 2,
139+
"createdAt": 1709654400,
140+
"schema": "FeedbackPublicV1",
126141
"isRevoked": false
127142
}
128143
],
129144
"count": 1
130145
}
131146
```
132147

148+
### List feedback (global)
149+
150+
```
151+
GET /api/feedback?network=mainnet&clientAddress=ADDR&tag1=starred&outcome=2
152+
```
153+
154+
Same parameters and response as per-agent endpoint, but returns feedback across all agents. Useful for indexers and scoring providers.
155+
133156
### Submit feedback
134157

135158
```
@@ -161,3 +184,5 @@ Server acts as counterparty and pays transaction fees. Rate limited per IP.
161184
- Reputation is computed by averaging all feedback values (no weighting)
162185
- `outcome` values: 0 = Negative, 1 = Neutral, 2 = Positive
163186
- Agent IDs follow CAIP-2 format: `solana:{genesis_hash}:{mint_address}`
187+
- `createdAt` is a Unix timestamp (seconds), approximate based on Solana slot times
188+
- `schema` field indicates whether feedback is blind (`FeedbackV1`) or public (`FeedbackPublicV1`)

packages/sdk/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.15.0] - 2026-03-05
9+
10+
### Added
11+
12+
- **`outcome` filter** on `FeedbackSearchOptions` - filter feedback by `Outcome.Positive`, `Outcome.Negative`, or `Outcome.Neutral`
13+
14+
### Changed
15+
16+
- `listAllAgents()` now uses batched `loadAgents()` internally instead of N+1 individual `loadAgent()` calls
17+
18+
### Fixed
19+
20+
- `listAllAgents()` with large limits no longer hits rate limits on default hosted proxy
21+
822
## [0.14.0] - 2026-03-05
923

1024
### Changed

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cascade-fyi/sati-sdk",
3-
"version": "0.14.0",
3+
"version": "0.15.0",
44
"description": "TypeScript SDK for SATI - Solana Agent Trust Infrastructure",
55
"repository": {
66
"type": "git",

packages/sdk/src/client.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,16 +1072,18 @@ export class Sati {
10721072
// Batch fetch all AgentIndex accounts
10731073
const indexes = await fetchAllMaybeAgentIndex(this.rpc, pdas);
10741074

1075-
// Load agent identities for existing indexes
1076-
const agents: AgentIdentity[] = [];
1075+
// Batch load agent identities for existing indexes
1076+
const existingMints: Address[] = [];
10771077
for (const index of indexes) {
10781078
if (index.exists) {
1079-
const agent = await this.loadAgent(index.data.mint);
1080-
if (agent) {
1081-
agents.push(agent);
1082-
}
1079+
existingMints.push(index.data.mint);
10831080
}
10841081
}
1082+
const loaded = existingMints.length > 0 ? await this.loadAgents(existingMints) : [];
1083+
const agents: AgentIdentity[] = [];
1084+
for (const agent of loaded) {
1085+
if (agent) agents.push(agent);
1086+
}
10851087

10861088
return { agents, totalAgents: total };
10871089
}
@@ -3005,6 +3007,7 @@ export class Sati {
30053007

30063008
if (options?.tag1 !== undefined && tag1 !== options.tag1) continue;
30073009
if (options?.tag2 !== undefined && tag2 !== options.tag2) continue;
3010+
if (options?.outcome !== undefined && item.data.outcome !== options.outcome) continue;
30083011
if (options?.minValue !== undefined && (value === undefined || value < options.minValue)) continue;
30093012
if (options?.maxValue !== undefined && (value === undefined || value > options.maxValue)) continue;
30103013

packages/sdk/src/convenience.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export interface FeedbackSearchOptions {
8686
tag1?: string;
8787
/** Filter by tag2 */
8888
tag2?: string;
89+
/** Filter by outcome (Positive, Negative, Neutral) */
90+
outcome?: Outcome;
8991
/** Minimum value (inclusive) */
9092
minValue?: number;
9193
/** Maximum value (inclusive) */

0 commit comments

Comments
 (0)