Skip to content

Commit 44bf1d6

Browse files
committed
docs: fix x402 guide verification section and wallet adapter imports
- Use listFeedbacks (has taskRef) instead of searchFeedback (doesn't) - Add missing hexToBytes import in server-side wallet adapter code - Both verified against actual SDK types and x402 API source
1 parent bcd25b9 commit 44bf1d6

2 files changed

Lines changed: 242 additions & 38 deletions

File tree

docs/guides/x402-feedback.md

Lines changed: 152 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,159 @@ description: Link feedback directly to x402 payment transactions
55

66
# x402 Payment Feedback
77

8-
::: warning Coming Soon
9-
This guide is under active development. SATI is designed as the feedback extension for [x402](https://x402.org) payments (see [PR #1024](https://github.com/coinbase/x402/pull/1024)), but the end-to-end integration guide is not yet ready.
10-
:::
8+
SATI is designed as the feedback extension for [x402](https://x402.org) payments. The payment transaction signature becomes the `taskRef`, creating a cryptographic link between payment and feedback.
119

12-
## What This Will Cover
10+
## How It Works
1311

14-
- Linking feedback attestations to x402 payment transaction hashes
15-
- The payment-as-taskRef model: the payment tx becomes the deterministic reference for feedback
16-
- Who pays for on-chain submission (agent pays for positive, client pays for negative)
17-
- Integrating SATI feedback into x402 seller and buyer flows
12+
1. Client pays for a service via x402 (HTTP 402 flow)
13+
2. Facilitator settles the payment on Solana, producing a transaction signature
14+
3. After service delivery, the facilitator (or client) submits SATI feedback with the payment signature as `taskRef`
15+
4. Feedback is permanently linked to the payment transaction on-chain
1816

19-
## In the Meantime
17+
## taskRef Derivation
2018

21-
- Read [How It Works](/how-it-works) to understand blind feedback and proof of participation
22-
- See the [Agent Marketplace](/guides/agent-marketplace) guide for the general feedback flow
23-
- Check the [sati-agent0-sdk reference](/reference/sati-agent0-sdk) for `giveFeedback` and `prepareFeedback` API details
24-
- Follow progress on [GitHub](https://github.com/cascade-protocol/sati)
19+
SATI's `taskRef` is 32 bytes. Solana transaction signatures are 64 bytes. To link them, hash the signature:
20+
21+
```typescript
22+
import { keccak_256 } from "@noble/hashes/sha3";
23+
import bs58 from "bs58";
24+
25+
// x402 settlement gives you a base58 transaction signature
26+
const txSignature = settlementResponse.transaction; // e.g. "5Kj..."
27+
28+
// Hash to 32 bytes for SATI taskRef
29+
const sigBytes = bs58.decode(txSignature);
30+
const taskRef = keccak_256(sigBytes);
31+
```
32+
33+
This is deterministic - the same payment signature always produces the same `taskRef`. Anyone with the payment tx can verify the link.
34+
35+
## Facilitator Integration
36+
37+
The facilitator is the natural feedback manager - already in the payment flow, trusted by both parties. Use x402's `onAfterSettle` lifecycle hook:
38+
39+
```typescript
40+
import { x402ResourceServer } from "@x402/core";
41+
import { Sati, Outcome, address } from "@cascade-fyi/sati-sdk";
42+
import { keccak_256 } from "@noble/hashes/sha3";
43+
import { createKeyPairSignerFromBytes } from "@solana/kit";
44+
import bs58 from "bs58";
45+
46+
const sati = new Sati({ network: "mainnet" });
47+
const facilitatorPayer = await createKeyPairSignerFromBytes(keypairBytes);
48+
49+
const server = new x402ResourceServer(facilitatorClient);
50+
51+
server.onAfterSettle(async (context) => {
52+
const txSignature = context.result.transaction;
53+
const sigBytes = bs58.decode(txSignature);
54+
const taskRef = keccak_256(sigBytes);
55+
56+
// The facilitator's payTo address is the agent's payment wallet.
57+
// Map it to the agent's SATI mint address (your registry lookup).
58+
const agentMint = await lookupAgentMint(context.requirements.payTo);
59+
60+
if (agentMint) {
61+
await sati.giveFeedback({
62+
payer: facilitatorPayer,
63+
agentMint: address(agentMint),
64+
taskRef,
65+
outcome: Outcome.Positive,
66+
value: 100,
67+
tag1: "x402",
68+
endpoint: context.paymentPayload.resource?.url,
69+
});
70+
}
71+
});
72+
```
73+
74+
## Client-Side Feedback (After Service Delivery)
75+
76+
If the client rates the service after receiving it, pass the payment tx as `taskRef`:
77+
78+
```typescript
79+
import { Sati, Outcome, address } from "@cascade-fyi/sati-sdk";
80+
import { keccak_256 } from "@noble/hashes/sha3";
81+
import bs58 from "bs58";
82+
83+
const sati = new Sati({ network: "mainnet" });
84+
85+
// paymentTxSignature from the x402 settlement response
86+
const taskRef = keccak_256(bs58.decode(paymentTxSignature));
87+
88+
await sati.giveFeedback({
89+
payer: clientKeypair,
90+
agentMint: address("AgentMint..."),
91+
taskRef,
92+
outcome: Outcome.Positive,
93+
value: 85,
94+
tag1: "starred",
95+
message: "Fast response, accurate results",
96+
});
97+
```
98+
99+
## Browser Wallet Flow (Marketplace)
100+
101+
For marketplaces where the user rates via browser wallet after an x402 purchase:
102+
103+
```typescript
104+
// Server: prepare feedback with the payment's taskRef
105+
const taskRef = keccak_256(bs58.decode(paymentTxSignature));
106+
107+
const prepared = await sati.prepareFeedback({
108+
counterparty: address(userWalletAddress),
109+
agentMint: address("AgentMint..."),
110+
taskRef,
111+
outcome: Outcome.Positive,
112+
value: 85,
113+
tag1: "starred",
114+
});
115+
116+
// Send messageBytes to frontend for signing (see Wallet Adapter section in SKILL.md)
117+
```
118+
119+
## Verifying Payment-Feedback Links
120+
121+
Given a feedback attestation, verify it links to a specific payment:
122+
123+
```typescript
124+
const result = await sati.listFeedbacks({
125+
agentMint: address("AgentMint...") as Address,
126+
});
127+
128+
const expectedTaskRef = keccak_256(bs58.decode(paymentTxSignature));
129+
130+
for (const fb of result.items) {
131+
// fb.data.taskRef is the 32-byte hash stored on-chain
132+
const ref = fb.data.taskRef;
133+
const matches = ref.length === expectedTaskRef.length &&
134+
ref.every((b, i) => b === expectedTaskRef[i]);
135+
}
136+
```
137+
138+
## REST API
139+
140+
Submit feedback with a payment link via the REST API (no wallet needed):
141+
142+
```bash
143+
curl -X POST https://sati.cascade.fyi/api/feedback \
144+
-H "Content-Type: application/json" \
145+
-d '{
146+
"agentMint": "AgentMint...",
147+
"value": 85,
148+
"tag1": "x402",
149+
"outcome": 2,
150+
"message": "Paid via x402, service delivered correctly"
151+
}'
152+
```
153+
154+
Note: The REST API generates a random `taskRef` - it does not accept a custom one. For deterministic payment linking, use the SDK.
155+
156+
## Cost Structure
157+
158+
| Operation | Cost |
159+
|-----------|------|
160+
| Feedback attestation | ~0.00001 SOL (ZK compressed) |
161+
| x402 payment (separate) | Varies by facilitator |
162+
163+
Feedback costs are negligible compared to the x402 payment itself. Facilitators can batch feedback submissions to reduce per-transaction overhead.

skills/sati/SKILL.md

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -230,37 +230,102 @@ For proof-of-participation, use the **FeedbackV1** schema (DualSignature mode).
230230

231231
The platform server prepares a SIWS (Sign In With Solana) message, the user signs it in their browser wallet, and the platform submits the transaction.
232232

233+
Uses `@solana/wallet-adapter-react` (works with Phantom, Solflare, Backpack, and any wallet implementing the Wallet Standard `signMessage` feature).
234+
235+
```bash
236+
npm install @solana/wallet-adapter-react @solana/wallet-adapter-wallets @solana/wallet-adapter-react-ui
237+
```
238+
239+
**Server (API route):**
240+
233241
```typescript
242+
import { Sati, Outcome, address, bytesToHex, hexToBytes } from "@cascade-fyi/sati-sdk";
243+
244+
const sati = new Sati({ network: "mainnet" });
245+
246+
// POST /api/prepare-feedback
247+
async function handlePrepare(req) {
248+
const { walletAddress, agentMint, value, tag1, outcome } = req.body;
249+
250+
const prepared = await sati.prepareFeedback({
251+
counterparty: address(walletAddress),
252+
agentMint: address(agentMint),
253+
outcome: outcome ?? Outcome.Positive,
254+
value,
255+
tag1,
256+
});
257+
258+
// Store `prepared` server-side (e.g. in session or cache keyed by walletAddress + agentMint)
259+
await cache.set(`feedback:${walletAddress}:${agentMint}`, prepared);
260+
261+
// Only send the SIWS message bytes to the frontend
262+
return { messageHex: bytesToHex(prepared.messageBytes) };
263+
}
264+
265+
// POST /api/submit-feedback
266+
async function handleSubmit(req) {
267+
const { walletAddress, agentMint, signatureHex } = req.body;
268+
269+
const prepared = await cache.get(`feedback:${walletAddress}:${agentMint}`);
270+
const result = await sati.submitPreparedFeedback({
271+
payer: platformPayer,
272+
prepared,
273+
counterpartySignature: hexToBytes(signatureHex),
274+
});
275+
276+
return { signature: result.signature, attestationAddress: result.attestationAddress };
277+
}
278+
```
279+
280+
**Frontend (React component):**
281+
282+
```tsx
283+
import { useWallet } from "@solana/wallet-adapter-react";
234284
import { hexToBytes, bytesToHex } from "@cascade-fyi/sati-sdk";
235285

236-
// Step 1: Server prepares feedback data
237-
const prepared = await sati.prepareFeedback({
238-
counterparty: address("UserWallet..."),
239-
agentMint: address("Agent..."),
240-
outcome: Outcome.Positive,
241-
value: 90,
242-
tag1: "starred",
243-
});
286+
function FeedbackButton({ agentMint }: { agentMint: string }) {
287+
const { publicKey, signMessage, connected } = useWallet();
288+
289+
async function handleFeedback() {
290+
if (!publicKey || !signMessage) return;
291+
292+
// 1. Server prepares the SIWS message
293+
const { messageHex } = await fetch("/api/prepare-feedback", {
294+
method: "POST",
295+
headers: { "Content-Type": "application/json" },
296+
body: JSON.stringify({
297+
walletAddress: publicKey.toBase58(),
298+
agentMint,
299+
value: 85,
300+
tag1: "starred",
301+
}),
302+
}).then((r) => r.json());
303+
304+
// 2. User signs with wallet (Phantom/Solflare popup)
305+
const messageBytes = hexToBytes(messageHex);
306+
const signature = await signMessage(messageBytes); // Returns Uint8Array (64-byte Ed25519)
307+
308+
// 3. Server submits the transaction
309+
await fetch("/api/submit-feedback", {
310+
method: "POST",
311+
headers: { "Content-Type": "application/json" },
312+
body: JSON.stringify({
313+
walletAddress: publicKey.toBase58(),
314+
agentMint,
315+
signatureHex: bytesToHex(signature),
316+
}),
317+
});
318+
}
244319

245-
// Step 2: Send to frontend (only messageBytes needs to cross the wire)
246-
// The server keeps the full `prepared` object; only send what the frontend needs to sign.
247-
const payload = {
248-
messageHex: bytesToHex(prepared.messageBytes),
249-
};
250-
251-
// Step 3: Frontend - user signs with wallet adapter
252-
const messageBytes = hexToBytes(payload.messageHex);
253-
const signature = await wallet.signMessage(messageBytes); // Returns Uint8Array (Ed25519 signature)
254-
255-
// Step 4: Server submits with platform payer (uses the full `prepared` object kept server-side)
256-
const result = await sati.submitPreparedFeedback({
257-
payer: platformPayer, // Platform pays gas
258-
prepared, // Full PreparedFeedbackData from step 1
259-
counterpartySignature: signature, // User's 64-byte Ed25519 signature
260-
});
320+
return (
321+
<button onClick={handleFeedback} disabled={!connected}>
322+
Rate Agent
323+
</button>
324+
);
325+
}
261326
```
262327

263-
> **Note:** `PreparedFeedbackData` contains multiple `Uint8Array` fields (`messageBytes`, `taskRef`, `dataHash`, `content`). If you need to serialize the entire object to JSON (e.g. for a stateless API), convert all `Uint8Array` fields with `bytesToHex()` and restore with `hexToBytes()`. The recommended pattern above avoids this by keeping `prepared` server-side.
328+
> **Note:** `signMessage` is `undefined` if the connected wallet doesn't support message signing. Always check `signMessage` before calling it. `PreparedFeedbackData` contains multiple `Uint8Array` fields (`messageBytes`, `taskRef`, `dataHash`, `content`). If you need to serialize the entire object to JSON (e.g. for a stateless API), convert all `Uint8Array` fields with `bytesToHex()` and restore with `hexToBytes()`. The recommended pattern above avoids this by keeping `prepared` server-side.
264329
265330
#### Revoke feedback
266331

0 commit comments

Comments
 (0)