Docs / Help / Changelog
Help

Changelog

What changed.

2026-11-07: buyer safety and flow#

  • Daily spend cap: new setting spendCapUsdDay (PUT /api/settings, Payments → Daily spend cap; null = no cap, 0 blocks every paid route; negative, non-finite or non-numeric values are 400). A paid route is skipped when the last 24 hours of ledger spend, plus the worst case of requests still in flight, plus this request's worst case (promptEstimate / outCap, price × 1.05) would pass the cap; free and local routes are never limited, and a skipped paid route fails over to them. If every route is skipped for the cap the answer is 402 spend_cap_reached with how to raise it. GET /api/status has spendCap: {usdDay, spentUsd}. Tests: router/test/spendcap.test.mjs.
  • Alerts on Home and Payments: non-blocking banners derived from live state, each with the one action that fixes it: "No ETH for gas" (faucet link and address copy), "Escrow below $1" (add funds), "You used 80% of today's spend cap" and "Daily spend cap reached" (raise the cap). No new polling.
  • Guided funding on Payments: a three-step "Fund this node" stepper (gas from a faucet, USDG: mint on the MOCK testnet token or fund from your wallet, deposit into the escrow), each step done or next with its button, collapsing to one line when complete. The advanced escrow form is unchanged below it.
  • Back up this node (Connect): what lives in the data directory and what losing it costs, its path, whether secrets are encrypted at rest, a checklist, and a download of the public facts only (node id, wallet address, escrow). GET /api/status gains node.dataDir and node.encrypted (a boolean; never a key or the passphrase).

