2026-11-07: buyer safety and flow#
- Daily spend cap: new setting
spendCapUsdDay(PUT /api/settings, Payments → Daily spend cap;null= no cap,0blocks every paid route; negative, non-finite or non-numeric values are400). 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 is402 spend_cap_reachedwith how to raise it.GET /api/statushasspendCap: {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/statusgainsnode.dataDirandnode.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; cookieelyra_adminv2, HMAC, per-boot key, 12 h, signed out on restart; logout revokes the cookie server-side). Admin if the address is inELYRA_ADMIN_ADDRESSES, else user. The old/gw/*routes and theelyra_acctcookie 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=1enables 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/chainand the node deposit path (cumulative with what the escrow already holds); on a real network the stricter of it andELYRA_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, andhandover/(handover/secrets.encis 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
#selland#m=modellinks still work); the chat drawer is replaced by a full-height Playground page (live models only,LIVE/LOCAL/CLEARbadges, 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'slocalStoragewith 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 underpanel/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) andGET /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 arerequest(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) andalert(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 fromconsumer/chat.mjs,provider/serve.mjsandreceipt.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 aleave()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/logouton 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 withELYRA_ADMIN_HOST+ELYRA_ADMIN_TOKENorELYRA_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 keepsv1). 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/chatusesELYRA_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.jsunderscript-src 'self'; connect-src 'self'(script-free otherwise). Newpanel/try/auth.js(signInWithWallet,getSession,signOut; EIP-1193, no AppKit dependency). Docs: newdocs/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): newElyraRegistrycontract (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) androuter/core/registry.mjs. Buyers readAnnounced/Revokedlogs 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 announcednodeId,boxKey, wallet andmetaHashmust match the hello (commitment defined indocs/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, settingsregistryDiscover/registryAnnounce,node contracts/deploy.mjs --registry [--bond <usd> --token 0x…], panel badge "from registry" on the Network peer list. TheAnnouncedevent also carriesboxKey(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=andGET|PUT /auth/username(ASCII[a-z0-9_]{3,20}, unique ignoring case, reserved words refused, set once), stored per wallet inaccounts.json(written atomically, mode 600, encrypted whenELYRA_PASSPHRASEis 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 theelyra_acctcookie 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/modelsand 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 claimverified), 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.mjsnow runsawait envCfg()beforeserver.listenand awaitsnode.relay.ready(startRelayClientinitialquick: trueregistration) before dialing--seed/ELYRA_SEEDS, so a seller running the#sellcommand on the public page registers its relay tunnel and upstream models before announcing/p2p/helloto the seed; (2)router/core/peers.mjsaccepts/r/<id>relay peers hosted on the node's own origin or an already-trusted peer's origin (isTrustedRelay), pushesannouncePeers()immediately onPUT /api/provideandPUT /api/chain, and re-announces inrefreshPeers()whenever a peer forgets ourpublicUrl; (3)addUpstreamautomatically switchesprovide.upstreamIdoff"demo-engine"when the first local source is added (never for hosted sources), andELYRA_ESCROWseeds a fresh node without overwriting owner edits; (4)POST /v1/chat/completions404 no_routeand the Playground / Models views (panel/views/playground.js,panel/views/models.js, fixed missingSstore import inmodels.js) explain when online sellers are hidden by settlement mismatch (escrow), policy filters, or missing sources; (5)#snip-useand#snip-sellon/try/dynamically includeELYRA_ESCROWand the live origin when on-chain settlement is active, peer rows linkUse node/Buy via your node →to#use(or open session funding whenELYRA_PUBLIC_PAY=1), andProvide/Networkshow aCopybutton forPeer URL, liveRelaystatus, and a first-step warning when no real model source is connected. Covered byrouter/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,$20per-wallet deposit cap, safedeposit - owedwithdrawal), hosted APIs vs. peer-to-peer sellers (HOSTED · CLEARvs.LIVE), and TEE privacy limits (LOCAL,REMOTE,CLAIMED,TEE · VERIFIED, milestone M1); removed stale test counts and outdated status claims fromdocs/STATUS.md; fixedfixLinksintools/pages/docs.mjsmangling externalhttp(s)://links into/blob/main/docs/https:/..., addedid="content"to404.html, expanded search indexing to 2,400 chars per section, fixed broken#public-market-page-optionaland../CHANGELOG.mdlinks, added#about/#how-it-works/#advantage/#industryanchors on the landing page, and raised--dimcontrast (#4a5670, 6.5:1) and sidebar fit intools/pages/docs.css. Covered byrouter/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 pxwide because the shell.chip { white-space: nowrap }overrode#chips .chip), and mobile top-bar nav clippingConnect your toolswhile hiding#use; (2) WCAG 2.1 AA contrast across--dim(#6b77923.90:1 →#4a56706.50:1),--ok(#0f9d6b3.34:1 →#0676475.05:1),--warn(#b97a0a3.26:1 →#8a57005.01:1),--sim(#8a5cf64.02:1 →#6938ef5.05:1), sidebar/dark-card labels,.badge.livevs.badge.sim, and white-on-whiteTry it/Open playgroundCTA buttons; (3) keyboard navigation and screen-reader labels — inset:focus-visiblerings on notched.btn/.navbuttons soclip-pathnever clips focus,for="..."andaria-labelon all 22 previously unlabelled form controls and 14 icon-only buttons,role="switch"+aria-checkedon toggles, keyboard-selectable SVG nodes in Network, and.app.inert = truewhile the chat dialog is open; (4) 1-seller and empty/error states —#s-cheapestshowsFree to tryinstead of—when a free model is online, sparse markets (<= 3models) auto-expand the first model row and show an honest 1-seller supply note,/pub/marketerror and filter-miss states includeRetry now/Reset filteractions, and failed chat turns preserve the visitor's typed prompt. Covered byrouter/test/panel.test.mjs(6/6 mutants killed). - Catalog measurement tool (
tools/catalog-stats.mjs,router/test/catalog-stats.test.mjs,docs/FAQ.md): addednpm 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 likeopenrouter/autoand negative-priced routers, and moving~vendor/*-latestaliases; collapsing:free/:batch/:extendedbilling tiers while keeping Ollama:1b/:8bparameter sizes; unifying-instruct/-chat/-itand cross-source open-weights names likellama3.2:1b↔meta-llama/llama-3.2-1b-instruct; collapsing dated snapshots when the undated base model exists; and merging multi-prefix vendor aliases likemeta+meta-llamaandbytedance+bytedance-seed). Against OpenRouter's public/api/v1/models(460raw entries across69raw prefixes), it reports 339 unique canonical models from 57 vendors (hosted,0P2P); against the live node (/var/lib/elyra), 1 live local model from 1 vendor (5unique models from3vendors across configured sources when the remote GPU and peer are online). Covered byrouter/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_USDseed, 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 exceedsmaxDepositUsdbefore 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 enforcecurrentDeposit + amount <= maxDepositUsdand display the cap in the UI. Covered byrouter/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 / arrayTypeError500s across/pub/*,/relay/*and/p2p/*), guarded/p2p/inferagainst non-stringwire.fromand malformed decrypted envelopes, cappedhits/payHits(<= 5000) andseenNonces(<= 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 inGET /pub/sessionto503 chain_unavailable. Covered byrouter/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/elyraon 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 inrouter/test/boot-config.test.mjs. Hardhat now compiles with the localsolcnpm package instead of fetching a compiler binary over the network;.github/workflows/ci.ymlverifiesnpm ci --omit=dev, contract tests, artifact and browser-bundle checksums (wallet.js.sha256,session.js.sha256),tools/pagesdocs build, and expanded secret-file patterns. - Multi-source selling (
upstreamIdsand"*"all healthy local sources): a seller can now offer models from several sources at once (PUT /api/providewithupstreamIds: [...]orupstreamId: "*", 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/marketand/p2p/inferuntil they recover;remotetunnel sources are never advertised as TEE; per-modelpriceswork across all sources. - Version badge:
VERSIONinrouter/core/limits.mjsnow readspackage.json(0.1.0) instead of a hardcoded"0.2.0", so/api/status,/healthzand the panel header matchpackage.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, needsELYRA_PUBLIC_TRY=1and 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 bytools/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:cheapeststops 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/modelsand/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=1turns 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 metricelyra_relay_jobs_total, status fieldnode.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/chainreturns 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/provideacceptsprices({model: usd per million tokens}) next to the defaultprice; the Provide view has a price field per model. Invalid entries (negative, non-numeric,null) are dropped;nullis 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/statushasnetwork.blockedandnetwork.settlement,GET /api/peershasselfand, per peer,settlementandroutable; 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_requiredwith what to do, instead of502 all routes failed. - Public market page: the signed-out page is now a live market.
GET /pub/marketlists 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 (modelinPOST /pub/chat); peers are shown for comparison and are never called from the page.ELYRA_TEAM_PEERSmarks 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,/metricshaselyra_upstreams_upandelyra_upstreams_down,/healthzhasserving. It returns as soon as it answers.GET /api/upstreamsreturnshealthper source. - Public try page: a pinned
ELYRA_TRY_MODELis 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.
remoteupstreams: a source on another machine reached through a loopback or LAN address (SSH tunnel, VPN) is flaggedremote(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-inELYRA_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 (depositForthe node address). Bundle built bytools/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 Ollamamodel: autowas still answered by a simulated peer with canned text. Seedocs/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,/apiand/v1on one named host behind a login (signed cookie, rate limits, CSRF check, proxied traffic never gets the localhost path,/metricsnever). 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(wasno-referrer, which made Chrome sendOrigin: nulland the login was refused). Regression test added (12 tests inremote.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, rewrittenREADME.md; new docsINSTALL,OLLAMA,USAGE,CONFIGURATION,DEPLOY-CLOUDFLARE, and a docs index. - Cloudflare Pages:
tools/pages/renders the landing + docs intodist/;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) andElyraRegistry(bond 0); addresses and tx hashes indocs/STATUS.md. docs/MAINNET.md: capped-beta runbook. Mainnet node data dir.elyra-maincreated 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 Architektfont files deleted frompanel/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-configtest on Windows).
Architecture (2026-11-06)#
- Repo restructured:
vpr/→router/,app/→panel/, landing mirror →site/. URLs unchanged (/,/app/). router/node.mjs(920 lines) split intorouter/core/(+consumer/,provider/),router/http/(route table per surface),router/lib/;router.mjsrenamedpolicy.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) anddocs/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,
ElyraEscrowredeployed + verified on Robinhood testnet,ELYRA_*env vars,.elyradata dir (legacy.solaisadopted).
TEE (2026-11-05)#
- Real attestation verification for Google Confidential Space (
router/tee.mjs), buyer-pinned image digests,VERIFIEDvsCLAIMED, stricterteeOnly, 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 (
earnDailyin/api/ledgerstats), 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_tokenscapped 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). Seedocs/STATUS.mdfor the finding table.
Operations#
- Protocol version negotiation (
PROTOCOL = 2, mismatch refused with a clear error,x-elyra-protocolheader), 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;MockUSDGfor testnet/local (open mint, labelled). Amounts are token units (USDG 6 decimals) = dollars × 1e6: no oracle, nousdPerEth. Receipts carryamountUnits.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.mjsadapter, 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 byteeOnly/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.jsonfrom 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/providerWalletin chain mode./p2p/inferbody 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 fromassets/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.mjsandstart-mirror.bat(superseded byrouter/server.mjs,start.bat).