Skip to content

Commit 7aa93c3

Browse files
committed
feat: zero-config SDK experience with hosted Photon proxy and IPFS uploader
- sati-sdk 0.6.0: add createSatiUploader() for hosted IPFS uploads, default Photon RPC proxy URLs at sati.cascade.fyi/api/photon/{network} - sati-agent0-sdk 0.3.0: registerIPFS()/updateIPFS() fall back to hosted uploader when pinataJwt is not set, add photonRpcUrl config passthrough - Worker: add POST /api/photon/:network proxy and POST /api/upload-metadata endpoints with per-IP rate limiting and payload size caps - Docs: rewrite guides for zero-config, update examples to use registerIPFS() as default path, restructure navigation
1 parent 735f89a commit 7aa93c3

35 files changed

Lines changed: 3770 additions & 1451 deletions

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

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,41 @@ import type { Endpoint, TrustMechanism } from "@cascade-fyi/sati-sdk";
3030
import bs58 from "bs58";
3131
import type { Env } from "../env";
3232

33+
// =============================================================================
34+
// Rate Limiter (per-isolate, best-effort)
35+
// =============================================================================
36+
37+
/** Per-IP sliding window rate limiter. State lives in module scope (shared within a CF Worker isolate). */
38+
const rateLimitBuckets = new Map<string, { count: number; resetAt: number }>();
39+
40+
function isRateLimited(key: string, maxRequests: number, windowMs: number): boolean {
41+
const now = Date.now();
42+
const bucket = rateLimitBuckets.get(key);
43+
if (!bucket || now >= bucket.resetAt) {
44+
rateLimitBuckets.set(key, { count: 1, resetAt: now + windowMs });
45+
return false;
46+
}
47+
bucket.count++;
48+
return bucket.count > maxRequests;
49+
}
50+
51+
// Periodic cleanup of stale buckets (runs at most once per minute)
52+
let lastCleanup = 0;
53+
function cleanupBuckets() {
54+
const now = Date.now();
55+
if (now - lastCleanup < 60_000) return;
56+
lastCleanup = now;
57+
for (const [key, bucket] of rateLimitBuckets) {
58+
if (now >= bucket.resetAt) rateLimitBuckets.delete(key);
59+
}
60+
}
61+
62+
// =============================================================================
63+
// Constants
64+
// =============================================================================
65+
66+
const MAX_METADATA_SIZE = 100 * 1024; // 100 KB
67+
3368
// =============================================================================
3469
// Types
3570
// =============================================================================
@@ -579,5 +614,87 @@ export function createIdentityApi(env: Env) {
579614
}
580615
});
581616