Unreleased#

  • Repos merged (2026-11-07): elyra-app merged into elyra. One product, one set of rules. Auth: wallet sign-in is ONE system, SIWE on /auth/nonce, /auth/verify, /auth/session, /auth/logout (EIP-4361 server-built single-use nonce; cookie elyra_admin v2, HMAC, per-boot key, 12 h, signed out on restart; logout revokes the cookie server-side). Admin if the address is in ELYRA_ADMIN_ADDRESSES, else user. The old /gw/* routes and the elyra_acct cookie no longer exist. ELYRA_GATEWAY=1 (needs the admin host) adds the username profile (GET|PUT /auth/username, name settable once) and gates /pub/chat (401 signin_required, 403 profile_required, admin sessions exempt); ELYRA_PUBLIC_PAY=1 enables the browser pay-per-use session (/pub/paid-chat, pay.js). User sessions never reach /api, /v1, /metrics, /api/events. Deposit cap: one cap, default $20 (depositCapOf(state.provide), provide.maxDepositUsd), enforced in the credit gate, hello, session pay, /api/chain, /api/provide, /pub/chain and the node deposit path (cumulative with what the escrow already holds); on a real network the stricter of it and ELYRA_MAINNET_DEPOSIT_CAP_USD (default 20, invalid never disables, 0 blocks) wins, so raising it on mainnet needs both. Kept from elyra: on-chain registry, spend cap, alerts and funding stepper, private Live feed, dark-first panel, public Market / Playground / Sell app, mainnet deployment. Kept from elyra-app: multi-source provide, relay, hello sanitising and gossip bounds, health probes and failover, the end-user docs pass, and handover/ (handover/secrets.enc is not in git).
  • Public app redesign (Market, Playground, Sell), 2026-11-07: the signed-out page is now a small product: header with Market / Playground / Sell / Docs, theme toggle and wallet button; hash routes (old #sell and #m=model links still work); the chat drawer is replaced by a full-height Playground page (live models only, LIVE / LOCAL / CLEAR badges, streaming with Stop, starter prompts, quota meter with the Visitor or Wallet tier, collapsible route and cost under every answer, honest error states with a next action, history kept only in the visitor's localStorage with a Clear history button); a Sell page with the three steps and the command; a "Paid models need your own node" card instead of any payment flow. The wallet menu separates Connect (reads the address) from Sign in (signInWithWallet, one off-chain signature) with a signed-in chip, Sign out, balances and test tokens. Phones get the nav in a second header row; keyboard focus, skip link, aria-current, AA contrast in both themes. Code split into ES modules under panel/try/. Test: router/test/try-page.test.mjs.
  • Private live feed and the Live view, 2026-11-07: GET /api/events (Server-Sent Events: heartbeat comments, Last-Event-ID / ?since= resume from the ring buffer, 8 streams at most, 30-minute stream life, slow readers dropped) and GET /api/events/recent (last ≤ 200 events), owner only: loopback or an admin session on the admin host, never a wallet user session (router/http/events.mjs, router/core/events.mjs). Events are request (buyer or seller side: peer, model, tokens, cost, time to first token, latency, ok / error / refused with a reason, simulated, the buyer's short wallet when you are the seller on-chain), peer (online, offline, discovered, via registry), voucher (issued, received, redeemed) and alert (spend cap, low gas, upstream down). The ring buffer is memory only, and every event is built from a whitelist: there is no field for a prompt, completion, key, API key or IP, wallets and tx hashes are shortened, secret-shaped values and free-text model names are dropped, and repeated alerts and a stranger's refused requests are throttled. Emitted from consumer/chat.mjs, provider/serve.mjs and receipt.mjs, peers.mjs, registry.mjs, consumer/wallet.mjs, provider/vouchers.mjs, provider/redeem.mjs, upstreams.mjs. Panel: new Live page (Start) with a presence card (peers and round-trip times, who serves you, who buys from you in the last 5 minutes) and a streaming feed (kind badges, SIM badge for the demo market, filter pills, pause and resume, a connection chip that backs off from 1 s to 30 s); the router gained a leave() hook so the stream closes when you leave the page. Tests: router/test/events.test.mjs (sanitizer, canary prompt never in an event, ring bound, resume, listener cleanup, 403/401 access, two real nodes).
  • Wallet sign-in (Sign-In with Ethereum), 2026-11-07: GET /auth/nonce?address=, POST /auth/verify, GET /auth/session, POST /auth/logout on the admin host (router/http/siwe.mjs). The server builds and remembers the EIP-4361 message per single-use 5-minute nonce; EOA signatures only (EIP-1271 is refused with a message). Roles: ELYRA_ADMIN_ADDRESSES (comma list, case-insensitive) = admin (same power as the token); any other address = user (identity and a larger public quota, 403 on /api, /v1, /metrics, never the panel). Remote admin now turns on with ELYRA_ADMIN_HOST + ELYRA_ADMIN_TOKEN or ELYRA_ADMIN_ADDRESSES (token optional then). The session cookie keeps its flags (HttpOnly, Secure, SameSite=Strict, 12 h, per-boot HMAC) and now carries a role (v2.<exp>.<role>.<address>.<mac>; the token login keeps v1). Rate limits: 20 nonces/min per client, 5 failed verifications lock a client for 10 min, 60 lock all. GET /pub/quota -> {tier: "visitor"|"wallet", used, limit, resetsAt}; /pub/chat uses ELYRA_TRY_WALLET_PER_HOUR (default 100) per signed-in address plus a 3x per-IP cap, the daily cap still applies. The login page offers "Sign in with wallet" when an admin address is set and loads /admin/login.js + /admin/auth.js under script-src 'self'; connect-src 'self' (script-free otherwise). New panel/try/auth.js (signInWithWallet, getSession, signOut; EIP-1193, no AppKit dependency). Docs: new docs/AUTH.md (threat model), docs/REMOTE-ADMIN.md, docs/CONFIGURATION.md. Tests: router/test/siwe.test.mjs (22). Paid usage for visitors is not built.
  • On-chain provider registry (2026-11-07; deployed on testnet and mainnet, addresses in docs/STATUS.md): new ElyraRegistry contract (announce/revoke, latest entry per wallet, TTL 1 h to 30 d, URL ≤ 256 bytes, optional USDG bond with the escrow's delayed single-use withdraw, no slashing, no admin keys) and router/core/registry.mjs. Buyers read Announced/Revoked logs from a persisted block cursor (bounded span and rate, fail closed) and feed each URL through the same path as gossip-learned peers: public-host check, signed hello, plus the announced nodeId, boxKey, wallet and metaHash must match the hello (commitment defined in docs/PROTOCOL.md). Sellers can opt in to announce (ELYRA_REGISTRY_ANNOUNCE=1, needs gas, never blocks boot). New: ELYRA_REGISTRY, ELYRA_REGISTRY_BLOCK, ELYRA_REGISTRY_DISCOVER, ELYRA_REGISTRY_POLL_MS, GET /api/registry, POST /api/registry/revoke, settings registryDiscover/registryAnnounce, node contracts/deploy.mjs --registry [--bond <usd> --token 0x…], panel badge "from registry" on the Network peer list. The Announced event also carries boxKey (a gap in the first design). Tests: contracts/test/ElyraRegistry.test.cjs, router/test/registry.test.mjs (a buyer with no seed finds a seller only through the registry).
  • Wallet accounts (opt-in ELYRA_GATEWAY=1, needs the admin host and the public page), merged onto the one SIWE sign-in: connect a wallet, sign one message, choose a name, and you have a profile on the node; chat on the node then needs it (/pub/chat: 401 signin_required, 403 profile_required; admin sessions are exempt). Sign-in itself is the single SIWE system on /auth/* (see the wallet sign-in entry); the gateway only adds the username profile: GET /auth/username?name= and GET|PUT /auth/username (ASCII [a-z0-9_]{3,20}, unique ignoring case, reserved words refused, set once), stored per wallet in accounts.json (written atomically, mode 600, encrypted when ELYRA_PASSPHRASE is set). The page has one account flow in the popover (connect, sign in, name, ready), a chat gate that opens it, and the pay-per-use session under "advanced". The original standalone /gw/* routes and the elyra_acct cookie were removed in the merge. Not built yet: USDG balance, top-up, API keys and billing. Tried in a browser with a mock wallet only, not a real wallet app.
  • Security: a peer's hello is now treated as hostile input. An audit found that one malicious peer (models: 5, [null] or [{}] in its hello) made the market, /api/status, /v1/models and every chat answer 500 on the whole node, and the bad entry was saved, so it survived restarts. Now every field of a hello is sanitised (router/lib/peer-hello.mjs: models need a string id and a plain price >= 0, bounded counts and lengths, control and bidi characters removed, a seller can never claim verified), entries already in a state file are cleaned at start, peer answers are size-capped (1 MB; 8 MB for hosted model lists), a gossip list triggers at most 8 lookups at once and 20 in total, and a full peer table drops the oldest dead peer instead of refusing everyone (so throwaway identities cannot lock honest peers out). 12 new tests, 11 of 12 mutations caught (the twelfth is an equivalent defence-in-depth check).
  • Seller & buyer first-run quality from the public page flow (router/server.mjs, router/core/relay-client.mjs, router/core/peers.mjs, router/core/node.mjs, router/core/upstreams.mjs, router/core/consumer/chat.mjs, router/http/api.mjs, router/http/api-chain.mjs, panel/try/try.js, panel/try/wallet.js, panel/views/provide.js, panel/views/onboarding.js, panel/views/network-panels.js, panel/views/models.js, panel/views/playground.js, router/test/first-run.test.mjs): fixed first-run boot & relay races and UI friction discovered on a fresh 3-node rig: (1) router/server.mjs now runs await envCfg() before server.listen and awaits node.relay.ready (startRelayClient initial quick: true registration) before dialing --seed / ELYRA_SEEDS, so a seller running the #sell command on the public page registers its relay tunnel and upstream models before announcing /p2p/hello to the seed; (2) router/core/peers.mjs accepts /r/<id> relay peers hosted on the node's own origin or an already-trusted peer's origin (isTrustedRelay), pushes announcePeers() immediately on PUT /api/provide and PUT /api/chain, and re-announces in refreshPeers() whenever a peer forgets our publicUrl; (3) addUpstream automatically switches provide.upstreamId off "demo-engine" when the first local source is added (never for hosted sources), and ELYRA_ESCROW seeds a fresh node without overwriting owner edits; (4) POST /v1/chat/completions 404 no_route and the Playground / Models views (panel/views/playground.js, panel/views/models.js, fixed missing S store import in models.js) explain when online sellers are hidden by settlement mismatch (escrow), policy filters, or missing sources; (5) #snip-use and #snip-sell on /try/ dynamically include ELYRA_ESCROW and the live origin when on-chain settlement is active, peer rows link Use node / Buy via your node → to #use (or open session funding when ELYRA_PUBLIC_PAY=1), and Provide / Network show a Copy button for Peer URL, live Relay status, and a first-step warning when no real model source is connected. Covered by router/test/first-run.test.mjs (6/6 mutants killed).
  • End-user documentation pass & docs generator fixes (README.md, docs/QUICKSTART.md, docs/PROVIDER.md, docs/WALLET.md, docs/FAQ.md, docs/CONCEPTS.md, docs/OLLAMA.md, docs/PANEL.md, docs/USAGE.md, docs/API.md, docs/STATUS.md, docs/SDD.md, tools/pages/docs.mjs, tools/pages/docs.css, site/index.html, router/test/docs.test.mjs): added end-user sections on non-custodial browser session-key pay-per-use (ELYRA_PUBLIC_PAY=1, $20 per-wallet deposit cap, safe deposit - owed withdrawal), hosted APIs vs. peer-to-peer sellers (HOSTED · CLEAR vs. LIVE), and TEE privacy limits (LOCAL, REMOTE, CLAIMED, TEE · VERIFIED, milestone M1); removed stale test counts and outdated status claims from docs/STATUS.md; fixed fixLinks in tools/pages/docs.mjs mangling external http(s):// links into /blob/main/docs/https:/..., added id="content" to 404.html, expanded search indexing to 2,400 chars per section, fixed broken #public-market-page-optional and ../CHANGELOG.md links, added #about/#how-it-works/#advantage/#industry anchors on the landing page, and raised --dim contrast (#4a5670, 6.5:1) and sidebar fit in tools/pages/docs.css. Covered by router/test/docs.test.mjs (1,082 internal HTML links + 56 repo links + 148 Markdown links checked, 0 broken; 6/6 mutants killed).
  • UX, accessibility & mobile audit (panel/try/try.css, panel/try/try.js, panel/try/index.html, panel/css/base.css, panel/css/components.css, panel/components/controls.js, panel/lib/nav.js, panel/views/*.js, router/http/login-page.mjs, router/test/panel.test.mjs): audited the public page and all 8 owner-panel views at 1440 px, 1280 px and 390 px mobile (both sparse 1-seller and rich 5-model states). Fixed: (1) chat drawer starter prompt chips overflowing at 390 px and 1440 px (499 px wide because the shell .chip { white-space: nowrap } overrode #chips .chip), and mobile top-bar nav clipping Connect your tools while hiding #use; (2) WCAG 2.1 AA contrast across --dim (#6b7792 3.90:1 → #4a5670 6.50:1), --ok (#0f9d6b 3.34:1 → #067647 5.05:1), --warn (#b97a0a 3.26:1 → #8a5700 5.01:1), --sim (#8a5cf6 4.02:1 → #6938ef 5.05:1), sidebar/dark-card labels, .badge.live vs .badge.sim, and white-on-white Try it / Open playground CTA buttons; (3) keyboard navigation and screen-reader labels — inset :focus-visible rings on notched .btn/.nav buttons so clip-path never clips focus, for="..." and aria-label on all 22 previously unlabelled form controls and 14 icon-only buttons, role="switch" + aria-checked on toggles, keyboard-selectable SVG nodes in Network, and .app.inert = true while the chat dialog is open; (4) 1-seller and empty/error states — #s-cheapest shows Free to try instead of — when a free model is online, sparse markets (<= 3 models) auto-expand the first model row and show an honest 1-seller supply note, /pub/market error and filter-miss states include Retry now / Reset filter actions, and failed chat turns preserve the visitor's typed prompt. Covered by router/test/panel.test.mjs (6/6 mutants killed).
  • Catalog measurement tool (tools/catalog-stats.mjs, router/test/catalog-stats.test.mjs, docs/FAQ.md): added npm run catalog:stats (node tools/catalog-stats.mjs [--data <dir>] [--openrouter] [--url <models-url>] [--rules] [--json]) to measure unique canonical models, unique canonical vendors, and hosted vs P2P vs local/remote capacity with six written deduplication rules (excluding simulated/offline sources, meta-routers like openrouter/auto and negative-priced routers, and moving ~vendor/*-latest aliases; collapsing :free/:batch/:extended billing tiers while keeping Ollama :1b/:8b parameter sizes; unifying -instruct/-chat/-it and cross-source open-weights names like llama3.2:1b ↔ meta-llama/llama-3.2-1b-instruct; collapsing dated snapshots when the undated base model exists; and merging multi-prefix vendor aliases like meta + meta-llama and bytedance + bytedance-seed). Against OpenRouter's public /api/v1/models (460 raw entries across 69 raw prefixes), it reports 339 unique canonical models from 57 vendors (hosted, 0 P2P); against the live node (/var/lib/elyra), 1 live local model from 1 vendor (5 unique models from 3 vendors across configured sources when the remote GPU and peer are online). Covered by router/test/catalog-stats.test.mjs (6/6 mutants killed).
  • Per-wallet deposit cap for mainnet readiness (router/core/provider/credit.mjs, router/http/api.mjs, router/http/api-chain.mjs, router/http/public.mjs, router/core/consumer/session-pay.mjs, tools/wallet/src/fund.mjs, panel/views/provide.js, panel/views/payments-chain.js, panel/views/wallet-fund.js, panel/try/wallet.js, panel/try/pay.js): added a configurable app-side per-wallet escrow deposit cap (maxDepositUsd, default $20 USDG, ELYRA_MAX_DEPOSIT_USD seed, contract unchanged; after the merge it is the one cap, see the merge entry). Sellers refuse (402 deposit_cap_exceeded) any buyer wallet whose on-chain escrow deposit exceeds maxDepositUsd before creating state or calling an upstream model; POST /api/chain/deposit, the panel's wallet-fund helper (fund.mjs / panel/vendor/wallet.js), and the public pay-per-use session all enforce currentDeposit + amount <= maxDepositUsd and display the cap in the UI. Covered by router/test/deposit-cap.test.mjs (6/6 mutants killed).
  • Public-surface load & abuse hardening (router/http/public.mjs, router/http/relay.mjs, router/http/io.mjs, router/http/app.mjs, router/core/provider/guard.mjs, router/core/provider/serve.mjs, router/core/consumer/session-pay.mjs): fixed /try/ path-traversal bypass via percent-encoded slashes (%2e%2e%2f) and double-encoding (%252e), rejected non-origin-form URLs (//..., /\...) and null bytes at request entry, required JSON request bodies to be plain objects (preventing "null" / primitive / array TypeError 500s across /pub/*, /relay/* and /p2p/*), guarded /p2p/infer against non-string wire.from and malformed decrypted envelopes, capped hits/payHits (<= 5000) and seenNonces (<= 20000) by evicting oldest entries when all entries are fresh, removed aborted buyer jobs from the relay queue (s.queue) so cancelled requests cannot stall real buyers or trigger dead seller work, stripped control characters from seller reply headers, and translated chain RPC failures in GET /pub/session to 503 chain_unavailable. Covered by router/test/public-load-abuse.test.mjs (6/6 mutants killed) and a 1,500-request local load drill.
  • Operations & CI hardening: verified non-disruptive restore of /var/backups/elyra on a spare loopback port (identity, encrypted wallet, signing/box keys, seller config and per-model prices intact; wrong passphrase fails closed) and documented the procedure in Run it in production and covered it in router/test/boot-config.test.mjs. Hardhat now compiles with the local solc npm package instead of fetching a compiler binary over the network; .github/workflows/ci.yml verifies npm ci --omit=dev, contract tests, artifact and browser-bundle checksums (wallet.js.sha256, session.js.sha256), tools/pages docs build, and expanded secret-file patterns.
  • Multi-source selling (upstreamIds and "*" all healthy local sources): a seller can now offer models from several sources at once (PUT /api/provide with upstreamIds: [...] or upstreamId: "*", or All healthy local sources in the Provide view) so a node's own local CPU model stays priced and on sale while a remote GPU source is offline. Offline sources are excluded from /p2p/hello, /pub/market and /p2p/infer until they recover; remote tunnel sources are never advertised as TEE; per-model prices work across all sources.
  • Version badge: VERSION in router/core/limits.mjs now reads package.json (0.1.0) instead of a hardcoded "0.2.0", so /api/status, /healthz and the panel header match package.json.
  • Public page in the panel's app shell: left sidebar (Market, Try a model, Sell capacity, Wallet, Connect your tools, Docs, GitHub) with the active section following the scroll, a node card at the bottom, a top bar with a "Testnet, no real money" chip and the wallet button, four separate bracketed stat cards (the last one dark, like the panel), same hero and cards. On a phone the sidebar becomes a scrolling tab bar. Still only real offers: no simulated peers. The CSS is a copy of the panel's shell rules, so a future change to the panel shell needs the same edit in panel/try/try.css.
  • Pay per token from the public page (opt-in ELYRA_PUBLIC_PAY=1, needs ELYRA_PUBLIC_TRY=1 and a chain): a visitor creates a throwaway session key held in their browser, deposits into the escrow for that session address from their own wallet (approve + depositFor, two confirmations), and chats with peer sellers. The node only relays the chat and hands back the seller's receipt amount; the page signs each voucher, and only for an amount that fits the price the visitor accepted (a seller or node that inflates the amount gets no signature). The node holds no money and no key for the visitor. Withdrawals are done by the session key itself and leave in the escrow whatever the session has signed and sellers have not collected yet (an end-to-end run found a version that withdrew everything and would have cheated the seller; fixed, covered). New: POST /pub/paid-chat, POST /pub/voucher, GET /pub/session, panel/try/pay.js, panel/try/session.js (ethers only, built by tools/wallet/build-session.mjs). Off by default: the routes are plain 404s. The page's CSP gains the chain's RPC origin only when paying is on. Not yet tried with a real wallet app.
  • Hosted sources are ranked by their real prices: a source that publishes per-model prices (OpenRouter style, input and output averaged per million tokens) is no longer treated as free, so auto:cheapest stops preferring hosted models just because no price was recorded. Unusable prices (missing, negative, non-numeric) fall back to the source price; zero stays a valid price. Pointing a router at OpenRouter lists 458 models from 69 vendors (hosted, labelled Clear; not peer-to-peer).
  • Reference prices in the market (opt-in ELYRA_REFERENCE_PRICES=1): next to a model's price the page shows what the same family, version and size lists for on OpenRouter (input / output per 1M tokens), and says it is not a seller here. Exact matches only; a 7B coder is not the 32B coder and a distilled R1 is not R1, so those show nothing. The node fetches OpenRouter's public model list once every 6 hours and sends nothing about visitors; if it fails the market works without it.
  • Public market page redesigned, market first: a compact hero with the install snippet, a slim stat strip instead of four large cards, a searchable market table (models nobody has priced yet sort last), the chat in a slide-over drawer (Esc closes, focus returns), the wallet in a header popover, node facts in the footer, and a legend that folds away. The sell snippet now uses the relay.
  • Reputation signals: the router now measures time to first token (streamed answers) and each peer's uptime (share of the last hour's probes it answered, shown after about five minutes). Uptime lowers a peer's reputation score; both appear in the public market ("first token 412 ms · up 97%") and in /api/models and /api/peers. Fixes a peer losing its probe history when it was re-added.
  • Relay (sell from behind a home router): ELYRA_RELAY_URL (or --relay) makes a node dial out to a relay and be reachable at <relay>/r/<id> with no port forwarding; ELYRA_RELAY=1 turns a public node into a relay. Requests and responses stay sealed end to end; the seller only ever runs the three peer endpoints for a relay. New metric elyra_relay_jobs_total, status field node.relay.
  • Wallet on the public page: a "Your wallet" card connects any browser wallet (MetaMask, Rabby, Coinbase Wallet; no dependency, no key, no signature), switches or adds the settlement chain, shows ETH and token balances and, on testnet, mints 100 test tokens. New GET /pub/chain returns only public chain facts (never the node's address or balances). Paying for chats from the visitor's wallet is not built yet.
  • Per-model prices for sellers: PUT /api/provide accepts prices ({model: usd per million tokens}) next to the default price; the Provide view has a price field per model. Invalid entries (negative, non-numeric, null) are dropped; null is not read as a free model.
  • Look: the control panel, the public market page and the sign-in page share one visual language again: indigo gradient with a faint grid, NB Architekt headings, monospace labels, bracketed cards and cut-corner buttons. The panel keeps every newer feature (wallet funding, OFFLINE and REMOTE badges, settlement notice, sign out).
  • Settlement mismatch is explained: a seller that settles on another escrow is no longer silently dropped. GET /api/status has network.blocked and network.settlement, GET /api/peers has self and, per peer, settlement and routable; Overview names the escrow and offers Use this settlement after a confirmation that shows the full address. Escrow addresses are now compared case-insensitively (the same contract written with different letter case used to be treated as different).
  • Unfunded buyer: when every route is skipped for lack of funds the API answers 402 payment_required with what to do, instead of 502 all routes failed.
  • Public market page: the signed-out page is now a live market. GET /pub/market lists real offers only (own local and remote sources, real peers; never simulated, stub or hosted-key entries), with price, speed (measured or claimed, otherwise null), reputation (null until there is history) and a privacy label. Visitors can chat with any model the node serves itself (model in POST /pub/chat); peers are shown for comparison and are never called from the page. ELYRA_TEAM_PEERS marks your own peers.
  • Health: every model source is probed every 15 s. Two failed probes mark it offline: it leaves routing and /v1/models, the panel shows OFFLINE with the reason, /metrics has elyra_upstreams_up and elyra_upstreams_down, /healthz has serving. It returns as soon as it answers. GET /api/upstreams returns health per source.
  • Public try page: a pinned ELYRA_TRY_MODEL is exact (offline instead of a silent fallback to another model); unknown paths for visitors are a plain 404 (not a login redirect); /robots.txt; the page text is generated from the real hosting (local or remote).
  • Docs: new Run it in production; Remote access rewritten as a user guide.
  • remote upstreams: a source on another machine reached through a loopback or LAN address (SSH tunnel, VPN) is flagged remote (POST /api/upstreams, checkbox in Connect). It is labelled REMOTE, never private, excluded from private routing, and refused in TEE mode. Without the flag such a source was labelled LOCAL while prompts left the machine.
  • Public try page (router/http/public.mjs, panel/try/, opt-in ELYRA_PUBLIC_TRY=1): signed-out visitors of the admin hostname get a real streamed chat on the node's local model, never a peer, hosted key or simulated answer; per-client hourly limit, daily cap, concurrency cap, strict CSP. Tests: router/test/public.test.mjs.
  • Wallet connect (Reown AppKit, ELYRA_REOWN_PROJECT_ID): fund the escrow from your own wallet (depositFor the node address). Bundle built by tools/wallet/. Tests: router/test/fund.test.mjs.
  • Control panel restyled as a plain product dashboard; sign-in page and sign-out button for remote admin.
  • Routing fix: a live listing now always beats the simulated market within a request (plan()). Before, after connecting Ollama model: auto was still answered by a simulated peer with canned text. See docs/DECISIONS.md. Tests: router/test/market.test.mjs.
  • Opt-in remote admin (router/http/remote.mjs, ELYRA_ADMIN_HOST + ELYRA_ADMIN_TOKEN): the real panel, /api and /v1 on one named host behind a login (signed cookie, rate limits, CSRF check, proxied traffic never gets the localhost path, /metrics never). Off by default; loopback rules otherwise unchanged. Tests: router/test/remote.test.mjs (11). Docs: docs/REMOTE-ADMIN.md.
  • Remote admin login page: Referrer-Policy: same-origin (was no-referrer, which made Chrome send Origin: null and the login was refused). Regression test added (12 tests in remote.test.mjs).
  • Panel UX: next-step banner on Overview (connect a real model / hide the simulated market), one-click Ollama detection from Overview, "connected" card in Connect, live models first in the Playground with an honest empty state and a wait indicator for cold model loads, tidier result badges. Mobile: fixed grid minimum widths that pushed every card past the screen (0 overflowing elements in all 8 views at 390 px), topology graph scrolls inside its box.
  • Test router/test/panel.test.mjs: every panel module must parse and every relative import must resolve to an export (the panel has no build step, so nothing else catches this).
  • Open-source readiness: LICENSE (MIT, matching the contract SPDX headers), SECURITY.md, CONTRIBUTING.md, rewritten README.md; new docs INSTALL, OLLAMA, USAGE, CONFIGURATION, DEPLOY-CLOUDFLARE, and a docs index.
  • Cloudflare Pages: tools/pages/ renders the landing + docs into dist/; wrangler.toml; .github/workflows/deploy-pages.yml. /app/* redirects to the install guide because the panel needs a local router.
  • Static demo removed. /app/ on the website redirects to the live node at app.elyrainference.com.
  • New docs: PANEL, API (control API reference), PROVIDER, FAQ, GLOSSARY.
  • Pages bundle ships a 404.html (otherwise Pages answers unknown paths with 200 and the landing). First live deploy: elyra-5cg.pages.dev.
  • Tests: node boot wait raised from 5 s to 30 s (router, TEE) and the encryption-at-rest test now stops each child once the boot banner is printed instead of after a fixed 4 s. On a loaded host boot took 4.5 s and tests failed spuriously. No runtime change.

0.2.0 — 2026-11-04#

Mainnet preparation (2026-11-07)#

  • Deployed on Robinhood mainnet: ElyraEscrow (real USDG) and ElyraRegistry (bond 0); addresses and tx hashes in docs/STATUS.md.
  • docs/MAINNET.md: capped-beta runbook. Mainnet node data dir .elyra-main created with an encrypted wallet.
  • Node-side deposit cap on real networks (ELYRA_MAINNET_DEPOSIT_CAP_USD, default now 20 after the merge; invalid values fall back to the default and never disable it, 0 blocks).

Panel redesign (2026-11-07)#

  • Whole control panel restyled as a modern dark-first product UI (system sans, rounded cards, pills, light theme toggle, mobile drawer, skeletons, inline form validation, OpenRouter-style Models list, chat-style Playground). The commercial NB Architekt / ki fonts are no longer used by the panel. Behaviour, endpoints and honest labels unchanged.

Public pages (2026-11-07)#

  • Try page, public market page and the sign-in page restyled to the panel design system (dark default, light toggle on try/market); ki/NB Architekt font files deleted from panel/try/.

Landing + licensing (2026-11-07)#

  • Loading screen now draws the Elyra mark (it still contained the template's old logo); favicons, touch icons and the share image (og:image) regenerated from the Elyra mark.
  • Added THIRD_PARTY.md: the MIT license does not cover the saved WordPress landing mirror, its fonts or bundled vendor assets.

User flow (2026-11-07)#

  • Panel navigation reordered by journey (Start / Set up / Use AI / Earn & pay) and page numbers made consistent; Overview renamed Home.
  • The guided checklist moved from the bottom of Home to the top, with progress and a single highlighted next step (Use AI / Sell AI); Provide gets a Go-live checklist that warns when the peer URL is localhost. Phones hide the hero copy so the checklist is on the first screen.
  • Fix: control-API writes are flushed to disk immediately, so a hard kill within 1.5 s no longer loses owner configuration (this was the failing boot-config test on Windows).

Architecture (2026-11-06)#

  • Repo restructured: vpr/ → router/, app/ → panel/, landing mirror → site/. URLs unchanged (/, /app/).
  • router/node.mjs (920 lines) split into router/core/ (+ consumer/, provider/), router/http/ (route table per surface), router/lib/; router.mjs renamed policy.mjs. No wire, API or on-disk format change; the 47 tests are unchanged.
  • Panel app.js (735 lines) split into ES modules (lib/, components/, views/, css/); more remote strings escaped; dead code removed.
  • Added docs/LANDSCAPE.md (competitors and reusable repos), docs/VPS.md (seller node on a VPS), docs/HANDOFF.md (agent onboarding) and docs/REGISTRY.md (discovery design, not built).

Rename (2026-11-06)#

  • New logo: supplied "E" mark everywhere (app, favicon, landing header/footer) and the 3D hero + token models regenerated from it (tools/brand/).
  • Solais → Elyra everywhere (see docs/STATUS.md → Naming). Wire-breaking: PROTOCOL 3, ElyraEscrow redeployed + verified on Robinhood testnet, ELYRA_* env vars, .elyra data dir (legacy .solais adopted).

TEE (2026-11-05)#

  • Real attestation verification for Google Confidential Space (router/tee.mjs), buyer-pinned image digests, VERIFIED vs CLAIMED, stricter teeOnly, TEE-mode seller (attestation in hello, local upstreams only), payout sweep, boot env config, redeem on shutdown, deploy/tee/ kit, docs/TEE.md. Breaking for routing: self-declared and demo TEE no longer count as private.

Roles (2026-11-05)#

  • Overview onboarding for buyers and sellers, seller Earnings panel (earnDaily in /api/ledger stats), app talks to its own origin on any port, no-store for app assets, landing marks 600+/150+ as a launch target.

Security review fixes (2026-11-05)#

  • Found while testing live: vouchers are bound to the escrow they were signed for; the voucher baseline is max(local cumulative, on-chain claimed) + amount (provider mirrors it). Mainnet-fork rehearsal script (npm run rehearse:mainnet) and its report.
  • Receipts are validated strictly and the amount is derived locally; vouchers must be canonical 65-byte signatures; provider credit is keyed by wallet with a fail-closed on-chain deposit check, worst-case admission control (max_tokens capped at 8192) and auto-redeem; requests are ed25519-authenticated with replay protection; wallet control is proven (EIP-191); voucher pushes are sealed; settlement is per-wallet and fault tolerant. Escrow v2: withdraw requests are single-use and expire (7 days). See docs/STATUS.md for the finding table.

Operations#

  • Protocol version negotiation (PROTOCOL = 2, mismatch refused with a clear error, x-elyra-protocol header), Prometheus /metrics, /healthz, structured JSON logs, GitHub Actions CI. Tests: metrics never contain prompt text; old-protocol peers are refused.

USDG settlement (supersedes the ETH design below)#

  • Deployed and exercised on Robinhood Chain Testnet: MockUSDG + ElyraEscrow verified on Blockscout; two-node paid trade settled on chain (addresses and tx hashes in docs/STATUS.md).
  • ElyraEscrow(token) is ERC-20: deposit(amount)/depositFor, balance-delta check, safe transfers; MockUSDG for testnet/local (open mint, labelled). Amounts are token units (USDG 6 decimals) = dollars × 1e6: no oracle, no usdPerEth. Receipts carry amountUnits.
  • router/net.mjs: DoH resolver so Robinhood hosts work behind DNS-hijacking ISPs; chain ids verified live.
  • Deploy: npm run chain:deploy:testnet (uses the node wallet). Panel: token/balances, mint (mock only), approve+deposit.

Added#

  • On-chain settlement for Robinhood Chain (Arbitrum Orbit, ETH gas): contracts/ElyraEscrow.sol (deposit, cumulative EIP-712 vouchers, redeem, delayed withdraw; 12 hardhat tests), router/chain.mjs adapter, consumer vouchers after every verified receipt, provider credit limit and escrow-funding check, /api/chain*, Payments → Escrow panel, explorer links. Verified end to end against a local EVM (router/test/chain.test.mjs). Not yet exercised on the live Robinhood testnet (RPC unreachable from the dev machine).
  • Sealed transport (protocol v2): prompts and completions encrypted to the provider's X25519 key; unsealed requests refused.
  • Token-inflation audit: receipts claiming far more output tokens than the received text are rejected.
  • SSRF guard for gossip-learned peers, per-consumer rate limit, deny list, startup re-contact of remembered peers, wallet/box-key binding in hello.
  • Landing: legal pages, removed agency trackers/socials, honest TEE wording.

Network view#

  • Rebuilt: ring topology (live / bridge / community), vendor logos on bridge nodes, animated packets on recent routes, hover tooltip, click-for-detail with per-peer model table and TRY, filters, stat tiles.

Fixed#

  • Hosted upstreams (OpenRouter, OpenAI, …) are no longer labelled LOCAL/private: they show HOSTED · CLEAR, score 0 on privacy, and are excluded by teeOnly / auto:private. Only localhost/LAN upstreams count as local.

Security#

  • Optional encryption at rest via ELYRA_PASSPHRASE (router/vault.mjs), tested (encrypted files, same identity across restarts, refuses without/with wrong passphrase).

Blockscout#

  • contracts/verify.mjs (+ artifacts/standard-input.json from the build), explorer address links and a verified-source badge in Payments. Tip: if the primary RPC is down, a Blockscout instance may serve JSON-RPC at <explorer>/api/eth-rpc; paste it as the RPC override.

Changed#

  • Receipts now carry amountWei/providerWallet in chain mode. /p2p/infer body is {from, sealed}.
  • Node address is now the real secp256k1 wallet (was a derived placeholder). Runtime dependency: ethers.
  • Client hang-ups no longer count as peer failures.

0.1.0 — 2026-11-04#

Added#

  • router/: Virtual Private Router daemon. OpenAI-compatible endpoint, policy ranking, failover, ed25519 signed receipts, peer gossip, provider mode, ledger + simulated-testnet settlement, loopback-only control API, static hosting of site and panel.
  • panel/: control panel rewritten in the landing's visual language (indigo gradient, paper surface, NB Architekt, bracketed cards). Views: Overview, Playground, Models, Routing, Network, Payments, Provide, Connect. Provider/model logos from assets/logos.
  • Simulated demo market (labelled) so routing works on a fresh install.
  • npm test: ranking rules + two-node integration (billing equals receipt, streaming, price-bait refusal, TEE-only, local-only API).
  • AGENTS.md, PRD.md, ROADMAP.md, docs/.

Changed#

  • Landing copy, meta, 3D labels and dashboard image rewritten from "AI visibility analytics" to "peer-to-peer AI inference".

Removed#

  • server.mjs and start-mirror.bat (superseded by router/server.mjs, start.bat).