Skip to content

Commit ca09dfa

Browse files
authored
feat(examples): read and save Perplexity data with capped Moksha payments (#102)
* feat(examples): read and save Perplexity data with capped Moksha payments * fix(examples): reject stale Perplexity hydration results * chore: verify reader against merged main
1 parent ef97c06 commit ca09dfa

20 files changed

Lines changed: 1668 additions & 264 deletions

File tree

‎.github/workflows/perplexity-demo.yml‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ on:
1515
- "tsconfig*.json"
1616
- ".github/workflows/perplexity-demo.yml"
1717
pull_request:
18-
branches: [main, develop]
1918
paths:
2019
- "examples/perplexity-demo/**"
2120
- "test/examples/perplexity-demo.test.ts"
@@ -50,5 +49,5 @@ jobs:
5049
node-version: 22.23.4
5150
cache: pnpm
5251
- run: pnpm install --frozen-lockfile
53-
- run: pnpm test test/examples/perplexity-demo.test.ts test/examples/nextjs-starter.test.ts
52+
- run: pnpm test test/examples/perplexity-demo.test.ts test/examples/nextjs-starter.test.ts examples/perplexity-demo/src/components/ConnectFlow.test.js
5453
- run: pnpm --filter perplexity-demo build

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ coverage/
1111
.env
1212
.env.local
1313
.next/
14+
.research-shelf/
1415
*.log
1516
__pycache__/
1617
.claude

‎examples/perplexity-demo/.env.local.example‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ APP_URL=http://localhost:3090
77
# Production Vana hosts with an explicitly fixed Moksha network.
88
# dev selects app-dev.vana.org; it still uses Moksha.
99
VANA_ENV=prod
10+
11+
# Explicit opt-in and both caps are required; unset means no payment.
12+
VANA_ALLOW_MOKSHA_PAYMENT=false
13+
VANA_MAX_FEE=0.02
14+
VANA_MAX_TOTAL=0.04
15+
# Optional private local persistence directory, never public/static storage.
16+
# VANA_SHELF_DIRECTORY=/private/path/research-shelf
17+
# Existing local dev Web approval only, not a new consent or source login.
18+
# VANA_APPROVAL_URL=http://localhost:3083
19+
# VANA_RESUME_REQUEST_ID=dcr_...

‎examples/perplexity-demo/README.md‎

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Perplexity demo — Research Shelf
22

3-
A separate Next.js example based on the ChatGPT starter. It creates signed Vana requests for `perplexity.conversations` on Moksha and shows the connection state beside an empty thread-list view. The original `nextjs-starter` is unchanged.
3+
A separate Next.js example based on the existing starter. It requests `perplexity.conversations`, reads through the app's own Moksha grant, saves conversation titles and update times locally, renders them, then acknowledges delivery. The original ChatGPT `nextjs-starter` is unchanged.
44

5-
This is BUI-929's connection/setup slice. `/api/data` returns HTTP 501 with `read_not_enabled`: it does not read data, authorize payment, persist threads or acknowledge delivery. The controller also refuses every Personal Server 402 before the SDK can sign a payment. The next slice, BUI-931, supplies the bounded paid read, save and display. BUI-932 supplies Read latest through the same grant after a connector edit. No fabricated conversation data is shown.
5+
The server refuses payments by default. Explicit Moksha opt-in, a per-read cap and a total cap are all required before it can transmit a payment, including replay of an existing signed receipt. No owner-key read, Mainnet payment, automatic faucet, Desktop dependency or Registry work is included.
66

77
## Choose one app identity
88

@@ -65,7 +65,7 @@ vana app escrow fund --network moksha --amount 1
6565
vana app escrow balance --network moksha --json
6666
```
6767

68-
Use the normal escrow path and check that the deposit is finalized with positive available balance. Do not use an owner-key shortcut or disable fees. A future paid read must check the current fee quote and a configured cap; a funded balance is not itself proof of successful app delivery. This version deliberately does not spend escrow.
68+
Use the normal escrow path and check that the deposit is finalized with positive available balance. Do not use an owner-key shortcut or disable fees. A future paid read must check the current fee quote and a configured cap; a funded balance is not itself proof of successful app delivery. Reading may spend test VANA only after the opt-in and caps below are configured.
6969

7070
## Run and review
7171

@@ -80,13 +80,52 @@ Open `http://localhost:3090` in a desktop browser. **Connect with Vana** signs a
8080

8181
Both `dev` and `start` bind to `127.0.0.1`. After building, use `pnpm --filter perplexity-demo start` to review the production build on the same local URL.
8282

83-
The Web owner-only unfamiliar-source approval is BUI-924's separate dependency. If Vana cannot approve Perplexity yet, keep the refusal visible; do not manufacture consent, substitute a public source or use owner credentials. After approval becomes available, this version still stops with “Data reads are not enabled” and leaves the thread list empty. Mobile/Desktop app handoffs, the public Registry and connector publication are outside this example.
83+
## Enable bounded test reads
84+
85+
Add server-only configuration deliberately:
86+
87+
```dotenv
88+
VANA_ALLOW_MOKSHA_PAYMENT=true
89+
VANA_MAX_FEE=0.02
90+
VANA_MAX_TOTAL=0.04
91+
```
92+
93+
Amounts are native Moksha VANA (18 decimals). These examples permit the observed 0.011 quote; recheck the current fee and finalized available escrow before choosing limits. `VANA_MAX_TOTAL` is a conservative authorization budget for each app/environment/owner/grant/scope ledger, including uncertain transmissions. It is not a chain accounting total. Reconnecting a browser does not reset it. Lowering or disabling the policy also blocks transmission of retained payment headers.
94+
95+
The example supports the selected pinned server's `grant` challenge. A changed payment scheme, network, asset, invalid challenge, increased quote, insufficient finalized escrow or cap violation stops before signing. Transport retries are disabled. A payment is persisted before transmission; the received server envelope is also saved privately before decoding into titles; an uncertain response retains and replays the same signed header instead of signing another charge for the same operation. New connector data versions can require a new bounded authorization.
96+
97+
`VANA_SHELF_DIRECTORY` selects a **private local** writable directory (default `.research-shelf`, gitignored). Files contain saved titles, trusted grant/server bindings and signed payment receipts. Directories use mode 0700, files 0600 and writes use fsync plus atomic rename. Do not publish this directory or its contents. The browser receives an opaque HttpOnly cookie and saved titles, never private keys, payment headers or a browser-selectable grant/server URL. This local single-owner example is not hosted multi-user authentication.
98+
99+
App/environment changes invalidate the session. The app rechecks live grant ownership, scope, expiry/revocation and the registered owner-bound server before a new read. A failed read preserves the earlier saved list. `/api/acknowledge` only accepts the exact saved revision; the UI invokes it after that list commits to the DOM. Confirmation failure offers a separate retry without rereading or repaying.
100+
101+
For an existing **local dev** approval test, set `VANA_APPROVAL_URL=http://localhost:3083` with `VANA_ENV=dev`. `VANA_RESUME_REQUEST_ID=dcr_...` may resume a deliberately selected existing approval server-side, checking its live grant/binding first; it does not mint consent or bypass a pending/refused approval. Leave it unset for normal Connect/new-request behavior.
102+
103+
### Interrupted-process recovery
104+
105+
The store fails closed if an action lock survives a process crash. Each `.lock` file records its process ID and creation time. Stop the owned demo process, confirm the recorded PID no longer exists and no other process is serving this store, then remove **only that abandoned `.lock` file** before restarting. Retain the connection and payment JSON files: deleting those would lose uncertain-payment protection and spending history. A live/unknown owner is not permission to clear a lock. Errors caught by a running process release their lock normally.
106+
107+
The Web owner-only unfamiliar-source approval is BUI-924's separate dependency. If Vana cannot approve Perplexity yet, keep the refusal visible; do not manufacture consent, substitute a public source or use owner credentials. The result is complete only when real conversations are saved and rendered and Vana receives acknowledgement. Mobile/Desktop app handoffs, the public Registry and connector publication are outside this example.
84108

85109
## Verify
86110

87111
```sh
88-
pnpm test test/examples/perplexity-demo.test.ts test/examples/nextjs-starter.test.ts
112+
pnpm test test/examples/perplexity-demo.test.ts test/examples/nextjs-starter.test.ts examples/perplexity-demo/src/components/ConnectFlow.test.js
89113
pnpm --filter perplexity-demo build
90114
```
91115

92-
Route tests use local HTTP fixtures with the real SDK and a public Hardhat test identity. They verify the signed app identity, exact conversations-only Moksha request, deployment selection, key/address validation, status polling and the no-read/no-payment boundary. They do not establish live owner consent, paid delivery or the final iteration loop.
116+
Route tests use fixed local HTTP responses with the real SDK and public Hardhat identity. They verify normal app-key payment signatures, spending and grant refusals, reconnect/restart receipt recovery, actual private-file persistence, no acknowledgement before saving, and separate acknowledgement retry. They do not establish live provider collection, real paid delivery, browser rendering or the changed-output loop.
117+
118+
The component regression renders the actual connection flow with delayed synthetic responses. It checks that a late saved-data snapshot cannot replace a newer delivered result, while ordinary saved-data restoration still works.
119+
120+
## Live proof: distinct outcomes
121+
122+
Use the maintained [connection verification evidence rules](https://github.com/vana-com/unity-surfaces/blob/a87843f90b9b570ce493e769d348b1a869a9ebae/.agents/skills/us-verify-data-connection-workflow/references/evidence-and-reruns.md) from merged #1933. Keep one private result per attempt with exact CLI/SDK/server/demo revisions, configuration, request/grant references, timestamps, actions and failures. Record these separately:
123+
124+
1. CLI emitted and synced real conversations.
125+
2. The app's grant returned the selected scope through the public server path; record the current fee, budget and actual payment/grant status.
126+
3. The app durably saved a recognizable stable thread ID/title/time, then displayed that saved value in its actual browser route.
127+
4. Acknowledgement succeeded after persistence and rendering. `approved`, HTTP 200 or `completed` alone is insufficient proof of all four.
128+
129+
Keep private titles and raw payloads outside Git/Linear. A successful server access log with no saved demo result is a failed consumer-delivery attempt, not end-to-end success. Preserve its receipt and cost; a fresh charged attempt requires an explicit operator decision rather than automatic replacement. Compare saved content to what the browser actually displays, and retain failure attempts rather than relabelling them. A grant's chain finalization and consumer acknowledgement are separate observations.
130+
131+
[Motel composition guidance](https://github.com/vana-com/unity-surfaces/blob/a87843f90b9b570ce493e769d348b1a869a9ebae/.agents/skills/us-verify-with-motel/SKILL.md) is optional diagnosis. Its existing exporter is Desktop-specific and does not capture this example. Use existing request/payment/save evidence first; add app-owned, metadata-only probes only for a concrete unresolved failure. Do not launch Desktop, upgrade Personal Server or build an observability programme for this milestone.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { shelf } from "../../../config.js";
2+
import {
3+
failure,
4+
json,
5+
onlyString,
6+
sameOrigin,
7+
session,
8+
} from "../../../server/http.js";
9+
10+
export async function POST(request: Request) {
11+
try {
12+
sameOrigin(request);
13+
await shelf.acknowledge(
14+
session(request),
15+
await onlyString(request, "revision"),
16+
);
17+
return json({ acknowledged: true });
18+
} catch (error) {
19+
return failure(error);
20+
}
21+
}
Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
import { NextResponse } from "next/server";
2-
import { config, vana } from "../../../config.js";
1+
import { config, vana, shelf } from "../../../config.js";
2+
import {
3+
failure,
4+
json,
5+
sameOrigin,
6+
SESSION_COOKIE,
7+
} from "../../../server/http.js";
38

4-
export async function POST() {
9+
export async function POST(request: Request) {
510
try {
6-
return NextResponse.json(
7-
await vana.createAccessRequest({ returnUrl: config.appUrl }),
8-
{ headers: { "Cache-Control": "no-store" } },
9-
);
10-
} catch {
11-
return NextResponse.json(
12-
{ error: "Failed to create access request" },
13-
{ status: 500 },
14-
);
11+
sameOrigin(request);
12+
const created = await shelf.connect(async () => ({
13+
...(await vana.createAccessRequest({ returnUrl: config.appUrl })),
14+
}));
15+
const response = json(created.request);
16+
response.cookies.set(SESSION_COOKIE, created.token, {
17+
httpOnly: true,
18+
sameSite: "lax",
19+
secure: new URL(config.appUrl).protocol === "https:",
20+
path: "/",
21+
maxAge: 60 * 60 * 24 * 7,
22+
});
23+
return response;
24+
} catch (error) {
25+
return failure(error);
1526
}
1627
}
Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
1-
import { NextResponse } from "next/server";
1+
import { shelf } from "../../../config.js";
2+
import {
3+
failure,
4+
json,
5+
onlyString,
6+
sameOrigin,
7+
session,
8+
} from "../../../server/http.js";
29

3-
/** Data reads and payment authorization belong to the next delivery step. */
4-
export async function POST(): Promise<Response> {
5-
return NextResponse.json(
6-
{
7-
code: "read_not_enabled",
8-
error:
9-
"Data reads are not enabled in this demo yet. No payment has been authorized.",
10-
},
11-
{ status: 501, headers: { "Cache-Control": "no-store" } },
12-
);
10+
export async function GET(request: Request) {
11+
try {
12+
return json(await shelf.snapshot(session(request)));
13+
} catch (error) {
14+
return failure(error);
15+
}
16+
}
17+
18+
export async function POST(request: Request) {
19+
try {
20+
sameOrigin(request);
21+
return json(
22+
await shelf.read(
23+
session(request),
24+
await onlyString(request, "requestId"),
25+
),
26+
);
27+
} catch (error) {
28+
return failure(error);
29+
}
1330
}
Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextResponse } from "next/server";
2-
import { vana } from "../../../config.js";
2+
import { shelf } from "../../../config.js";
3+
import { failure, json, session } from "../../../server/http.js";
34

45
export async function GET(request: Request) {
56
const params = new URL(request.url).searchParams;
@@ -13,13 +14,8 @@ export async function GET(request: Request) {
1314
return NextResponse.json({ error: "Invalid requestId" }, { status: 400 });
1415
}
1516
try {
16-
return NextResponse.json(await vana.getAccessRequestStatus(requestId), {
17-
headers: { "Cache-Control": "no-store" },
18-
});
19-
} catch {
20-
return NextResponse.json(
21-
{ error: "Failed to check access request" },
22-
{ status: 500 },
23-
);
17+
return json(await shelf.status(session(request), requestId));
18+
} catch (error) {
19+
return failure(error);
2420
}
2521
}

‎examples/perplexity-demo/src/app/globals.css‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,24 @@ a:focus-visible {
207207
animation: none;
208208
}
209209
}
210+
211+
.thread-list {
212+
list-style: none;
213+
padding: 0;
214+
max-height: 460px;
215+
overflow-y: auto;
216+
}
217+
.thread-list li {
218+
padding: 12px 0;
219+
border-bottom: 1px solid #27272a;
220+
}
221+
.thread-list span {
222+
display: block;
223+
overflow-wrap: anywhere;
224+
}
225+
.thread-list time {
226+
display: block;
227+
font-size: 12px;
228+
color: #a1a1aa;
229+
margin-top: 4px;
230+
}

‎examples/perplexity-demo/src/app/page.tsx‎

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,9 @@ export default function Home() {
99
Bring your Perplexity conversations into your own app through Vana.
1010
</p>
1111
<ConnectFlow />
12-
<section className="card threads" aria-labelledby="threads-title">
13-
<h2 id="threads-title">Your threads</h2>
14-
<p className="empty-state">No threads loaded.</p>
15-
<p>
16-
Conversation titles and last-updated times will appear here once data
17-
reads are enabled.
18-
</p>
19-
</section>
2012
<p className="scope-note">
21-
This demo prepares approval for Perplexity conversations only. Data
22-
reads and payments are not enabled yet.
13+
Reads use this app’s own grant and capped Moksha test VANA payments.
14+
Your saved list stays available if a later read fails.
2315
</p>
2416
</main>
2517
);

0 commit comments

Comments
 (0)