openvibers/openvibe.exchange AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Exchange |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | "Every AI model, one account." Exchange is the network's AI front: every model OpenVibe can run with the |
| 6 | live price, speed and uptime of every offer that serves it, one chat that puts up to three models side by |
| 7 | side, the OpenAI- and Anthropic-compatible endpoint, community machines, the uncensored tier, companions |
| 8 | and the AI arena. It **reads OpenVibe.AI** (catalog, aggregate usage, and the runs a chat makes for a |
| 9 | person) and keeps only two things of its own: a person's chats and their 18+ confirmation. Node.js/ |
| 10 | Express, PostgreSQL (PGlite in development), CommonJS, **no build step**. |
| 11 | |
| 12 | - Service id `exchange`, port `5150`, origin `https://openvibe.exchange`, unit |
| 13 | `openvibe-exchange.service`, release root `/opt/openvibe.exchange`. |
| 14 | - The v1 marketplace is retired: `/api/v1/*` answers `410`, and its tables stay as they are, read-only. |
| 15 | Every old chat row remains for a one-time import into Chat. |
| 16 | |
| 17 | ## What it owns |
| 18 | |
| 19 | - The pages and their live numbers: `/` (every model, popular, cheapest, uncensored, community machines, |
| 20 | endpoint), `/models`, `/m/<org>/<model>` (price in/out/cached per million, p50/p95, uptime, data policy, |
| 21 | Try it, curl and the SDK line), `/api` (endpoint, model ids, keys, aggregate usage), `/start`, `/why` |
| 22 | (a real run's explain), `/faq`, `/sell` (Link machines and pools; what is **not** sold), `/uncensored`, |
| 23 | `/ai`, `/status`. |
| 24 | - A person's chats (`server/chat/store.js`) and their 18+ confirmation; companions, lorebooks, notes, |
| 25 | memory and the Locker; the arena and compare surfaces; the deep-dive/analysis pages. |
| 26 | - The one-time import contract: `GET /internal/exchange/chat/export` requires Chat's service token with |
| 27 | `exchange.chat.export` and the authenticated person's `x-ov-subject`; it returns only that person's |
| 28 | unsealed chats. |
| 29 | - The `/chat` redirect: 301 to `https://openvibe.chat/vibe`, carrying `?models=` and mapping old `@vN` |
| 30 | model ids. Model pages' Try it links go there directly. |
| 31 | - Its own PostgreSQL state, migrations and account-data receipts. |
| 32 | |
| 33 | Not owned: the catalog/prices/usage and the model runs (AI), the models and provider keys (AI/Network), |
| 34 | identity, chat itself (Chat), ratings (Bench), machines and pools (Network/Link), money (Billing), Media. |
| 35 | |
| 36 | ## What it reads (AI and Network) |
| 37 | |
| 38 | - `GET https://ai.openvibe.services/api/v1/exchange/catalog` — models grouped, with every live offer: |
| 39 | price (rate cards), measured speed/uptime, `metadata.data_policy` where recorded. Prepaid capacity has |
| 40 | no price (`free: true`); **BYOK and restricted capacity are never an offer**. |
| 41 | - `GET .../api/v1/exchange/usage?days=30` — aggregate requests, tokens and spend by day/model/product |
| 42 | (`usage_daily`; never a prompt, answer or person). |
| 43 | - `POST .../api/v1/runs` with `workflow: ai.complete` and `on_behalf_of` — one chat turn or Try it, on the |
| 44 | person's allowance, with Exchange's token (`ai.run.create`, namespace `ai.complete`). |
| 45 | - `GET .../api/v1/community/offers` — live community offers; a pool says **how many** machines serve it, |
| 46 | never which ones ("Community pool (N machines)"). |
| 47 | - `GET https://openvibe.network/api/v1/machines` and `/machine-pools` — the person's machines, pools and |
| 48 | poolable machines, with their Network token (names, free VRAM/RAM, local network key, approvals, |
| 49 | readiness); `POST /api/v1/machine-pools` proposes, `.../:id/revoke` stops. Link measures links and fit; |
| 50 | Exchange shows what Network carries and says "not measured yet" when it carries nothing. |
| 51 | - AI unreachable is **said, not papered over**: the catalog read keeps the last good copy for five minutes |
| 52 | and pages carry an honest notice; no number is invented. |
| 53 | |
| 54 | ## Repository map |
| 55 | |
| 56 | - **Entry:** [server/index.js](server/index.js) — `createApp()`, listen, graceful stop. |
| 57 | - **App:** [server/app.js](server/app.js) — helmet/CSP, `/auth`, `/internal/events`, `/shared`, |
| 58 | `/api/v1/arena`, `/api/v1/analysis`, `/api/v1/compare`, `/api/v1`, pages, machine endpoints. |
| 59 | - **Reads:** [server/ai.js](server/ai.js), [server/network.js](server/network.js), |
| 60 | [server/bench.js](server/bench.js), [server/compare.js](server/compare.js), |
| 61 | [server/model-directory.js](server/model-directory.js), [server/model-search.js](server/model-search.js), |
| 62 | [server/compute-prices.js](server/compute-prices.js), [server/catalog-history.js](server/catalog-history.js), |
| 63 | [server/analysis.js](server/analysis.js). |
| 64 | - **Person state:** [server/chat/store.js](server/chat/store.js), [server/companions/](server/companions/) |
| 65 | (`store.js`, `group.js`, `voice.js`, `adult.js`, `memory.js`, `lorebook.js`, `locker.js`, `taking-notes.js`), |
| 66 | [server/actor.js](server/actor.js), [server/sharing.js](server/sharing.js). |
| 67 | - **Arena:** [server/arena/store.js](server/arena/store.js), `category.js`; HTTP in |
| 68 | [server/http/arena.js](server/http/arena.js), `compare.js`, `compare-site.js`, `analysis.js`, |
| 69 | `deep-dive.js`, `dashboard.js`, `search.js`. |
| 70 | - **Pages/API:** [server/http/pages.js](server/http/pages.js), `api.js`, `console.js`, `companions.js`, |
| 71 | `principal.js`, `router.js`, [server/openapi.js](server/openapi.js); [server/render/](server/render/). |
| 72 | - **Config/db:** [server/config.js](server/config.js), [server/db.js](server/db.js). |
| 73 | - **Deploy:** [deploy/systemd/openvibe-exchange.service](deploy/systemd/openvibe-exchange.service), |
| 74 | [deploy/nginx/openvibe.exchange.conf](deploy/nginx/openvibe.exchange.conf). |
| 75 | |
| 76 | ## Commands |
| 77 | |
| 78 | ```bash |
| 79 | npm ci |
| 80 | npm start # development: PGlite in data/pglite, http://localhost:5150 |
| 81 | npm test # test/run.js, PGlite, mock Network/AI |
| 82 | npm test -- pages chat retirement # filter by word |
| 83 | npm run test:pg # same on PostgreSQL (EXCHANGE_TEST_STORE=pg) |
| 84 | npm run seed:proof # scripts/seed-proof.js |
| 85 | ov test # the harness's sharded run of what the change reaches |
| 86 | ``` |
| 87 | |
| 88 | `DATABASE_URL` serves, `DATABASE_DIRECT_URL` migrates; `OV_OAUTH_CLIENT_SECRET` makes sign-in and the AI |
| 89 | client-credentials token work. See `.env.example`. |
| 90 | |
| 91 | ## Database and migrations |
| 92 | |
| 93 | Exchange's own PostgreSQL database (ADR-035; PGlite under `data/pglite` in development). Async through |
| 94 | `openvibe-sdk/db`; migrations `migrations/NNNN_*.sql` applied at boot; never edit an applied migration, add |
| 95 | a new number. **Exchange v1's tables are read-only**: nothing is written to them any more, and the old chat |
| 96 | rows exist only for the one-time import. A new column for the current product goes in a new migration. |
| 97 | |
| 98 | ## Auth, privacy and the 18+ gate |
| 99 | |
| 100 | - People sign in through Network SSO (OAuth client `exchange`, PKCE). Exchange's AI calls use its own |
| 101 | client-credentials token with `on_behalf_of` the person, so the run is theirs (allowance, receipt). |
| 102 | - A page rendered for a signed-in person is `private, no-store`; a public page is `public, max-age=60`. |
| 103 | Do not cache a signed-in page publicly. |
| 104 | - The 18+ confirmation is on the **account**, not a cookie: `/uncensored` explains the abliterated models |
| 105 | and the hard floor. Do not move the gate into client state. |
| 106 | - The old-chat export is service-only: Chat's token (`exchange.chat.export`) plus `x-ov-subject`; it |
| 107 | returns only that person's unsealed chats. Never expose it publicly. |
| 108 | - Service callers need a Network service token audience `openvibe.exchange` and the route's capability; |
| 109 | account export/deletion goes through `/internal/events`. |
| 110 | |
| 111 | ## Frontend loading model |
| 112 | |
| 113 | Every page is **server-rendered with `openvibe-shared/kit` and complete without JavaScript**; there is no |
| 114 | SPA, bundler or transpiler. `openvibe-shared` supplies the Frame/kit/legal/SEO and `/shared/*` from the |
| 115 | pinned node_modules. The pages negotiate cleanly for agents (JSON/llms) and their numbers come from the |
| 116 | same AI reads. CSP is in [server/app.js](server/app.js). |
| 117 | |
| 118 | ## Deploy |
| 119 | |
| 120 | Unit [deploy/systemd/openvibe-exchange.service](deploy/systemd/openvibe-exchange.service) runs |
| 121 | `/opt/openvibe.exchange/server/index.js` on `127.0.0.1:5150`, env `/etc/openvibe/exchange.env`; vhost |
| 122 | [deploy/nginx/openvibe.exchange.conf](deploy/nginx/openvibe.exchange.conf). The README does not document |
| 123 | an `ovhost deploy exchange` command — use the host's own service installer. Migrations run at boot; no |
| 124 | secret in a command line, file or log. |
| 125 | |
| 126 | ## Tests |
| 127 | |
| 128 | `npm test` runs each `test/*.test.js` in its own process with PGlite and in-process Network/AI mocks. It |
| 129 | pins the pages and their numbers, the catalog/usage reads and the "last good copy for five minutes" rule, |
| 130 | the `/chat` redirect and old `@vN` mapping, the v1 retirement (`410`), the 18+ gate, companions/arena/ |
| 131 | compare, the internal chat export (subject-scoped), account export/deletion and discovery. `ov test` runs |
| 132 | the same. |
| 133 | |
| 134 | ## Conventions |
| 135 | |
| 136 | - CommonJS, 4-space indent, single quotes, semicolons; reads live in `server/ai.js`/`server/network.js`, |
| 137 | person state in `server/*/store.js`, HTTP in `server/http/`. |
| 138 | - A number shown to a person comes from AI or Network and carries its freshness; a missing number is said, |
| 139 | never defaulted. Prices are per million tokens with the currency and the offer. |
| 140 | - Errors are problem+json with stable codes; an AI/Network failure is a clear notice, not a 500 page. |
| 141 | - The v1 surface is retired: do not revive `/api/v1` marketplace routes; keep the `410`. |
| 142 | |
| 143 | ## Pitfalls |
| 144 | |
| 145 | - **Never invent a price, a model or an uptime.** If AI does not answer, keep the last good catalog for at |
| 146 | most five minutes and show the notice; a page with no data says so. |
| 147 | - **BYOK and restricted capacity are not offers**, and prepaid capacity is `free: true`; do not list them |
| 148 | as purchasable. |
| 149 | - **A community pool never names its machines**, only the count. |
| 150 | - **Person state is private:** chats, companions, notes and the 18+ flag are per subject; a signed-in page |
| 151 | is `no-store`. The old-chat export is the only cross-service read and is subject-scoped by Chat. |
| 152 | - **Do not write Exchange v1's tables.** They are the migration's archive; the old chat rows are for the |
| 153 | one-time import only. |
| 154 | - **`/chat` is a 301 to Chat/Vibe** with `?models=` and the old `@vN` mapping; do not rebuild a local chat |
| 155 | page. |
| 156 | - **Money is AI's/Billing's**; Exchange charges a turn to the person's allowance through AI, never a local |
| 157 | ledger. |
| 158 | - **No fabricated "selling compute"**: `/sell` says plainly what is not sold (personal plans, personal API |
| 159 | keys); machines/pools are Network's truth. |
| 160 | - **A unit description that says "trade what people make" is stale** (the remake is the AI front); do not |
| 161 | use it as the product description. |
| 162 | |
| 163 | ## Documentation |
| 164 | |
| 165 | - [README.md](README.md) — pages, what it reads, money/free use, the retired v1, run. |
| 166 | - [STATUS.json](STATUS.json) — the remake's state. |
| 167 | - [docs/](docs/) — platform review, shared analysis, C2 notes and screenshots. |
| 168 | - OpenVibe.Contracts holds `exchange.*`/`ai.*` shapes; AI and Network own their upstream APIs. |
| 169 |