617+
// ---------------------------------------------------------------------------
618+
// POST /api/photon/:network - Photon RPC proxy
619+
// ---------------------------------------------------------------------------
620+
621+
app.post("/api/photon/:network", async (c) => {
622+
cleanupBuckets();
623+
624+
const network = c.req.param("network");
625+
if (network !== "devnet" && network !== "mainnet") {
626+
return c.json({ error: "Invalid network - must be devnet or mainnet" }, 400);
627+
}
628+
629+
// Rate limit: 120 requests per minute per IP
630+
const ip = c.req.header("cf-connecting-ip") ?? c.req.header("x-forwarded-for") ?? "unknown";
631+
if (isRateLimited(`photon:${ip}`, 120, 60_000)) {
632+
return c.json({ error: "Rate limit exceeded - max 120 requests per minute" }, 429);
633+
}
634+
635+
const rpcUrl = env.RPC_URLS[network].rpc;
636+
const body = await c.req.text();
637+
638+
const response = await fetch(rpcUrl, {
639+
method: "POST",
640+
headers: { "Content-Type": "application/json" },
641+
body,
642+
});
643+
644+
const result = await response.text();
645+
return new Response(result, {
646+
status: response.status,
647+
headers: { "Content-Type": "application/json" },
648+
});
649+
});
650+
651+
// ---------------------------------------------------------------------------
652+
// POST /api/upload-metadata - Upload metadata JSON to IPFS
653+
// ---------------------------------------------------------------------------
654+
655+
app.post("/api/upload-metadata", async (c) => {
656+
cleanupBuckets();
657+
658+
// Rate limit: 10 uploads per minute per IP
659+
const ip = c.req.header("cf-connecting-ip") ?? c.req.header("x-forwarded-for") ?? "unknown";
660+
if (isRateLimited(`upload:${ip}`, 10, 60_000)) {
661+
return c.json({ error: "Rate limit exceeded - max 10 uploads per minute" }, 429);
662+
}
663+
664+
if (!env.PINATA_JWT) {
665+
return c.json({ error: "Server misconfigured: missing PINATA_JWT" }, 500);
666+
}
667+
668+
// Payload size cap
669+
const contentLength = Number(c.req.header("content-length") ?? 0);
670+
if (contentLength > MAX_METADATA_SIZE) {
671+
return c.json({ error: `Payload too large - max ${MAX_METADATA_SIZE / 1024}KB` }, 413);
672+
}
673+
674+
let data: unknown;
675+
try {
676+
const body = await c.req.text();
677+
if (body.length > MAX_METADATA_SIZE) {
678+
return c.json({ error: `Payload too large - max ${MAX_METADATA_SIZE / 1024}KB` }, 413);
679+
}
680+
data = JSON.parse(body);
681+
} catch {
682+
return c.json({ error: "Invalid JSON body" }, 400);
683+
}
684+
685+
if (!data || typeof data !== "object") {
686+
return c.json({ error: "Request body must be a JSON object" }, 400);
687+
}
688+
689+
try {
690+
const uploader = createPinataUploader(env.PINATA_JWT);
691+
const uri = await uploader.upload(data);
692+
return c.json({ uri });
693+
} catch (error) {
694+
console.error("[upload-metadata] ERROR:", error);
695+
return c.json({ error: error instanceof Error ? error.message : "Upload failed" }, 500);
696+
}
697+
});
698+
582699
return app;
583700
}

docs/.vitepress/config.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,77 @@
11
import { defineConfig } from 'vitepress'
2+
import llmstxt, { copyOrDownloadAsMarkdownButtons } from 'vitepress-plugin-llms'
23

34
export default defineConfig({
5+
markdown: {
6+
config(md) {
7+
md.use(copyOrDownloadAsMarkdownButtons)
8+
},
9+
},
10+
11+
vite: {
12+
plugins: [llmstxt({ excludeIndexPage: false })],
13+
},
14+
415
title: 'SATI',
5-
description: 'Solana Agent Trust Infrastructure - Open trust layer for AI agents',
16+
description: 'Solana Agent Trust Infrastructure - On-chain identity and reputation for AI agents',
617
base: '/sati/',
718

819
head: [
920
['meta', { name: 'theme-color', content: '#14F195' }],
1021
['meta', { property: 'og:type', content: 'website' }],
11-
['meta', { property: 'og:title', content: 'SATI - Solana Agent Trust Infrastructure' }],
12-
['meta', { property: 'og:description', content: 'Production-ready agent reputation on Solana. ~$0.002 per attestation.' }],
22+
['meta', { property: 'og:title', content: 'SATI - On-chain identity for AI agents' }],
23+
['meta', { property: 'og:description', content: 'Verifiable track record for AI agents on Solana. Proof of participation, zero infrastructure, ~$0.002 per attestation.' }],
1324
],
1425

1526
themeConfig: {
1627
nav: [
17-
{ text: 'Guide', link: '/getting-started' },
18-
{ text: 'Specification', link: '/specification' },
19-
{ text: 'SDK', link: 'https://www.npmjs.com/package/@cascade-fyi/sati-sdk' },
28+
{ text: 'Getting Started', link: '/getting-started' },
29+
{ text: 'Guides', link: '/guides/agent-marketplace' },
30+
{ text: 'Reference', link: '/reference/' },
2031
{ text: 'GitHub', link: 'https://github.com/cascade-protocol/sati' },
2132
],
2233

2334
sidebar: [
2435
{
25-
text: 'Introduction',
36+
text: 'Start Here',
2637
items: [
27-
{ text: 'What is SATI?', link: '/' },
38+
{ text: 'Home', link: '/' },
2839
{ text: 'Getting Started', link: '/getting-started' },
40+
{ text: 'How It Works', link: '/how-it-works' },
2941
]
3042
},
3143
{
32-
text: 'Guide',
44+
text: 'Guides',
3345
items: [
34-
{ text: 'Core Concepts', link: '/guide/concepts' },
35-
{ text: 'Agent Registration', link: '/guide/agent-registration' },
36-
{ text: 'Feedback & Reputation', link: '/guide/feedback' },
37-
{ text: 'Delegation', link: '/guide/delegation' },
46+
{ text: 'Agent Marketplace', link: '/guides/agent-marketplace' },
47+
{ text: 'x402 Payment Feedback', link: '/guides/x402-feedback' },
48+
{ text: 'Register an MCP Agent', link: '/guides/mcp-agent' },
49+
{ text: 'Query Reputation', link: '/guides/query-reputation' },
50+
{ text: 'Browser Wallet Flow', link: '/guides/browser-wallet' },
3851
]
3952
},
4053
{
41-
text: 'Concepts',
54+
text: 'API Reference',
4255
items: [
43-
{ text: 'Validation', link: '/concepts/validation' },
56+
{ text: 'Overview', link: '/reference/' },
57+
{ text: 'sati-agent0-sdk', link: '/reference/sati-agent0-sdk' },
58+
{ text: 'sati-sdk', link: '/reference/sati-sdk' },
4459
]
4560
},
4661
{
47-
text: 'Reference',
62+
text: 'Deep Dive',
4863
items: [
4964
{ text: 'Specification', link: '/specification' },
5065
]
5166
},
67+
{
68+
text: 'Advanced',
69+
collapsed: true,
70+
items: [
71+
{ text: 'Transaction Sizes', link: '/advanced/transaction-sizes' },
72+
{ text: 'Compute Unit Benchmarks', link: '/advanced/benchmarks' },
73+
]
74+
},
5275
],
5376

5477
socialLinks: [

docs/.vitepress/theme/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import DefaultTheme from 'vitepress/theme'
2+
import type { Theme } from 'vitepress'
3+
import CopyOrDownloadAsMarkdownButtons from 'vitepress-plugin-llms/vitepress-components/CopyOrDownloadAsMarkdownButtons.vue'
4+
5+
export default {
6+
extends: DefaultTheme,
7+
enhanceApp({ app }) {
8+
app.component('CopyOrDownloadAsMarkdownButtons', CopyOrDownloadAsMarkdownButtons)
9+
},
10+
} satisfies Theme
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
#### 2025-12-17 14:50:51.180068 UTC
1+
---
2+
title: Compute Unit Benchmarks
3+
description: CU measurements for all SATI program instructions
4+
---
25

3-
Solana CLI Version: solana-cli 3.1.1 (src:7096e605; feat:3652419286, client:Agave)
6+
# Compute Unit Benchmarks
7+
8+
Measured 2025-12-17 with Solana CLI 3.1.1 (Agave).
49

510
| Name | CUs | Delta |
611
|------|------|-------|
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
# SATI Transaction Size Guide
1+
---
2+
title: Transaction Sizes
3+
description: Solana transaction size constraints and how SATI optimizes for them
4+
---
5+
6+
# Transaction Sizes
27

38
Understanding Solana's transaction size constraints and how SATI optimizes for them.
49

@@ -335,4 +340,4 @@ console.log(`Transaction size: ${txBytes.length} bytes`);
335340
- [Solana Transaction Size Limits](https://solana.com/docs/core/transactions#transaction-size)
336341
- [Address Lookup Tables](https://solana.com/docs/advanced/lookup-tables)
337342
- [Ed25519 Instruction Format](https://docs.solanalabs.com/runtime/programs#ed25519-program)
338-
- [SATI Specification](./specification.md)
343+
- [SATI Specification](/specification)

docs/benchmarks/README.md

Lines changed: 0 additions & 76 deletions
This file was deleted.

0 commit comments

Comments
 (0)