Changelog

What is new in Glide

Every release, what changed and when. Newest first.

0.36.8.1

Latest

Token logos sit straight in their circles.

Fixed

  • **Token artwork looked off-centre in the swap picker.** Most token logos are a circle whose transparent corners reach the edge of the image, and we drew a 1px ring of our own directly on top of that edge - so USDC read as a small blue disc floating inside a bigger pale circle. The ring is gone wherever there is real artwork to show; it still marks the placeholder for a token with no logo. Logos with an opaque square source, like SOL and PYTH, hid the problem, which is why only some rows looked wrong.
  • **The image was also a pixel down and to the right.** In React Native an absolutely-positioned child is measured against the padding box, inside the border, so `top: 0` with a fixed width started one pixel in and ran a full width from there - losing the bottom-right corner to the clip. Pinning all four edges keeps the logo concentric with its circle at any border width.

0.36.8.0

Tokens look like themselves, the Recipients screen stops asking for work you have already done, and the app can take an update without being closed twice.

Added

  • **Real token artwork.** The swap picker showed every token as a coloured circle with three letters in it. It now shows each token's own logo, matched by contract address rather than by ticker, so a token that merely calls itself USDC cannot borrow the real one's artwork.
  • **Choose the currency your balance is shown in, in the app.** USD, EUR and GBP, the same three the web app has had. If a rate cannot be fetched the balance stays in USD and says so, rather than showing a euro sign in front of a dollar figure.
  • **Take an update without closing the app twice.** A Refresh button appears when a new version is ready, and one tap takes it. Swipe it up to dismiss. Settings also has a permanent "Refresh Now" that checks on demand and only restarts if there is genuinely something newer.

Changed

  • **Import buttons retire once you have used them.** Importing your phone contacts hides the phone button; connecting Google hides the Google one. Each goes independently, and only after the import has fully finished, so a connection that drops halfway never costs you the way back in.
  • **A cleaner Recipients screen.** "Add" now reads as the main action, the three buttons are the same size whatever their labels say, and the "2,583 contacts imported" line no longer sits there long after the import.

Fixed

  • Thirteen buttons across the web app were rendering with no styling at all, because they asked for a style that does not exist. They look like buttons again.

0.36.7.1

Internal only. Nothing in this release changes what a customer sees.

Added

  • A release check that fails when a release would reach nobody. Glide mobile ships over the air, and an update only reaches installs whose app version matches it exactly - so publishing one is not the same as shipping it. Twice in two days a release was declared shipped and reached no customers: once because the public App Store version was older than the update, once because an approved build sat waiting for a human to press Release. Both were caught by a person reading a runbook. Now the runbook is executable.

0.36.7.0

Glide pays the gas. A customer holding only USDC can now move their own money without first going out to buy ETH or SOL, and the exchange rates on the home screen are real again.

Added

  • **Sponsored sends.** Send USDC from a wallet with no gas in it at all. Glide covers the network fee, and on Solana also the deposit that creating a new recipient's token account requires. Proven on mainnet from wallets holding exactly zero: Base `0x6070dfd1…` and Solana `3wdDUqqm…`.
  • **Noah decisions reach you wherever you are.** Identity checks that pass, fail or need another document now arrive as a push notification, an email and an in-app message rather than only changing a status you had to go and look at.
  • **Currency flags across the app.** USD, EUR and 41 other currencies show their country's flag wherever an amount appears.
  • **A live exchange-rate chart.** The Convert chart plots the rate you would actually get, over the timeframe you pick, and the change shown above it describes the line beneath it.

Fixed

  • The home currency toggle said "Exchange rates unavailable" while Glide held perfectly good rates in its own database. EUR and GBP work again.
  • A rate is no longer shown once it is more than three hours old. It used to keep converting your balance at whatever was last recorded, with nothing on screen to say how old that was.
  • Removed the mock card from the home screen. The activity list now uses the full width instead of sharing it with a picture of a card nobody has.

Changed

  • CAD is priced from CADC. Paytrie is switched off until it has production credentials.
  • Ethereum sends are declined while network fees are above 1 gwei, with the live price quoted and Base offered instead.

0.36.6.1

Ethereum balances work again. They had two separate faults, and the error message blamed the wrong one.

Fixed

  • Your ethereum balance shows up again. The euro coin on ethereum was recorded with one wrong character in its address, so that balance could never be read and the whole ethereum total was reported as unavailable. It looked like a network problem and was not. The other three networks were never affected.
  • Balances survive a slow network. When our balance provider was unreachable, the backup we fell back to for ethereum was one that refuses our requests, so ethereum had no working backup at all. There are now two working backups for every network.

0.36.6.0

Wallet repair moved to the server, so it no longer depends on which version of the app you are running. An expired sign-in now renews itself instead of stranding you on a screen that will not work. Settings finally has the same bottom bar as the rest of the app.

Fixed

  • Every account gets its wallets, whatever version you are on. The previous release created the missing wallets from inside the app, which only helped people who had already updated. Wallet creation now happens on the server on your first signed in request, so an account without a wallet is repaired the next time it talks to Glide, from any device and any version. The 127 accounts that existed before this release were repaired directly.
  • Your session renews itself. Sign in tokens expire after a while, and the app kept using the expired one. Every screen came back refused while the app still looked signed in, and the retry button retried the same dead token forever. Glide now renews the token once and retries, and signs you out properly when it genuinely cannot.
  • Screens stopped showing our internal wording. A failed balance check used to print "Not authenticated" above a retry button and leave you there. Errors now read as sentences, and a session that has ended says so.
  • The Settings bottom bar sat higher than every other screen and used different icons. It is the same bar now, in the same place.

Changed

  • Price and peg alerts are off unless you ask for them. They fired on market moves nobody had opted into. Turn them back on under Notifications.
  • Verification during signup says what it is for, and you can skip it. The step now explains that it gets you USD and EUR bank accounts and is approved in 2 minutes, and it is honest that sending and receiving already work without it. Skip for now appears only during signup. Opened from Settings there is no skip, because you went there on purpose.
  • Onboarding stopped offering a save that never happened. Five screens showed Save and exit while saving nothing. The ones that cannot save now say so.
  • New bottom bar icons, drawn from the same set the website uses, filled in when selected, over frosted glass.

0.36.5.0

Ten weeks of new accounts were created without a crypto wallet behind them, so deposits had nowhere to land. That is fixed, and the accounts affected repair themselves the next time you sign in.

Fixed

  • New accounts get their wallets again. Since 5 July every account created on Glide had no wallet behind it, which meant no deposit address and nothing to send from. Signing in worked normally, so nothing looked wrong. 99 of 127 accounts were affected. Your wallets are created the next time you open the app or the site, with nothing for you to do.
  • Your balances show the full picture. When one network was slow to answer, the page threw away the balances it had already fetched for that network and said they were unavailable. It now keeps what it has and asks again only for what is missing. A balance we genuinely cannot read is still reported as unknown rather than shown as zero.
  • Dollar and euro deposits settle on Base. Dollars arrive as USDC and euros as EURC, both into the same wallet, so a deposit no longer depends on which currency you sent. Accounts still pointed at the old network move across on their next deposit, without you needing new bank details.
  • The admin customer page opens again. Looking up any customer returned an error, because the page asked for a name field that was never stored.

Added

  • A public changelog at glide.co/changelog, which is this file.

0.36.4.0

A crash that took out the whole recipients section on iPhone, a deposit screen that blanked every time you opened it, a set of business screens that had never been connected to anything, and a long tail of taps that did nothing at all.

Fixed

  • Recipients no longer crashes the app. Adding, opening or editing a recipient closed the app immediately, because the screen asked the phone for country names in a way iPhones do not support. Every automated test passed throughout, since the machines running them do support it.
  • The deposit screen stops flickering. It cleared the card and re-fetched your account details from the bank on every open, so the numbers vanished and came back each time. They are now shown at once and quietly confirmed behind the scenes.
  • Accepting a vault invitation works. The app was sending the wrong reference, so every acceptance was rejected.
  • Payroll run details, gas sponsorship and vault policies, tokens and contacts all load. Each was asking the server a question it does not answer.
  • Agents, earnings, referrals, scheduled payments, Telegram, spending and payroll open with real data on mobile. Fifteen screens were built against endpoints that were never created; all of them now use the ones behind the equivalent web pages.
  • Transaction details show the chain, hash and addresses again.
  • Choosing a currency when adding a recipient no longer shows a column of identical placeholder symbols. 226 of the 249 currencies now carry their country's flag, and the shortcuts sit in an even grid.
  • Your deposit details now appear the moment you open the screen. The USD and EUR account numbers are kept on the device and drawn immediately, then confirmed in the background, so the card is never empty while you wait.
  • Tapping a business account on your profile switches into it. It used to open a list of settings and switch nothing at all.
  • Every option on the send screen goes somewhere that works. The screen itself could not be reached from anywhere in the app, and two of its choices led to the same place under different names.
  • Back buttons work everywhere in Pay. Opening a payment screen from a link or a notification left the back control doing nothing.
  • The dashboard opens on browsers that withhold site data. Private windows, some in-app browsers, and anyone who has turned site data off saw a blank page. Picking a workspace works there too now, for the session.
  • The admin panel responds to a click straight away, shows where you are on every one of its pages, and keeps its navigation when a page fails to load.
  • Deposit details are no longer requested twice when you switch currency and switch back, and details our banking partner has stopped honouring are no longer shown again.
  • The recent email on the sign-in screen is the same size as the box above it.
  • Sending money is reached the same way from Home and from Pay.
  • Signing out clears everything held for you on the device, from every screen that can sign you out.

Changed

  • Choosing how to send money is now its own screen rather than three words above the form. Bank transfer, someone on Glide, and a wallet address read as a caption before, and the wallet option behaved differently from the other two.
  • Per-code IFSC pages now redirect to the bank they belong to, folding roughly 180,000 near-empty pages into the 260 that people actually search for.
  • Agent trust tiers show the four the platform actually enforces, with their real limits, instead of five invented ones.
  • Agent health reports the state of an agent's grants — active, expired, revoked, next expiry — rather than uptime figures nothing measures.
  • Vault security shows who can sign and how many signatures a movement needs.
  • A scheduled payment can be paused from mobile but not resumed there, which is what the server supports; the row now says so instead of offering a switch that only worked one way.

Added

  • JavaScript fixes now reach the app without an App Store review, so a fix like the ones above can ship the same day rather than next week.

Under the hood

  • Crash reports from the app are readable, with source maps checked and uploaded before an update ships rather than after it is already live.
  • One profile request is shared across screens instead of one per screen, which also closed a window where a business screen could briefly load another workspace's figures.

0.36.3.0

Business workspaces were carrying a lot of personal baggage and a few surfaces that quietly did not work. This release separates the two and makes navigation open from memory instead of waiting on the network.

Fixed

  • Contacts belong to a workspace. Your personal address book no longer appears inside a company, and one company's recipients no longer appear inside another. Contacts imported before this update stay personal, so a business workspace starts empty.
  • Business verification is reachable. A founder who had already verified personally was never prompted to verify their company, and nothing linked to the business verification flow, so a business could not get a USD account. Prompts and the deposit screen now check the company's verification rather than the person's.
  • Sending from a Solana vault works. The chain picker offered only the four EVM chains even when the workspace had a Squads vault, because the two vaults were looked up as one.
  • Paying a single contractor from a vault works. Vault payroll required at least two recipients, so the smallest teams could not run payroll at all.
  • Creating a workspace no longer drops you back into Personal. The app now keeps your new workspace selected instead of resetting it a moment later.
  • Button labels are readable again. Several buttons, including Top up, rendered dark text on a dark background, and many borders were missing.
  • Settings no longer offers to export your personal wallet key from inside a company's settings.
  • A false "EURC off peg" alert is gone. It compared a euro stablecoin against the US dollar, so it reported the exchange rate as a depeg on every check and warned people who held none.
  • Invoices has a place in the navigation, so there is a way back to it.
  • Workspace pickers that duplicated the switcher in the header have been removed from nine pages; every page now follows the one in the header.

Changed

  • Moving between pages loads from memory. Data for a destination is fetched when you hover its link, so the page is ready before the click lands.
  • Equity moved after Payroll, and the Cards tab is hidden until a card partner is connected end to end.
  • The NFT gallery has been removed from the app.
  • Payroll contract templates dropped their region prefixes: Contractor Monthly, Employee Monthly, Contractor Bi-weekly, Employee Bi-weekly, Contractor Hourly, Contractor Milestone, Contractor PAYG.

0.36.2.0

Three fixes that each removed a way the product could fail silently: a banking-partner outage that blocked deposits, a mobile workspace that could not be created, and app crashes that were never being reported.

Fixed

  • Bank deposit details survive a temporary outage at the banking partner. When the account-details service is unavailable, Glide re-serves the same verified account number and reference it issued before, after confirming with the partner that the account is still active and still bound to your wallet. If it cannot confirm that, it says the details are temporarily unavailable rather than showing anything unverified. (#482, PRD-22)
  • Creating a business workspace from the mobile app works again. The request was missing the workspace type, so every attempt failed. (#489)
  • App crashes on iPhone and Android are now reported to Sentry. Crash reporting had never been switched on for the mobile apps, so faults could only be guessed at from user reports. (#490)

Under the hood

  • The Google Search Console verification file is kept in the repository, so a deploy can no longer remove it and un-verify the domain. (#487)
  • CI runs on self-hosted runners, with enough headroom that a busy machine no longer fails a job that would otherwise pass. (#488, #491)

0.36.1.0

Eight production bugs reported through Sentry are fixed, each verified against the exact failing event, plus progress feedback for large contact imports.

Fixed

  • Bank transfer form fields no longer fail with a 500 when Aeon's required-field endpoint returns 404: the request now matches Aeon's documented shape and falls back to a verified corridor catalog. (#480, PRD-18)
  • Deposit details for an unsupported currency or a wallet that is not ready return a clear precondition error instead of a 500; USD and EUR deposits are unchanged. (#484, PRD-19)
  • Identity verification no longer hardcodes USD when opening a Noah onboarding session, and a rejected fiat option surfaces as a support-facing error rather than a 500. (#479, PRD-20)
  • Business balance sync verifies Particle universal-account addresses again by sending the SDK's request envelope; vendor error details are preserved for diagnosis. (#478, PRD-21)
  • Solana balance reads in the swap screen retry transient RPC failures with a timeout and surface a retry action on mobile instead of failing the screen. (#483, PRD-23)
  • Cancelling a money request works in production again: migration 0161 adds the missing `cancelled` status value that an earlier duplicate-numbered migration never applied. (#481, PRD-24)
  • Next.js no longer attempts to create an image disk cache on the server, removing the EACCES unhandled rejection on `/_next/image`. (#477, PRD-25)

Added

  • Contact import shows loading and saving progress for large Google and phone address books; the Add, Import and Google Contacts actions share one style; contact rows offer Send; a payee chosen from Recipients keeps their name through the send flow; the send method tab reads "Your contact". (#486)

Operations

  • This release applies one database migration (0161). The Noah bank-deposit recovery change (#482) is held back pending verification with production account data.

0.36.0.0

Every public page, article and widget now leads with the global USD account: hold USD, receive and send euros, pay like a local in 100+ countries. Card-first pitches are gone from personal surfaces; business pages keep their payroll, invoicing, treasury and cap-table narrative.

Changed

  • Marketing claims ledger: new approved constants for the account positioning, the 100+ countries reach, the primary and secondary calls to action and the personal product summary, each with evidence and an audience switch (personal / business) so every widget draws copy from one source. (#474)
  • Shared CTA widgets (article closer and rail, blog and SWIFT signup bands, bank-directory band, bank page CTA sub-lines) and the homepage, root metadata and Open Graph image now use the account narrative; the bank CTA sub-line no longer repeats the button. (#474)
  • All 3,118 generated articles (country, living, send-money, places, card-abroad, comparison) and the 50 authored guides close with one account band; inline calls to action are topic-specific, comparison articles get real comparison headings, the 64 getting-paid guides and generic learn guides are personal, and the quality scorer no longer rewards repeated positioning copy. (#475)
  • 92 public route families (FX calculators and 244 corridors, send-money, SWIFT, IFSC and routing-number directories, coverage, compare, personas, products, learn, 2,180 stock and crypto pages, quotes, tracking, founders), their metadata, seven Open Graph generators, the about page, the onboarding welcome and both LLM mirrors now render the positioning line once per page in a closing account band; business routes keep the business summary. (#476)

Fixed

  • Removed unapproved claims from touched pages: "180 countries", "no FX markup", "platform is free", "real bank account" and "banking license" wording, and a competitor row that carried Glide copy. (#474, #476)
  • Catalog-wide regression guards: every generated and authored article and 2,783 server-rendered routes are checked for exactly one positioning line, a closing band, no slogan or button label used as a heading, no card pitch in personal closers and no bank identity applied to Glide. (#475, #476)

0.35.0.2

Fixed

  • Android: 16 KB memory-page compliance for Google Play production. JNA 5.14.0 → 5.18.1 in the enclave-signer module; shipped as Play build 1.0.2 (17). No web changes. (#473)

0.35.0.1

Changed

  • Site title, meta description and PWA manifest now read "Glide — Global USD Account. Hold USD, receive and send Euros, pay like a local in 100+ countries."

0.35.0.0

Personal accounts now move money straight from their own Privy wallet: deposits, sends, swaps and bank payouts on web and in the Expo app all use the verified embedded wallet, with a currency-first send flow modelled on Wise.

Added

  • Currency-first bank send on web and Expo: pick the receiving currency, then the recipient, amount, a live Noah quote, and an explicit wallet confirmation. USD ACH/Fedwire and EUR SEPA are enabled; other Noah payout channels are discovered at runtime and rendered from Noah's own form schema, so unavailable corridors never show a made-up method. (#470)
  • Recipients: searchable list with currency and category filters, person/business account-holder type, currency-specific bank details (USD postal address + checking/savings, EUR payout reference), editing, archive and exact-recipient reuse. Renamed from Beneficiaries in the consumer navigation. (#470)
  • Contact import from the phone address book, Google Contacts and CSV, on Send and on Recipients, with one-click import and owner-scoped search. Typed email or phone recipients can continue without a saved contact; unknown accounts still fail before any funding. (#470)
  • Live statements: monthly activity and bank-details PDFs from real Noah USD/EUR account data, month choices following account tenure. (#470)
  • Wise-style profile hub on web and Expo: avatar entry, other accounts, grouped settings, full-page detail editing, live inbox, and wallet backup in Security via Privy's secure key export after email step-up. (#470)
  • Expo web preview with a web-specific Privy adapter; Expo bottom navigation is now Home / Pay / Swap / Recipients. (#470)
  • Web branding refresh: "Glide — Global Account for Crypto and Fiat", a PWA manifest and icons, and a sitemap that derives trust, skills and founders pages from their registries. (#471)
  • iOS 27 scene-lifecycle Expo config plugin so the app starts through a SceneDelegate as Xcode 27 requires. (#471)

Changed

  • Personal context no longer auto-provisions or selects a treasury vault; business vaults are unchanged. (#470)
  • Admin user details return only the KYC display name, never the raw verification payload. (#472)

Fixed

  • Money-safety fixes from the release review: deposit reads rebind the routing-aware Noah workflow; wallet sends record the recipient's receipt exactly once; wallet-funded payouts share one atomic finalizer across webhook, replay and poller; ledger balances stay withdrawable; business EUR payouts resolve entity ownership; six-decimal USDC balances are compared in raw units; preflight failures unlock the Pay button; bank instructions keep Noah references; personal statements no longer hide sends bound to a business bank account. (#470)
  • Security: phone-number payments resolve only to verified, unique phone identities; Noah recipient forms are encrypted with the secret vault and encrypted legacy recipients stay editable and payable. (#470)
  • Main CI is green again: the four stale drift guards, `pnpm audit` (0 high / 0 critical) and the OSV supply-chain scan (0 unignored). (#472)

0.34.1.0

Fixed

  • Envelope encryption (KYC identity data, bank accounts, beneficiaries, MFA secrets, partner webhook secrets) now uses Google Cloud KMS in the Glide GCP project instead of a dead AWS KMS key. AWS stays selectable for self-hosters via `KMS_PROVIDER=aws`. (#469)

0.34.0.0

Other products can now bring Glide payments into their own app. A partner pre-creates Glide accounts for its users by email, the users consent once, and the partner reads their KYC, wallet, US receiving account and payout history, gets signed webhooks, and pays them in batches from a Glide treasury.

Added

  • Partner API: `POST /api/partner/oauth/token` (client credentials issued by Glide itself), `POST /api/partner/users` (idempotent pre-creation with a claim link), `GET /api/partner/users/:id/{kyc,wallets,us-account,transfers}`, `POST /api/partner/kyc/:id/link`. Every call is scope-checked against the user's consent and audited (migrations 0155–0157). (#468)
  • Consent screen at `/partner/consent` and partner access management under Settings › Integrations.
  • Signed webhooks (`kyc.status_changed`, `us_account.provisioned`, `transfer.settled`, `transfer.failed`) from a transactional outbox, delivered with retries, an HMAC signature and SSRF-safe delivery.
  • Partner payouts: `POST /api/partner/treasury/transfers/batch` moves USD from a partner-bound treasury entity to each user's Glide balance in one atomic batch, with per-item and per-batch idempotency, caps and immutable receipts (migrations 0158–0159). Admin procedures bind, fund, cap and suspend a treasury.
  • A "Glide balance" block on the consumer home for USD received from a partner, with a path to convert and withdraw to a bank.

Changed

  • Pre-created accounts start without an account type; the owner picks it on first sign-in.
  • Sending to an email now records the recipient's side of the transfer too, so it shows in their history and can trigger their webhooks.

Fixed

  • Noah USD receiving accounts are now persisted in production (routing was missing from the provisioning call).
  • Currency conversion upserts use the NULL-safe balance index; personal balance lookups never pick up an entity balance.
  • Noah withdrawal failures refund exactly once; uncertain outcomes stay held for reconciliation.
  • Fresh installs match `schema.ts` (migration 0160 restores objects production already had).

0.33.0.0

You can send USDC to a bank account without finishing identity verification. The calculator prices the send the way Wise does — you send, they receive, fee and arrival on the same screen — then collects the right bank details for ACH, wire, SEPA, Faster Payments, PIX, SPEI, or Interac.

Added

  • Bank send on Pay and on the dashboard Bank transfer tab. Pick an amount, choose who gets it (someone else, a business, or yourself), fill the corridor form, review, send. Recipients save for next time.
  • Live Noah sell-channel quotes when the corridor is entitled; an instant local fee table when Noah is slow or the account is not entitled, so the rate line never goes blank.
  • Postal address and account type on saved recipients (migration 0155), which US ACH and Fedwire require.

Changed

  • Bank payouts no longer wait on KYC. Cards still do. Home and Pay copy say so.
  • A send holds the USDC you typed. The rail fee comes out of what the recipient gets, not as a second debit on top.

Fixed

  • Noah payouts no longer send a Glide user id as the Noah CustomerID, which made non-KYC sends look like a KYC miss.
  • A leftover Aeon tag can no longer steal a Noah bank send.

0.31.0.0

Customers who get stuck mid-KYC can restart from the beginning without waiting for an operator. The admin panel moves off `glide.co/admin` onto its own host.

Added

  • **Restart from the beginning** on personal identity verification (dashboard, settings, and the mobile KYC screen). A stuck, expired, or rejected hosted session mints a new HostedURL on the same vendor applicant — Noah keys customers by Glide user id, so a second CustomerID would look like a duplicate. Approved identities and suspended or closed accounts are refused. Restarts are rate-limited to 5 per hour
  • Admin panel at **admin.glide.co**. `glide.co/admin/*` 308s there. Founder inboxes `darshan.bathija@gmail.com` and `darshan@glide.co` are super-admins even before `users.type` is `admin`; extra addresses can be added with `GLIDE_SUPER_ADMIN_EMAILS`

Changed

  • Admin login skips embedded-wallet creation so Privy does not throw on `admin.localhost` / `admin.glide.co`
  • Localhost keeps `/admin` as a subdirectory: Privy treats only exact `localhost` as a secure origin, so redirecting local admin onto `admin.localhost` would break sign-in

Fixed

  • Once KYC status was `in_progress` or `review`, the dashboard and settings pages hid every CTA, so a customer who hit a vendor error had no way to continue

0.30.0.0

The US routing-number directory had 10,548 institution pages built, live, and submitted — and only 34 of them had ever recorded a Search Console impression. This release fixes why, and adds the bank-by-state pages the demand was actually asking for.

Added

  • Bank × state routing pages at `/routing-number/<bank>-<state>` — 44 demand-verified pages across Chase, Bank of America, and Wells Fargo (`routing number chase california` alone is 33,100/mo). Every routing number is asserted from the bank's own published page and then verified against the FedACH dataset before it ships: 9-digit format, ABA checksum, present in the routing index, and owned by an entity in the same bank family. Rejected rows are printed by the generator and never written
  • A–Z bank directory at `/routing-number/banks` and `/routing-number/banks/<initial>` — 28 pages that give all 10,549 institution pages a real internal link, two clicks from the hub. Letter pages are statically generated and deliberately unpaginated so no bank sits more than two hops deep
  • `helpLinks` slot on the shared bank-page template, so every institution page cross-links the routing-number checker and the where-is-it-on-a-check explainer — both 165,000/mo terms that previously had almost no internal links pointing at them

Changed

  • The ACH sitemap shard is now ordered by demand instead of alphabetically. It previously opened with `1199-seiu-federal-credit-union`, `121-financial-credit-union`, `167th-tfr-federal-credit-union`, so a crawler working down the file never reached the banks people search for — which is exactly the pattern the impression data showed. It now opens with Chase, Bank of America, Wells Fargo, Citi, and U.S. Bank, with sitemap priority tiered to match
  • Bank pages surface their state routing pages as a coverage facet, on the axis people actually search

Fixed

  • Three institutions whose slug is also a featured alias (`wells-fargo`, `pnc-bank`, `u-s-bank`) are no longer submitted in the sitemap. Those URLs render the featured bank and carry a canonical pointing at it, so advertising them spent crawl budget on a duplicate. They remain resolvable
  • State pages can never serve a servicing-office number in place of a customer routing number. The FedACH `state` field records where an institution's ACH office is registered, not where the account was opened — Chase has 44 records stamped FL and one stamped CA — so deriving state pages from it would have answered "Chase routing number California" with a Northridge branch number

0.29.0.0

Glide Equity replaces a standalone cap-table tool with a complete, organization-scoped equity workspace inside Glide.

Added

  • Full Equity product at `/equity` with overview, cap table, stakeholders, share classes, equity plans, shares, options and RSUs, transactions, fundraising instruments, files, data rooms, eSign, updates, reports, audits, team administration, company settings, Form 3921, and 409A workflows
  • Two activation paths: create a cap table from scratch or migrate a reconciled five-workbook Carta export with byte-exact source archives, cross-report validation, predictable retries, multiple-plan preservation, and guided legal-term confirmation
  • Holder invitations and equity acceptance, with server-enforced self-only portfolios and documents for members while organization owners and equity administrators retain the complete company view
  • Mutable, auditable equity operations for issuances, grants, exercises, forfeitures, transfers, conversions, valuations, financing rounds, investor updates, document assignment, secure data rooms, and ordered or parallel electronic signatures
  • Responsive Glide navigation and product surfaces for desktop, tablet, and mobile, including accessible dialogs, empty and failure states, and public invitation, data-room, and signing experiences
  • Cap table and equity reference silo: 40 plain-English guides under `/equity/<slug>` with a hub at `/equity/guides`, plus 8 free client-side calculators (cap table builder, dilution, vesting, SAFE conversion, option exercise with AMT, exit waterfall, founder split, token vesting) and an even-handed Carta comparison and alternatives cluster (#446)
  • Cornerstone guide: migrating a Carta cap table to Glide Equity
  • Business payments repositioning: the marketing site now leads with payroll, invoicing, cap table, and payments; new `/payroll` landing page with a fee calculator, `/product/invoicing` and `/product/cap-table` hubs, and a two-dropdown header nav (#445)
  • Signup intent: business signups are asked what brings them to Glide — banking or cap table; cap-table signups land directly on Glide Equity after setup, with Equity first in their navigation (#450)

Changed

  • Existing `/captable` links now lead to Glide Equity, and the primary Glide navigation uses the Equity product name and branding
  • Regulatory positioning moved to a partner model: registration-number disclosures replaced by partner-performed regulated legs, with every regulatory assertion routed through a single claims ledger (#445)
  • Equity guides and calculators now say the product is live and route readers to Glide Equity signup; comparison pages updated from "in development" to the shipped, equity-only product with no pricing or customer claims invented (#448, #449)

Fixed

  • Equity authorization, plan capacity, invitation acceptance, imports, ledger changes, audits, and signing ceremonies are transaction-safe under concurrent requests
  • Public access links are tenant-scoped and state-aware; signature links expire, are single-use, stop serving documents after terminal states, and support safe resend after delivery failure
  • SAFE conversion calculator no longer rejects valid cap tables (solver replaced with exact bisection) and no longer undershoots share counts; AMT breakeven returns whole option counts; cap-table CSV export neutralizes spreadsheet formula injection (#446)
  • `/payroll` fee table matches the quote engine exactly — USDC payout fees split by chain instead of claiming "free" on chains that charge (#445)
  • Marketing redirect targets survive first-time signup: visitors arriving from a cap-table guide now reach Glide Equity after account creation instead of being dropped on the banking dashboard (#450)

0.28.1.0

Removed

  • The 9 remaining CoinGecko-priced coins (KAU, BORG, Mantis, NXM, PEANUT, USDA, JAAA, OHM, BUIDL) — every listed cryptocurrency is now priced directly from an exchange (#444)

0.28.0.0

The crypto directory gets sharper: every listed coin is one people actually search for, priced from real exchanges.

Added

  • Multi-exchange price venue chain — Binance, Hyperliquid, OKX, KuCoin, Gate, MEXC, Kraken, Coinbase — covering 167 of 176 coins with direct exchange pricing (#443)

Changed

  • Crypto universe revised to 176 demand-backed coins based on real search-volume research; zero-search institutional and RWA tokens removed, notcoin/Toncoin/Polygon/TURBO/DOGS added (#443)

Removed

  • 24 zero-demand crypto pages and the unpriceable TLTx xStock (#443)

0.27.1.0

Crypto market data goes Binance-primary: every coin refreshed with real prices, and the CoinGecko rate-limit gap is closed.

Added

  • Binance as the primary crypto price and chart-history source (108 directly mapped pairs), with per-asset source provenance (#442)

Fixed

  • Crypto refresh gap: 209 of 210 crypto assets now carry live data (was 96/200 + 0/10 xStocks); CoinGecko demoted to a properly paced slow lane for market cap, supply, and unlisted assets (#442)

0.27.0.0

The content engine lands: 25 send-money corridor pages, 13 more B2B guides, and editorial profiles on 170 of the most-searched assets.

Added

  • /send-money hub plus 25 country corridor pages with above-the-fold calculators, provider fee comparisons with as-published citations, and receive-side rail guides; five countries disclose indicative-rate fallbacks (#439)
  • 13 tranche-2 B2B education articles (CHAPS vs BACS, payment rails, AP automation, working capital, and more), all adversarially fact-checked (#438)
  • Editorial profiles on 170 top assets across /stock and /crypto pages — company/protocol background, FAQs with structured data, zero advice language (#440)

0.26.0.0

Stock pages grow up: real fundamentals, eight chart ranges with a hover crosshair, cleaner /stock URLs, and embeddable widgets for partners.

Added

  • Market cap, P/E ratio, and average volume on every equity asset page, refreshed daily (#436)
  • Chart ranges 1D / 1W / 1M / 6M / YTD / 1Y / 5Y / All with an accessible hover crosshair — mouse, touch, and full keyboard navigation (#436)
  • Embeddable FX calculator and bank-lookup widgets at /embed/\*, with a self-serve embed generator at /partners/embed (#437)

Changed

  • Public asset URLs moved from /invest/_ to /stock/_ with permanent redirects; sitemap and internal links updated (#436)

0.25.0.0

Live market data arrives: 2,211 asset pages across 11 equity markets and crypto, plus the famous bank codes people actually search.

Added

  • Stock pages for the top 1,000 US companies and the top 100 in each of ten more markets (India, Japan, UK, China, Hong Kong, Canada, France, Germany, South Korea, Taiwan) — server-rendered prices, key statistics, and charts, with tokenized xStock cross-links where Glide offers the asset (#BUILD2).
  • Price pages for the top 200 cryptocurrencies with live data and layered risk information (#BUILD2).
  • 23 famous bank-code pages (CHASUS33, BOFAUS3N, routing numbers 021000021, 026009593 and more) promoted from noindex with genuinely differentiated code-level guidance; the long tail of thin code pages stays excluded by design.

0.24.1.0

Added

  • 15 business-education articles: paying international vendors and contractors, FX risk management, multi-currency accounting, international payroll, invoicing, AR/AP, reconciliation, treasury, interbank rates, wire-fee tactics, and a cross-border payments checklist (#BUILD4).

Fixed

  • The 304 highest-traffic bank pages are statically pre-rendered again (fast loads restored) via config-level route rewrites, after a middleware-based rewrite failed behind the TLS proxy in production.

0.24.0.0

The currency converter grows up: ~340 live-rate corridor pages covering every currency pair people actually search for.

Added

  • Currency converter pages for ~170 demand-verified currency pairs in both directions — live mid-market rates, conversion tables, per-pair currency facts, and dense cross-linking between related pairs (#BUILD1).
  • Heading-hierarchy, cache-header, and metadata-length fixes across marketing and directory pages.

0.23.1.1

Fixed

  • Social share images now appear on the homepage, directory hubs, blog archives, and FX corridor pages, and article share images resolve correctly (#433 follow-up).
  • Blog tag links use canonical slugs; /login is excluded from search indexes; long bank names truncate cleanly in titles and descriptions.

0.23.1.0

Mobile money actions come alive: Pay, Swap, Earn, and Invest now lead somewhere real on the app, with the safety rails a money path deserves — and both security CI gates are green for the first time since 08-05.

Added

  • Mobile Pay: one Send entry with a method chooser (Glide contact, email or phone, wallet address) and a working Request Money flow with sharing, history, and cancellation (#432).
  • Mobile Swap: reads your signing wallet's on-chain balances before quoting, offers 25/50/75/100% amount controls, and signs, broadcasts, confirms, and records real Jupiter swaps (#432).
  • Mobile Earn & Invest: pools are selectable as auto-earn targets, eligible Solana investments execute end-to-end from the app, and Predictions is gone from Earn (#432).

Fixed

  • The app shows the actual executed quote — estimated out-amount and price impact — before you sign a swap or buy, and blocks signing when the price moved past your slippage tolerance (#432).
  • An ambiguous failure after broadcast can no longer double-swap or double-buy: the app retries the idempotent confirm, tells you the transaction was submitted, and never re-arms the form (#432).
  • Vault-custody accounts can't buy around their multisig anymore: the mobile buy path enforces the same vault-routing gate as every other execution path (#432).
  • SOL-legged swaps record correctly (native SOL flows were invisible to the recorder), swap history records USD amounts instead of raw token units, and confirm calls are replay-proof under concurrency (#432).
  • Swapping with an SPL token but no SOL for network fees now says so up front instead of failing at broadcast (#432).

Security

  • Both security CI gates are green again: pnpm 9's silently-dead bare-major override selectors rewritten as explicit ranges, 14 high advisories ratcheted (js-yaml, fast-uri, nanoid, brace-expansion, undici, next 16.2.11, postcss, sharp, socket.io-parser), hono 4.13.1 + body-parser 2.3.0 + tar 7.5.21 for the OSV gate, and the two unfixable image-size CVEs registered as accepted risk (#434).

0.23.0.0

The marketing site grows up: a redesigned homepage with real product demos, 40 researched bank guides, 13 in-depth money-movement articles, and the FX calculator works again.

Added

  • Deep, fact-checked guides on 40 top bank pages across the IFSC, SWIFT, and routing-number directories — every claim verified against primary sources (bank fee schedules, RBI, the Fed), with visible FAQs backed by matching structured data (#433).
  • 13 long-form articles: seven Learn pillars (SEPA, SWIFT wires and correspondent banking, ACH, India's UPI/IMPS/NEFT/RTGS rails, IBAN with a worked checksum, stablecoin accounts, how Glide keeps money safe) and six blog cornerstones (multi-currency accounts, crypto debit cards, an honest no-KYC explainer, sending money to India, stablecoins for cross-border payments, Wise alternatives) (#433).
  • Routing numbers for fintechs and credit unions missing from FedACH: BECU, Chime (with its dual partner banks), SoFi, and Varo get real pages; DCU, Green Dot, and PenFed now resolve as aliases to their chartered entries (#433).
  • Redesigned server-rendered homepage with seven recorded product demo clips, plus a marketing component kit (FAQ accordions, comparison tables, step flows, stat strips) shared across all marketing pages (#433).
  • A dedicated routing-number checker page and a visual "routing number on a check" explainer; blog gains pagination for all 3,139 posts, tag and author archives, and an RSS feed at /feed.xml (#433).

Fixed

  • The FX calculator and send-money calculator work again on all 44 corridor pages: the public quote call no longer requires a signed-in wallet session (#433).
  • IFSC search no longer returns the same branch twice for exact-code queries (#433).
  • Canonical URLs and social-share links are now absolute; every major page type gets a proper Open Graph image (the site previously had none); SWIFT pages no longer emit FAQ structured data without visible FAQs (#433).

0.22.7.0

Send before you verify, paste any wallet address, claim your @tag at signup — and cross-chain works again.

Added

  • Send and receive money without completing identity verification: transfers and money requests between Glide users are book transfers with no bank partner in the loop, so they no longer sit behind KYC. Bank rails — ACH, wire, SEPA, bank transfer, card push, QR payout, payroll — keep their gate, now enforced server-side on the resolved rail and currencies rather than any client-supplied label (#429).
  • Paste-to-send on web and mobile: every recipient field gains a Paste button, and a pasted EVM or Solana address routes to the on-chain rail with chain auto-detection. Previously anything without an `@` was parsed as a phone number, so pasted addresses were submitted as doomed phone sends (#429).
  • Mobile on-chain send at `/pay/wallet`: proposes through the same vault multisig path as web (never broadcasting from the embedded EOA), including EVM→EVM cross-chain via CCTP with an always-explicit destination-network picker. Solana↔EVM is refused with an explanation rather than mis-routed (#429).
  • Reserve your Glide @tag during first signup: a dedicated step after OTP with live availability checking; skip is remembered permanently, returning users are never intercepted, and claims are atomic under races. The consumer avatar now opens Settings (with Sign out one hop away) instead of signing you out (#431).
  • Request money on mobile is real: it creates requests via the same backend as web, lists history, cancels pending ones, and shares server-derived paylinks. The broader dead-button audit wires split, add money, vault create, invoices, expenses, batch transfer, and vault access/members to real endpoints — or makes them say honestly that they're web-only (#430).

Fixed

  • Cross-chain sends were dead on every surface: the Particle smart-account name `'glide'` is rejected by the live API, so universal accounts never initialized and every attempt fell to a "try again shortly" that never recovered. Web and mobile now share the accepted `UNIVERSAL` constant from `@glideco/shared` (#429).
  • Two Hermes-only crashes caught before shipping: React Native's missing `TextDecoder` now honors `{fatal: true}` (borsh relies on the throw to detect corrupt data), and `structuredClone` is guarded (#429).
  • Vault owner and threshold changes from mobile now use per-wallet Safe quorum, preserve the current quorum when adding a signer, and fan proposals across every enabled chain with partial-failure reporting (#430).

0.22.6.1

Cold-start brand identity: the archived purple Defx splash is gone, and first-time open gets a short cream wordmark intro.

Fixed

  • Native splash and favicon no longer show the archived purple Defx mark — they use the current cream wing on forest green, with splash background `#1D4A38`.

Added

  • First-launch brand animation: a single centered cream wordmark (arrow + Glide) rises in on forest green, then the tagline, then soft exit. Plays once per install via AsyncStorage.
  • HTML preview at `apps/mobile/previews/startup-brand-animation.html` for design review without a device build.

Changed

  • In-app boot / Privy splash screens use the current cream-and-green brand marks instead of a cream spinner, so cold start never flashes legacy identity.

0.22.6.0

Hosted identity verification on both platforms, and a search index keyed on the axis people actually search.

Added

  • Identity verification runs the provider's own hosted session inside the app — an in-app WebView on mobile, an embedded iframe on web — instead of bouncing you out to Safari (#427).
  • Our own capture flow is kept as the fallback for a provider that returns no hosted session, with identical behaviour on web and mobile (#427).

Changed

  • Verification no longer asks for legal name, date of birth, nationality, address and document details a second time: the provider collects them once, in their session (#427).
  • An already-approved account (201) or a review still running (202) is read as success rather than failure — the retry prompt those used to show risked a duplicate application being rejected (#427).
  • Directory pages are indexed on the bank-name axis instead of the code axis: the sitemap goes from 404,439 to 53,389 URLs (14 shards to 4). Leaf code pages still resolve and stay linked from their bank page, now `noindex,follow` so their equity flows upward. Demand research put "bank of baroda ifsc code" at 301,000/mo against 110/mo for an actual branch code.
  • Privy and Particle providers moved out of the root layout into per-group layouts, so static directory pages stop shipping wallet SDKs they never call: a directory page drops from 4,099,513 bytes / 31 files to 646,391 / 10 (−84%). `/login` and `/dashboard` keep the full stack.

Fixed

  • Mobile aborted at boot when `EXPO_PUBLIC_PRIVY_APP_ID` was absent — `PrivyProvider` renders children without mounting the SDK, and all nine `usePrivy()` consumers destructured a null context before any UI rendered (#427).
  • Web `/get-started` read `link?.url` while the mutation returns `kycLink`, inside a swallowing `catch`, so it never opened a verification session at all (#427).
  • CSP `frame-src` now lists the verification host, so the planned flip from report-only to enforce will not blank the verification panel (#427).
  • Hosted-session header drew under the iOS status bar — a `Modal` renders in its own native hierarchy that the root `SafeAreaProvider` never measures (#427).
  • The iOS camera permission prompt explains ID and selfie capture instead of claiming the camera is only for WalletConnect QR codes (#427).
  • `robots.txt` advertises all 14 sitemap shards; a hardcoded `slice(0, 8)` had hidden shards 8–13 (~160k IFSC and blog URLs) from crawlers that do not read the sitemap index.

0.22.5.1

Blog quality elevation — independent scorer ≥9.5 content / 10 design vs Wise + Borderless.

Added

  • `quality-score.ts` independent multi-dimension scorer + corpus gate test (content ≥9.5, design ≥9.5).
  • `elevate.ts` universal post-elevation: week-one calendar, fee-stack anatomy, true-cost matrix, mid-market/DCC pedagogy, emergency playbook, expanded FAQs.
  • Article on-page TOC, breadcrumb JSON-LD, currency chip on post meta; deeper places money logistics.

Changed

  • Country, fintech, places, and cornerstone generators all run through elevation for consistent depth.
  • BlogSignupCta deep-links to `/login?redirect=/select-type`.
  • FX corridor pages (`/fx-calculator/[corridor]`) render real per-corridor content — rate explainer, sending guidance, five sections and a FAQPage JSON-LD block — instead of a bare converter widget (#426).

Fixed

  • The elevated blog corpus was being built into every request, hanging the site under load; it is now loaded only where it is actually rendered (#425).

0.22.5.0

Public multi-category travel & fintech blog — Wise-competitive SEO content marketing on Borderless brand.

Added

  • Public `/blog` hub with category indexes and generated posts across travel, living abroad, send corridors, cards abroad, students/family, work/visas, and compare.
  • Country guide generators: 10 travel + 10 living articles per eligible country (50+ travel / 60+ living countries) with rail pedagogy, enrichments, and FAQ depth.
  • Places guides: top tourist cities (top-10 places) and top-country place hubs (top-25 places).
  • Fintech expansion packs: send corridors, cards abroad, students/family, work/visas, compare content.
  • Blog UI: PostCard, BlogNav, BlogBlocks, BlogSignupCta (deep-green band) under public layout.
  • Blog URLs in core sitemap; Blog link in marketing nav/footer (alongside PR #422 IA).
  • Vitest suites: `blog-content`, `blog-places`, `blog-fintech-expansion`.

Changed

  • Public layout: Blog added to marketing IA nav/footer after Solutions / Product / Coverage / Trust / Tools / Learn.
  • Sitemap core partition includes blog hub, categories, and all post slugs.

0.22.4.0

Customer marketing IA — use cases, coverage, trust, product hubs, tools, learn, and compare pages, with CTAs into the real app and corridors from the live FX catalog.

Added

  • Public solutions hub `/for` and use-case pages (freelancers, startups, crypto teams, agencies, DAOs, agents).
  • Coverage map: `/coverage` countries, currencies (registry-backed), corridors (from `TOP_CORRIDORS`), and chains.
  • Trust pages: custody, multisig, compliance, and “if Glide vanishes” survival guide.
  • Product hubs: send, cards, convert, earn — each with login redirects into consumer and business app routes.
  • Tools: corridor explorer (full FX catalog + convert deep links), IBAN checker, card eligibility.
  • Learn guides: SEPA, FPS, multisig, agents, stablecoins, rails, EMI vs bank.
  • Compare extensions: traditional bank, Wise-class, self-custody vs custodial.
  • Shared marketing chrome + `lib/marketing/*` (app-links, coverage, content, IBAN validate).
  • Vitest coverage for IBAN validation, corridor wiring, and login-redirect app links.

Changed

  • Public nav/footer links Solutions, Product, Coverage, Trust, Tools, Learn, Compare.
  • Compare and alternatives CTAs use `/login?redirect=/select-type`.
  • Sitemap core partition includes the new marketing URLs.
  • `.gitignore` only ignores root `/coverage` so the marketing `/coverage` route can be committed.

0.22.3.0

Worldwide SWIFT/BIC code SEO directory — every bank, every country, full sitemap.

Added

  • Public `/swift-code` hub with server-side search across ~112k BIC codes and ~39k banks in 232 countries.
  • Country pages (`/swift-code/country/{slug}`), bank pages, and dual bank/BIC routes with brand aliases (e.g. `/swift-code/chase`).
  • Multi-part SEO sitemaps via `generateSitemaps` covering every country, bank, and BIC URL (8-char + XXX forms).
  • `robots.ts` allowing crawl and advertising the sitemap index.
  • Signup CTAs for international payments on every SWIFT surface.
  • Import pipeline: `pnpm swift:import` / `scripts/import-swift-codes.mjs` from MIT public BIC dataset.

Changed

  • FX calculator and send-money calculator link into the SWIFT directory for internal SEO.

0.22.2.0

Mobile stability follow-up to 0.22.1.0: fixes the TestFlight build 7 crash, refreshes the app icon, and hardens Privy OTP login.

Fixed

  • iOS 26+/27 mid-session crash on native-stack unmount: `react-native-screens` `setViewToSnapshot` no longer reassigns the controller view, and profile refetches no longer unmount the shell (stale-while-revalidate with session-bound caching; revoked sessions are evicted) (#412).
  • Privy OTP failures now surface actionable error messages; the new allowlist checker fails closed (including on rate limiting) and gates store submissions (#411).
  • Pod targets floored at iOS 15.1 so builds succeed on Xcode 26+ (#409).

Changed

  • App icon updated to the cream/green brand mark on iOS and Android (#408).
  • CI: test job timeout fits the grown suite; fork-canary scaffold matches the tightened skill schema (#410).

0.22.1.0

Security-hardening release: lands the comprehensive P1 audit (#406) — 21 audit findings plus 40 review findings closed across MCP, web, mobile, connectors, and the migration pipeline — and removes the temporary consumer-send cap (#407).

Security

  • MCP approvals now bind the complete action (recipient, chain, asset, amount, batch contents) via a canonical digest at mint and redemption — approval substitution is rejected; money-movement USD values are resolved server-side and forged client hints fail before persistence (#406).
  • Production JWKS retrieval requires HTTPS; internal grant/tenant responses are strictly schema-validated; rate limiting fails closed with timeouts (#406).
  • Entity membership acceptance is tracked explicitly — pending invitations no longer pass authorization-bearing reads (multisig, invoices, beneficiaries, gas tank, treasury) (#406).
  • Vendor connectors fail closed on malformed responses (Gnosis Pay, Stripe Issuing limit merges), sandbox credentials are isolated from live ones (Wirex), and Monerium OAuth state is AES-256-GCM encrypted so the PKCE verifier is no longer recoverable (#406).

Fixed

  • Money movement is retry-safe: stable idempotency keys with entity/personal scoping, atomic CAS state transitions, and webhook settlements that release held balances exactly once with unmatched events left retryable (#406).
  • Mobile: deep links no longer crash the navigation parser, the wallet screen no longer refetches in a loop, first-login KYC can't be skipped by a navigation race, push registration recovers after permissions are enabled, and fiat balances use a consistent available-only basis (#406).
  • Migration runner rebuilt: checksum ledger, lease heartbeats, concurrent-index recovery, explicit baseline import, read-only dry-run, PostgreSQL 16 floor (#406).

Changed

  • Removed the $50 consumer-send threshold — sends of any amount proceed through the standard controls (KYC, rate limit, idempotency, balance) (#407).

0.21.1.0

Removes the `glide install` CLI command — the local installer that wrote MCP server entries into 7 agent-runtime config files (Claude Desktop, Cursor, Codex, etc.). Superseded by adding the remote MCP endpoint directly to a client's config, which needs no local CLI step. The shared `installers/` module that `install` and `onboard` both used is untouched — `glide onboard -y` still drives the same runtime-config writers as part of its combined setup flow. All other operator commands (`demo`, `doctor`, `migrate`, `docs:env`, `partner submit`, `consistency`, `deploy`, `secrets sync`, `policy lint`, `onboard`) are unaffected and intentionally retained.

Removed

  • `glide install` command (`packages/cli/src/commands/install.ts` + its test suite).

Changed

  • `docs/FEATURES_BUSINESS.md` — "One-command MCP installer for 7 agent runtimes" bullet replaced with the remote-MCP onboarding instruction.

0.21.0.0

Glide × Defx web redesign. The entire web app (every consumer, business, auth, and settings route) is restyled onto the Defx V3 glass design system via a new shared primitive layer (`apps/web/src/components/ui/*` — Pane, Btn, Pill, Input, Seg, Tabs, Switch, Sparkline, Money, OTP, an inline-SVG icon set, and the brand lockup) plus expanded `glide-*` recipes in `glide-tokens.css`. The grey-frosted primary CTA is preserved (the one intentional divergence from Defx's electric blue, per DESIGN.md). tRPC wiring is unchanged across every screen — layout changed, behavior did not.

Changed

  • Consumer surfaces moved from the `(consumer)` route group (which served them at the URL root) to a real `/consumer/*` segment, so the in-app `/consumer/*` nav + post-login links resolve. 308 redirects added for the old bare-root paths (`/home`, `/wallet`, `/pay`, `/swap`, `/earn`, `/card`, `/beneficiaries`, + sub-routes) to preserve external/bookmarked links.
  • Consumer top-nav gains a Wallet tab (Home · Wallet · Pay · Swap · Earn · Card); business sidebar regrouped (ACCOUNTS · MOVE MONEY · TREASURY · OPERATIONS · SETTINGS).
  • Dropdown/select consistency: added a canonical glass `Select` primitive and normalized 49 native `<select>` classNames onto the `glide-input` glass recipe so every dropdown trigger reads identically. A two-surface audit confirmed dropdown wiring at 91/91 (web) + 37/37 (mobile) = 100% — every selector has a real handler + populated options.
  • Mobile redesign primitives: shared `apps/mobile/src/components/ui/*` glass primitive set + `GlassTabBar`, mirroring the web Defx layer; mobile selectors already share one glass vocabulary (`MobileBottomSheet` + `Seg`/`Tabs`), zero native OS pickers.

Fixed

  • Empty-state crash hardening across rebuilt screens: consumer-card `categoryLocks`, gas-tank `BigInt`, savings counter, and dashboard number formatters all guard undefined / non-finite server data.
  • `Sparkline` gradient ids are now per-instance (`useId`) — two same-color, equal-length sparklines on one page no longer collide on a duplicate SVG gradient id (the second fill was resolving to the first gradient).
  • `Switch` primitive is keyboard-operable (Space/Enter + `tabIndex`); amount-entry inputs show a `:focus-within` ring; the spending categorical palette drops the off-system purple-violet tone.

0.20.0.5

Mobile shipping push. iOS Glide.app now boots, signs locally on a free-tier Apple ID, and reaches the real login screen on iPhone 16 Pro hardware. The branch carries every shippable mobile change from v0.18.0.0 through v0.20.0.5: free-tier signing, a hand-rolled React Navigation 7 routing stack to replace expo-router, 80 new mobile screens (35 → 115) mirroring the web app surface-for-surface, the Defx glass + smoke design system applied across every screen, and the boot-crash + icon + glass-pane fixes that turn it from "compiles" into "renders."

Added

  • **iOS free-tier sideload path.** Personal Team signing builds and installs to a paired iPhone without a paid Apple Developer account. App Attest, APNs push, Sign in with Apple, and the `aps-environment` entitlement are gated off via `EXPO_PUBLIC_APP_ATTEST_DISABLED` and a new `with-free-tier-entitlement-strip` plugin so Xcode signing succeeds on a free Apple ID. The same flag short-circuits runtime calls to App Attest and push-token registration so signing-path code is dead until a paid certificate ships.
  • **Hand-rolled React Navigation 7 routing stack** (`apps/mobile/src/lib/expo-router-shim.ts` + `route-table.ts`). `expo-router@5.0.7` rendered an empty `<View/>` under React 19.1 because it was never tested against React 19's concurrent rendering. The shim translates the `router.push/replace/back/canGoBack/setParams`, `useRouter`, `useLocalSearchParams`, and `<Redirect>` API one-to-one onto `@react-navigation/native-stack` + `@react-navigation/bottom-tabs`. Metro resolves `'expo-router'` to the shim path, so 29 existing import sites compile unchanged. Single source of truth for routes lives in `ROUTES` — both the regex resolver and `LINKING_CONFIG` derive from it.
  • **App V2 — 80 new mobile screens** mirroring the web app surface-for-surface (35 → 115). Consumer surfaces: Wallet tab + per-asset detail, Pay (send / bank transfer / QR), Swap with UniversalSwapPanel, Earn (vaults / invest / predict / portfolio), Cards, Beneficiaries detail, transaction detail with backoff polling, wallet confirm-export. Business surfaces: entities + invites + members, vault create + settings + activity, the full Vault Controls IA (Access · Backup · Rules · Operations), Send / Convert / Add money / Request, org multisig defaults, settings shell with 10 sections (general · my-account · workspaces · members · security · notifications · integrations · billing · KYC · legal), multi-vendor KYC (5 vendor cards), Telegram pair magic-link landing. Plus P1/P2/P3 stubs for Payroll, Invoicing, Agents, Statements, Split, KOL earnings, Spending analytics, legal pages.
  • **Defx design system applied across every mobile screen.** Smoke + nebula backdrop fixed to the viewport, glass panes with alpha-white borders, Defxinter wordmark, Defx Mono for IDs, frosted-grey primary CTAs, semantic state colors (green deposits, red failures, amber pending), 12/16 radii.
  • **Glide brand-mark launcher icon.** Replaces the placeholder Expo icon with the dark-purple-on-lavender Defx glyph baked across `icon.png`, `adaptive-icon.png`, `splash-icon.png`, `favicon.png`, and the iOS `AppIcon` asset set.
  • **Mobile-side env routing via `Constants.expoConfig.extra`** with `process.env` fallback. The xcodebuild Bundle phase doesn't source `.env.local`, so `app.config.js` (which runs in plain Node with dotenv loaded) is now the source of truth for `EXPO_PUBLIC_API_URL`, `EXPO_PUBLIC_PRIVY_APP_ID`, `EXPO_PUBLIC_APP_ATTEST_DISABLED`. Privy auth and tRPC API URL now reach the Release JS bundle reliably.
  • **Persistent native patches** for free-tier compilation: `expo-localization@16.0.1`, `expo-crypto@55.0.10`, `expo-dev-menu@55.0.29`, `expo-standard-web-crypto@55.0.10` (Swift-side fixes for `@unknown default` enums, missing `StaticAsyncFunction`/`StaticFunction` APIs on expo-modules-core 3.0.29, `appContext.reloadAppAsync` → `bridge?.requestReload`, and `window` → `globalThis` in the WebCrypto polyfill). Persisted via `pnpm patch-commit` so `pnpm install` reapplies them automatically. Documented in `apps/mobile/patches/README.md`.

Changed

  • **Mobile route resolution semantics.** `router.push` and `<Redirect>` now route through the regex-based `resolveHref()` resolver. Group prefixes like `/(tabs)/home` are stripped before matching. Object-form `router.push({pathname, params})` is supported. Unknown paths throw in dev rather than silently falling through. A dispatch queue + `isReady` guard replays any navigation calls that fire before `NavigationContainer.onReady` (closes the first-render redirect race).
  • **Mobile splash + boot tree.** Lifted out of the now-deleted `_layout.tsx` files (`(auth)/`, `(tabs)/`, `(tabs-business)/`, top-level). The new `apps/mobile/src/App.tsx` mounts `SafeAreaProvider → PrivyProvider → PrivyTokenBridge → NavigationContainer → RootNavigator`, with `Sentry.init` and `useFonts` lifted into the same tree.
  • **Mobile bottom-tab structure.** Consumer is now 6 tabs (Home · Wallet · Pay · Swap · Earn · Cards). Business stays 4 (Treasury · Vaults · Approvals · More). Everything else reaches via deep-link drill-downs through the shared route table.

Fixed

  • **App-icon canvas.** Previous icon.png had artwork in only the top-left ~330×330 of a 1024×1024 PNG (qlmanage rasterized the SVG honoring its `width=96` attribute). iOS rendered the icon as a tiny purple glyph in an otherwise white square. Fixed by rewriting the SVG's `width`/`height` to 1024 before rasterization so the artwork fills the full canvas.
  • **Sentry boot crash.** `Sentry.wrap(AppInner)` on an uninitialized SDK lazy-inits the native side with default options, which crashes `EXC_BAD_ACCESS` in `+[SentrySDKInternal startWithOptions:]` on iOS. Gated `Sentry.wrap` behind a non-empty DSN check, with conditional `Sentry.init` only when a real DSN is provided.
  • **`@expo/config` `null → {}` mutation.** `getConfig()` silently mutates `null` values in `extra` into `{}` (an empty object) when serializing `EXConstants.bundle/app.config`. Downstream truthy checks misfire: `({}).trim` is `undefined`, so calling it throws `TypeError: undefined is not a function` at module-eval time. Hermes Release builds suppress the stack trace, so the symptom is a pure black screen with no crash log. Fix: omit keys entirely when the value would be null/empty (`...(value ? {KEY: value} : {})`), and runtime checks now type-guard with `typeof === 'string'` before calling string methods.
  • **SafeAreaView rendering at zero size.** Migrating ~95 screens from React Native's deprecated `SafeAreaView` to `react-native-safe-area-context`'s `SafeAreaView` without a `SafeAreaProvider` at the root meant every screen rendered as a 0×0 view. No crash, no warning, just a black screen. Fixed by wrapping the root tree in `<SafeAreaProvider/>`.
  • **`expo-blur` ViewManagerAdapter crash in Hermes Release on iOS 26.5.** First mount of any `GlassPane` past the login screen threw `Invariant Violation: View config getter callback for component ViewManagerAdapter_ExpoBlur_ExpoBlurView_* must be a function (received undefined)`. The native module's ViewManager never registered. Routed `GlassPane` through the solid-tint `<View>` path that was already the Android fallback. The Defx glass recipe still reads as "glass over smoke" because the alpha-white border + low-alpha tint over the fixed smoke backdrop does the visual work — Gaussian blur was a polish layer, not load-bearing. Once `expo-blur` registers reliably under Hermes Release, restoring iOS blur is a one-line change in `GlassPane.tsx`.

0.17.3.0

Wallet-mode security hardening. Makes asset identity contract-aware and chain-aware to block same-symbol spoof tokens from inheriting trusted registry metadata. Replaces the USD-value visibility gate with spam/import classification that stays altcoin-friendly. Hardens swap confirmation, Send, batch approvals, WalletConnect, activity pagination, and balance-sync against forged inputs, duplicate writes, and trust-boundary violations. Six critical defects found in `/review` are fixed in this release.

Fixed

  • **LiFi swap HMAC confirmation:** `confirmUniversalSwap` now verifies a server-minted HMAC digest before accepting any swap confirmation, binding `userId`, route intent, exact calldata, and TTL. Prevents clients from forging swap audit rows for routes they didn't execute.
  • **TOCTOU double-booking in `confirmUniversalSwap`:** Added `ON CONFLICT DO NOTHING` + migration `0127_transactions_lifi_source_tx_hash_unique.sql` (unique partial index on `(user_id, metadata->>'sourceTxHash') WHERE vendor='lifi'`). Concurrent calls for the same on-chain tx now converge to one audit row.
  • **Missing timeout on second RPC call:** `eth_getTransactionByHash` in `assertSuccessfulLiFiReceipt` now has a 10 s `AbortController` timeout matching the receipt-fetch loop. Was previously unbounded.
  • **Cross-chain replay via missing `tx.chainId` check:** On-chain verification now compares `tx.chainId` against the HMAC-bound `expected.chainId`. A polygon tx could previously be submitted as a base swap confirmation.
  • **`NEXTAUTH_SECRET` HMAC fallback removed:** `getLiFiConfirmSecret` no longer falls back to `NEXTAUTH_SECRET` / `AUTH_SECRET`. Requires `LIFI_SWAP_CONFIRM_SECRET` in production; dev keeps a literal fallback.
  • **Critical anomaly signals bypass bucket counting:** `StormSuppressor.shouldPush` now updates the bucket before the critical-severity early return, so critical signals count against the window while still always passing through.
  • **Client-forged wallet balances:** `syncParticleBalance` no longer writes `input.positions` into `currency_balances`. Balance reconciliation is server-side only.
  • **Cursor pagination scope:** `transaction.list` and `activity.list` cursor lookups now require `userId` match, preventing cross-user pagination anchoring.
  • **WalletConnect payload binding:** `wcMarkRequestProposalCreated` validates that the linked proposal's `subCalls` intent exactly matches the original `eth_sendTransaction` params.
  • **Solana send missing signer check:** `sendRouter` throws early when `senderAddress` is absent for same-chain Solana sends.
  • **Token-2022 rejection:** `broadcastSolanaSplTransfer` rejects mints owned by `TOKEN_2022_PROGRAM_ID` before any signing.
  • **Batch risk annotations use per-transfer amounts:** `computeRiskAnnotations` iterates all `payload.transfers` entries rather than reading a single top-level `recipient` field, so each recipient gets an individual history lookup.

Added

  • **`apps/web/src/server/lib/lifi-confirmation.ts`:** HMAC digest creation, expiry, and on-chain receipt verification for LiFi swaps (new module).
  • **`apps/web/src/lib/wallet-asset-id.ts`:** Contract-aware asset identity keyed by `{ chain, contractAddress, decimals }`. Prevents same-symbol spoof tokens from colliding with trusted registry entries in pickers and detail routes.
  • **Spam token classification:** `useUniversalBalance` flags tokens as `isSpamLikely` when their symbol matches the registry but the contract address does not. Spam tokens are excluded from the wallet list and server position sync.
  • **Import token limit:** `wallet.importToken` enforces a per-user cap of 100 imported tokens.
  • **`calldata.data` size cap:** `evmCalldataSchema.data` now enforces `.max(131072)` to prevent megabyte-sized HMAC inputs.
  • **`sourceTxHash` schema tightened:** Changed from `z.string().min(8).max(128)` to `z.string().regex(/^0x[0-9a-fA-F]{64}$/)` in both `confirmUniversalSwap` and `getUniversalSwapStatus`.
  • **`tokenContractAddressSchema`:** Shared Zod schema for swap `contractAddress` fields; accepts empty string for native assets or a 32-64 char token address. Replaces ad-hoc `z.string()` in three swap procedures.
  • **LiFi poll ageout renamed:** `destinationStatus='no_destination_after_24h'` renamed to `'recovery_required'` with `operatorEscalationRequired: true` to surface stuck bridge legs in the operator queue.

Changed

  • **`rejectBridgePlumbingInput`:** Treats empty `contractAddress` as native (no bridge lookup) rather than throwing on an empty-string lookup.
  • **`verifyLiFiQuote`:** Chain ID check now also validates against `EVM_CHAIN_NAME_TO_ID[expectedChain]`, not only the quote's own `calldata.chainId`.
  • **`imported-token-balances.ts`:** Replaced hand-rolled ABI encoding with `viem`'s `encodeFunctionData` / `decodeFunctionResult`. Imported-token address normalization now lowercases EVM addresses before Multicall encoding.
  • **Wallet activity/transaction pagination:** Compound sort `ORDER BY created_at DESC, id DESC` replaces single-column sort, stabilising pages when two rows share a timestamp.

0.17.2.2

OpenRouter default-model correction. v0.17.2.1 shipped with the default `OPENROUTER_MODEL` set to `deepseek/deepseek-chat-v3-0324:free` — a model slug I guessed. Live verification against OpenRouter's `/api/v1/models` catalog returned 404 "No endpoints found for deepseek/deepseek-chat-v3-0324:free." This patch swaps the default to **`openai/gpt-oss-120b:free`**, verified live with a tool-use probe (returned `finish_reason: tool_calls` + a valid `tool_calls` array for the `accounts_balance` tool).

Changed

  • **`agent.ts` default model:** `deepseek/deepseek-chat-v3-0324:free` → `openai/gpt-oss-120b:free`. Same fall-through behavior — `OPENROUTER_MODEL` env still overrides.
  • **`openrouter-adapter.ts` doc:** updated the catalog-of-suggestions list to models actually in OpenRouter's catalog with tool-use support (Qwen3 Next 80B, Gemma 4 31B, NVIDIA Nemotron 3 Super, plus paid Anthropic).
  • **`openrouter-adapter.test.ts`** the one hypothetical model used in the OPENROUTER_BASE_URL-override test now uses a real model slug for honesty.

Already set in Vercel prod

  • `OPENROUTER_API_KEY` (the user's key)
  • `OPENROUTER_MODEL=openai/gpt-oss-120b:free` (matches this release's default; explicit override for clarity)

0.17.2.1

Naming correction on top of v0.17.2.0. The Telegram agent provider swap landed under the wrong product name — `OPENCODE_*` env vars + adapter file. User clarified the target was **OpenRouter** (openrouter.ai), not OpenCode (sst.dev/opencode). v0.17.2.1 renames everything, adopts OpenRouter's required attribution headers, and picks a sensible free-model default.

Changed

  • **File rename:** `apps/web/src/lib/telegram/opencode-adapter.ts` → `openrouter-adapter.ts`. Symbol rename: `callOpencode` → `callOpenRouter`. Test file renamed in lockstep. `git mv` preserves history.
  • **Env vars renamed (no production usage yet — v0.17.2.0 deployed but no key set):** - `OPENCODE_API_KEY` → `OPENROUTER_API_KEY` - `OPENCODE_BASE_URL` → `OPENROUTER_BASE_URL` (default now `https://openrouter.ai/api/v1` — was `https://opencode.ai/zen/v1`) - `OPENCODE_MODEL` → `OPENROUTER_MODEL` (default now `deepseek/deepseek-chat-v3-0324:free` — solid tool-use, generous free tier on OpenRouter) - Two new vars: `OPENROUTER_HTTP_REFERER` (default `https://glide.co`) and `OPENROUTER_X_TITLE` (default `Glide Telegram Bot`) — OpenRouter requires these attribution headers for free-tier rate-limit pools and the per-app analytics dashboard.

Added

  • **OpenRouter-required attribution headers** on every call: `HTTP-Referer` and `X-Title`. Both have sensible defaults; overridable via env so dev / staging routes show up separately in OpenRouter's analytics.
  • **1 additional unit test** covering the `OPENROUTER_HTTP_REFERER` + `OPENROUTER_X_TITLE` env overrides (24 total adapter tests vs 23 in v0.17.2.0).

Migration notes

  • **Same upgrade path as v0.17.2.0 — only the env var names changed.** Set in Vercel prod env when ready: - `OPENROUTER_API_KEY` — required - `OPENROUTER_MODEL` — pick a free-tier model from openrouter.ai/models (e.g., `deepseek/deepseek-chat-v3-0324:free`, `moonshotai/kimi-k2:free`, `meta-llama/llama-3.3-70b-instruct:free`) - `OPENROUTER_BASE_URL` and the attribution headers can stay unset to use defaults.
  • **If you set `OPENCODE_*` vars from v0.17.2.0**: delete them. The new code reads `OPENROUTER_*` only. (There was a ~24h window where v0.17.2.0 was deployed with `OPENCODE_*` env vars — no production usage; the bot continued to use Anthropic since no key was set.)

0.17.2.0

Telegram freeform agent — provider swap from direct Anthropic to OpenCode (any OpenAI-compatible aggregator). Replaces `ANTHROPIC_API_KEY` as the primary trust path with `OPENCODE_API_KEY` + `OPENCODE_BASE_URL` + `OPENCODE_MODEL`. Anthropic is preserved as a transitional fallback so a single env-var flip controls the provider for the bot's LLM brain.

Added

  • **`apps/web/src/lib/telegram/opencode-adapter.ts`** — wire-boundary translator. Converts: - Anthropic tool shape (`{name, description, input_schema}`) → OpenAI function shape (`{type:'function', function:{name, description, parameters}}`) - Anthropic-shape messages with `[{type:'tool_use', id, name, input}]` / `[{type:'tool_result', tool_use_id, content}]` content arrays → OpenAI `tool_calls` on assistant turns + N x `role:'tool'` messages for results - System prompt → leading `role:'system'` message (OpenAI doesn't take a top-level `system` field) - OpenAI choices response → Anthropic-shape `{content:[...], stop_reason}` so the agent loop in `agent.ts` is unchanged
  • **Provider selector `callLlm()`** in [agent.ts](apps/web/src/lib/telegram/agent.ts) — prefers OpenCode when `OPENCODE_API_KEY` is set, falls back to direct Anthropic when only `ANTHROPIC_API_KEY` is set. Either provider failure returns null → the existing "I cannot reach my brain right now" fallback fires.
  • **23 adapter unit tests** at [apps/web/tests/telegram-opencode-adapter.test.ts](apps/web/tests/telegram-opencode-adapter.test.ts) — covers tool translation (required fields, nested schemas), message translation (assistant text + tool_use, fan-out of tool_result blocks), response translation (text-only, tool_calls only, mixed, malformed JSON in arguments, finish_reason mapping), and the `callOpencode` HTTP shim (URL trailing-slash normalization, base URL override, Bearer auth header, omit tools when empty, non-2xx → null, throw → null).

Changed

  • **`agent.ts` header doc** updated to reflect provider routing. Tool-use loop is unchanged — `callLlm()` returns Anthropic-shape so the existing `response.content.filter(b => b.type === 'tool_use')` logic still works regardless of which provider answered.

Migration notes

  • **Set in Vercel prod env when ready:** - `OPENCODE_API_KEY` — required to activate OpenCode path - `OPENCODE_BASE_URL` — defaults to `https://opencode.ai/zen/v1`. Override for OpenRouter (`https://openrouter.ai/api/v1`) or any other OpenAI-compatible endpoint. - `OPENCODE_MODEL` — defaults to `claude-sonnet-4-5` (OpenCode routes to Anthropic). Override for free-tier models like `moonshotai/kimi-k2:free`, `deepseek/deepseek-chat-v3:free`, `meta-llama/llama-3.3-70b-instruct:free`, etc.
  • **Anthropic key can stay for now** — provider selector tries OpenCode first; if OpenCode 4xx/5xx, Anthropic answers. Once OpenCode is verified stable in prod, the `ANTHROPIC_API_KEY` can be removed entirely and `callAnthropic()` deleted (filed as P2 follow-up in TODOS.md).
  • **Tool-use compatibility caveat:** free-tier small models (e.g., Llama 3 8B free) handle tool-use poorly. The 12 tools the agent exposes (`accounts_balance`, `transactions_list`, `cards_freeze`, etc.) require a model that emits valid JSON `tool_calls`. Verified good: Kimi K2, DeepSeek V3, Llama 3.3 70B, Qwen 2.5 72B. If autodecode of an `arguments` string fails, the adapter passes an empty input object so the in-process tool dispatcher returns an error payload and the model recovers on the next turn.

0.17.1.0

Telegram V2 plan-completeness hotfix. v0.17.0.0 shipped the Telegram V2 feature surface (Phase 1-4 + 4.5 delight pack) plus Phase 1's `TELEGRAM_PHASE_1_ENABLED` killswitch the Opus adversarial-review F2 finding caught. The remaining four per-phase rollout flags + the delight-pack flag the CEO-locked plan called for (DEPLOY-1) were documented in the v0.17.0.0 CHANGELOG but never wired into code. This release closes that drift so operators have the granular killswitches the plan promised.

Added

  • **`apps/web/src/lib/telegram/phase-gates.ts`** — central flag-reader module. Exports `phaseEnabled(1|2|3|4)`, `delightPackEnabled()`, `notificationsEnabled()` (AND-gate over Phase 4 + notifications killswitch), plus `ensurePhase(chatId, phase, featureName?)` / `ensureDelightPack(chatId, featureName?)` guard helpers that send the canonical "rolling out, use the web for now" reply and return false. Fail-closed posture: requires exact literal string `"true"` to enable. Read env at call-time so a Vercel env flip propagates on next cold start without a redeploy.
  • **11 phase-gates regression tests** at [apps/web/tests/telegram-phase-gates.test.ts](apps/web/tests/telegram-phase-gates.test.ts) — covers literal-string contract (`'1'`, `'yes'`, `'TRUE'`, `' true'` all reject; only exact `'true'` enables), the AND-gate semantics on `notificationsEnabled()`, the canonical reply copy + MarkdownV2 parse mode, silent pass-through when a flag is on.

Changed (wired killswitches)

  • **`TELEGRAM_PHASE_2_ENABLED`** gates the 4 Phase 2 commands at entry: [/onboard](apps/web/src/lib/telegram/commands/onboard.ts), [/kyc](apps/web/src/lib/telegram/commands/kyc.ts), [/deposit](apps/web/src/lib/telegram/commands/deposit.ts), [/profile](apps/web/src/lib/telegram/commands/profile.ts). When OFF, users see "/foo is rolling out. Use the web app at glide.co for now."
  • **`TELEGRAM_PHASE_3_ENABLED`** gates the 9 Phase 3 commands at entry: [/entities](apps/web/src/lib/telegram/commands/entities.ts), [/notifications](apps/web/src/lib/telegram/commands/notifications.ts), [/contacts](apps/web/src/lib/telegram/commands/contacts.ts), [/paylink](apps/web/src/lib/telegram/commands/paylink.ts), [/rewards](apps/web/src/lib/telegram/commands/rewards.ts), [/borrow](apps/web/src/lib/telegram/commands/borrow.ts), [/request](apps/web/src/lib/telegram/commands/request.ts), [/cards-new](apps/web/src/lib/telegram/commands/cards-new.ts), [/cards-limits](apps/web/src/lib/telegram/commands/cards-limits.ts). Same gating posture.
  • **`TELEGRAM_PHASE_4_ENABLED` × `TELEGRAM_NOTIFICATIONS_ENABLED`** AND-gate now applies to ALL Telegram outbound notification surfaces. v0.17.0.0 had the gate on the multisig fan-out Inngest function only via the per-flag `TELEGRAM_NOTIFICATIONS_ENABLED` check; deposit-received and KYC outcome notifications bypassed it entirely (caught by Opus F5 but only fixed via per-channel prefs, not the system-wide killswitch). v0.17.1.0 routes all three through the unified `notificationsEnabled()` helper. Operator semantics: turn Phase 4 off and ALL Telegram pushes stop, even for users opted-in via `/notifications`.
  • **`TELEGRAM_DELIGHT_PACK_ENABLED`** gates the 5 Phase 4.5 delight surfaces: [/diff](apps/web/src/lib/telegram/commands/diff.ts) (command), [/undo](apps/web/src/lib/telegram/commands/undo.ts) (command), `/pay` no-args recipient auto-suggest (falls back to format help when off), `/send` + `/pay` smart-amount tokens (`$25`, `all`, `50%` — falls back to legacy numeric parser when off), `/confirm` stale-link auto-refresh (falls back to "no approved actions waiting" when off).
  • **`TELEGRAM_SIGNUP_RATE_LIMIT_PER_HOUR` + `TELEGRAM_SIGNUP_RATE_LIMIT_PER_CHAT_PER_HOUR`** env vars now read by `LIMITER_CONFIG` (defaults 5/h and 3/h respectively, matching v0.17.0.0 behavior). Operators can loosen for launch traffic spikes without a code change. Plan reference: CEO Decision SEC-1 cap.

Migration notes

  • **No DB migrations.** This release is purely env-flag wiring + helper module + tests.
  • **Default behavior is identical to v0.17.0.0** if no new env vars are set: every new flag defaults OFF, which means commands and notifications gracefully refuse with the "rolling out" reply. Operators who want v0.17.0.0's "ship features, no per-phase kill" posture should set all 5 flags to `"true"` at the same time.
  • **Deploy sequence:** 1. Deploy v0.17.1.0 to prod (`vercel --prod`) 2. Wait for health check green (`curl -s https://glide.co/api/health`) 3. Set `TELEGRAM_PHASE_{1,2,3,4}_ENABLED=true` + `TELEGRAM_DELIGHT_PACK_ENABLED=true` + `TELEGRAM_NOTIFICATIONS_ENABLED=true` in Vercel prod env 4. Trigger a fresh deploy so the env propagates (Vercel propagates env on next deploy, not on env change) 5. BotFather `/setinline` → "Find Glide actions" before flipping `TELEGRAM_INLINE_MODE_ENABLED=true`

0.17.0.0

Telegram V2 — full feature parity with the web app. @GlidePaybot becomes a first-class banking client: signup + KYC + deposits + sends + invests + cards + notifications, all from chat. The wedge is hybrid magic-link signup — bot collects email, server emails a Privy magic link, ONE web tap completes Privy email-OTP + provisions embedded wallets + binds the chat → back to bot for the rest of the journey. Two browser exits are unavoidable today (Privy SDK constraints): the one-time signup tap, and the biometric step-up for money-moving ≥$50. Everything else is in chat. Ships behind per-phase feature flags (`TELEGRAM_PHASE_{1,2,3,4}_ENABLED`, all default OFF) with old commands working unchanged when flags are off.

Added

  • **Hybrid magic-link signup flow.** Anonymous chat sends `/start` (no code) → bot creates a `telegram_chat_sessions` row, collects email, mints a sha256-hashed signup token (plaintext lives only in the URL), and emits an Inngest event (`telegram/signup-email`) that fires the SES `telegram-pair-link` template. User taps the link, lands on [apps/web/src/app/telegram-pair/[token]/page.tsx](apps/web/src/app/telegram-pair/[token]/page.tsx) (Defx-styled), completes the standard Privy email-OTP widget pre-filled with the bound email, which provisions embedded EVM + Solana wallets in the same round-trip. `user.completeTelegramPair({token})` atomically stamps `users.telegram_chat_id`, issues a fresh bot grant, and pushes a personalized welcome message back to the chat. Existing web-signed-up users with a matching email auto-bind (no duplicate row) via the email-claim path in [apps/web/src/server/trpc.ts](apps/web/src/server/trpc.ts) `isAuthed` middleware (ENG-11). Email-mismatch + chat-collision + already-consumed + expired-token all fail closed with user-actionable messages. Per-chat (3/hr) + per-email (5/hr) Upstash rate caps.
  • **Multi-turn flow framework** at [apps/web/src/lib/telegram/flows/index.ts](apps/web/src/lib/telegram/flows/index.ts). Every flow exports `{ name, initialStep, stateSchema, steps, onCallback? }` and is dispatched uniformly for free-text or callback advance. Per-flow Zod schema validates `flow_state` before every step (ENG-4) — invalid state clears the session and replies with a generic recovery message (ENG-3). 15-min idle TTL with Inngest sweep.
  • **Opaque callback dispatch.** Telegram caps `callback_data` at 64 bytes; the helper at [apps/web/src/lib/telegram/callbacks.ts](apps/web/src/lib/telegram/callbacks.ts) supports two forms: `static:<verb>:<arg>` for stateless taps (refresh, nav) and 6-char base62 short IDs for stateful taps stored at `flow_state.callbacks[shortId] = {action, args}`. Auto-cleans with session expiry (ENG-6).
  • **Idempotency-first webhook.** First action after the `X-Telegram-Bot-Api-Secret-Token` check is `INSERT INTO telegram_update_dedupe (update_id) VALUES ($1) ON CONFLICT DO NOTHING RETURNING update_id` — duplicate `update_id` returns 200 with no further work (ENG-1, CODEX-6). Defends against Telegram retries on non-200 and concurrent double-taps that would otherwise mint duplicate pending actions.
  • **Action-fingerprint idempotency for step-up.** `telegram_pending_actions` gains a `fingerprint varchar(64) NOT NULL` column = `sha256(userId|actionType|JSON.stringify(payload))` with partial UNIQUE INDEX `WHERE status='pending'`. `mintPendingAction` becomes: INSERT ON CONFLICT DO NOTHING RETURNING; if no row returned, SELECT existing and return that row's URL. User double-tap, button race, or retry all converge to the same pending action (ENG-8).
  • **Durable per-user active entity for Telegram context.** New `users.active_entity_id_telegram uuid REFERENCES entities(id) ON DELETE SET NULL` column. `/entities` flow writes it; every entity-scoped read goes through `getActiveEntityForUser(userId)` which re-checks membership and falls back to the user's default entity when the user was removed since they set it (ENG-15, CODEX-14).
  • **Phase 2 — onboarding parity.** `/onboard` (Individual / Business picker via inline keyboard), `/kyc` (Sumsub link delivery + status polling), `/deposit` (chain picker → HD-derived address + QR PNG via `qrcode` lib with LRU cache; fiat ACH/SEPA via `provisionVirtualAccount`), `/profile` (account summary + KYC status + paired chat indicator). KYC link creation extracted to internal-callable helper at [apps/web/src/server/lib/kyc/create-kyc-link.ts](apps/web/src/server/lib/kyc/create-kyc-link.ts) (CODEX-13). PLACEHOLDER deposit-address guard surfaces "Deposit addresses are being provisioned — try in a few minutes" instead of leaking the master-keys-not-set sentinel (CODEX-8).
  • **Phase 3 — full feature command set.** `/entities` (list + switch + persist), `/notifications` (per-event channel toggles incl. telegram), `/contacts` (filtered beneficiaries — individual/business/employee/contractor/vendor), `/paylink`, `/rewards`, `/borrow` (read-only positions; opens defer to web for Privy on-chain sign), `/request <amt> from <email/@tag>`, `/cards-new`, `/cards-limits`. Multi-turn `/send` + `/pay` keep one-shot syntax as the fast path; missing args drop into a guided flow with inline-keyboard recipient + chain + amount + confirm steps. **Vault-routing inheritance** (CODEX-9): every money-moving flow checks `findActiveVaultForUser` before any custodial debit and renders a vault proposal link when the user has an EVM vault.
  • **Phase 3.5 — restricted inline mode** at [apps/web/src/lib/telegram/inline-mode.ts](apps/web/src/lib/telegram/inline-mode.ts). Keyword-only whitelist (`deposit` | `pay` | `help`); anything else returns `[]` so financial intent + recipient names never leak through Telegram's inline-query infrastructure. Opt-in via `/inline-mode-enable` (paired users only); `users.inline_mode_enabled boolean DEFAULT false`. Per-user 30/min Upstash cap. `cache_time=0` for address-bearing results, `cache_time=60` for static help. `pay` result deep-links to the bot's `/pay` flow rather than embedding amount/recipient in the inline tap.
  • **Phase 4 — outbound notifications.** `'telegram'` added to `NotificationChannel` enum at [apps/web/src/lib/notification-prefs.ts](apps/web/src/lib/notification-prefs.ts) + the zod enum at [apps/web/src/server/routers/user.ts](apps/web/src/server/routers/user.ts) `setNotificationPreference` (ENG-14, CODEX-4). Fan-out wired into: Noah webhook `customer.approved` / `declined` → KYC outcome message; Inngest `deposit/received` event → "Received $X via Y"; multisig proposal lifecycle (created/signed/executed/expired) via new Inngest `apps/web/src/inngest/functions/send-multisig-telegram.ts`. Default OFF per channel — users opt in via `/notifications`.
  • **Phase 4.5 — delight pack.** `/diff` (what changed since last view — new column `users.last_telegram_view_at`). `/pay` with no args auto-suggests top 3 recipients by frequency from `transactions.metadata->'recipient'`. Smart amount shortcuts (`$25`, `all`, `50%`) on `/send` + `/pay` via [apps/web/src/lib/telegram/parse-amount.ts](apps/web/src/lib/telegram/parse-amount.ts) — dollar-prefixed values route through smart-parse so `Number('$25')` never reaches the numeric parser (caught by `/review`). `/undo` cancels the latest non-completed pending action within 60s with atomic refund + status flip in one db tx; scoped with subquery `WHERE id = (SELECT id ... ORDER BY created_at DESC LIMIT 1)` so a single tap cancels at most one row (caught by `/review`). `/confirm` stale-link auto-refresh: when only expired rows are found, mint a fresh pending-action for the most recent expired payload.
  • **8 new migrations 0119–0126.** - `0119_telegram_chat_sessions.sql` — per-chat flow state machine, 15-min idle TTL. - `0120_telegram_signup_tokens.sql` — sha256-hashed magic-link tokens (CODEX-7); partial UNIQUE on `chat_id` `WHERE consumed_at IS NULL` so concurrent /start replies serialize at the index (ENG-10); CHECK `email = LOWER(email)` consistent with migration 0084. - `0121_telegram_update_dedupe.sql` — `update_id bigint` idempotency ledger. - `0122_users_active_entity_telegram.sql` — durable per-user active entity column. - `0123_drop_telegram_bot_grants_agent_principal_fk.sql` — drops the FK added in 0070 that prevented `issueBotGrant` from writing a self-contained jti (CODEX-2; bot-grant.ts:151 already commented "no agent_principals row"). Drizzle schema also updated to remove `.references()` on `agentPrincipalId` so future `drizzle-kit generate` doesn't reintroduce it (ENG-12). - `0124_telegram_pending_actions_fingerprint.sql` — action-fingerprint column + partial UNIQUE on pending (ENG-8). - `0125_users_inline_mode_enabled.sql` — opt-in flag for restricted inline mode. - `0126_users_last_telegram_view_at.sql` — `/diff` baseline timestamp.
  • **`@glideco/telegram-bot-router@0.2.0`.** Extended from `sendMessage`/`setWebhook`/`setMyCommands` to also expose `answerCallbackQuery`, `answerInlineQuery`, `sendPhoto`, `setChatMenuButton`, `editMessageReplyMarkup`. Webhook update type widened to accept `inline_query` alongside `message` + `callback_query` (CODEX-5).
  • **Ack-fast posture.** Webhook handler at [apps/web/src/app/api/telegram/route.ts](apps/web/src/app/api/telegram/route.ts) keeps inbound latency under 100ms by routing long-running side-effects (SES, multisig notifications) through Inngest fire-and-forget (ENG-7).
  • **38 new vitest cases.** `telegram-signup-helper.test.ts` (5), `telegram-callbacks.test.ts` (16), `telegram-phase3-parsers.test.ts` (5), `telegram-parse-amount.test.ts` (15). 4533 total tests pass.

Changed

  • **`@/db/schema` users table** gains `activeEntityIdTelegram`, `inlineModeEnabled`, `lastTelegramViewAt`. `telegramPendingActions` gains `fingerprint` (NOT NULL) with partial UNIQUE on pending. `telegramBotGrants.agentPrincipalId` drops its FK to `agent_principals` (matches CODEX-2 migration 0123).
  • **`isAuthed` middleware email-claim path.** When the by-`privyUserId` lookup fails and BEFORE the auto-create INSERT, also `SELECT … FROM users WHERE LOWER(email) = $1 AND privy_user_id IS NULL LIMIT 1`. If found, atomic `UPDATE … SET privy_user_id = ?, last_login_at = NOW() WHERE id = $row AND privy_user_id IS NULL RETURNING *` (defends vs concurrent). Closes the auto-pair race for users who signed up on web pre-Telegram and now complete the magic-link pair (ENG-11, CODEX-12).
  • **`telegram_pending_actions` mint path** routes through `actionFingerprint()` so identical payloads converge on a single pending row (ENG-8). User-visible: tapping "Approve" twice in 60s no longer produces two confirm URLs.
  • **Noah webhook fan-out** at [apps/web/src/app/api/webhooks/noah/route.ts](apps/web/src/app/api/webhooks/noah/route.ts) now emits Telegram notifications when paired users hit `customer.approved` / `customer.declined`. Channel-gated by per-user preferences.

Fixed

  • **Smart-amount regex passed `$25` through `parsePayArgs` / `parseSendArgs`** which then called `Number('$25')` → `NaN` → "Invalid amount". Both flows now drop the `\$?` from the bypass regex so any token with a leading `$` routes through `parseSmartAmount` (caught by `/review`, [pay.ts](apps/web/src/lib/telegram/commands/pay.ts), [send.ts](apps/web/src/lib/telegram/commands/send.ts)).
  • **`/undo` had no LIMIT/ORDER on its UPDATE** — a single tap would cancel every pending+approved row for the user. Fixed via scoped subquery `WHERE id = (SELECT id ... ORDER BY created_at DESC LIMIT 1)` so exactly one row flips (P0, caught by `/review`).
  • **`/undo` refund + status flip was non-atomic** — a crash between the balance UPDATE and the status UPDATE could double-refund on retry. Both writes now run inside one `db.transaction()` with the source-account id narrowed to a local const (caught by `/review`).
  • **Circular FK type inference** between `users.active_entity_id_telegram → entities.id → entities.owner_user_id → users.id` caused TS7022 "implicitly any". Fixed with `AnyPgColumn` annotation on the `users` references callback.

Migration notes

  • Apply migrations 0119–0126 via `scripts/apply-pending-migrations.sh START_FROM=0119`. None require maintenance windows — all new tables, NULL columns, or partial UNIQUE indexes that only constrain new rows.
  • All `TELEGRAM_PHASE_{1,2,3,4}_ENABLED` flags default OFF. Existing slash commands keep working with all flags OFF; flip per-phase via Vercel env after smoke tests.
  • BotFather one-time setup before flipping `TELEGRAM_INLINE_MODE_ENABLED`: `/setinline` description "Find Glide actions".
  • New env vars (all optional, with sensible defaults): `TELEGRAM_SIGNUP_TOKEN_TTL_MIN`, `TELEGRAM_SIGNUP_RATE_LIMIT_PER_HOUR`, `TELEGRAM_SIGNUP_RATE_LIMIT_PER_CHAT_PER_HOUR`, `TELEGRAM_SESSION_TTL_MIN`, `TELEGRAM_NOTIFICATIONS_ENABLED`, `TELEGRAM_PHASE_1/2/3/4_ENABLED`, `TELEGRAM_INLINE_MODE_ENABLED`, `TELEGRAM_DELIGHT_PACK_ENABLED`, `TELEGRAM_INLINE_MODE_RATE_LIMIT_PER_MIN`. All server-side (no `NEXT_PUBLIC_` prefix per CODEX-11 — bot rollout flags gate webhook + Inngest behavior, not browser code).
  • Existing `/start <code>` (web-originated 8-char pairing codes) keeps working unchanged. The hybrid magic-link path only activates when `/start` is sent with no argument.

Adversarial-review hardening (Opus + Codex, post-/ship)

  • **F1 (P0 — Opus)** Global rate-limit backstop reused the same per-user `Ratelimit` for the cluster-wide bucket, capping Telegram signup at 3/h cluster-wide. Added a distinct `telegramSignupGlobalBackstop` limiter (1000/h) + new `globalBackstopKey` option on `enforceAuthenticatedRateLimit` so callers with tight per-user budgets can bind to a generous global limiter.
  • **F2 (P0 — Opus)** CHANGELOG-promised `TELEGRAM_PHASE_1_ENABLED` killswitch did not exist in code. Gated the anonymous magic-link `/start (no code)` lane behind the env flag; with the flag OFF the bot replies with the legacy "pair via web" copy, matching the CHANGELOG's rollout posture.
  • **F3 (P1 — Opus)** No Inngest sweep cron registered for `telegram_chat_sessions`, `telegram_signup_tokens`, or `telegram_update_dedupe`. Added [apps/web/src/inngest/functions/sweep-telegram-tables.ts](apps/web/src/inngest/functions/sweep-telegram-tables.ts) — every 15 min, drops expired chat sessions, expired/consumed signup tokens (consumed >7d), and dedupe rows >24h. Privacy + cost.
  • **F4 (P1 — Opus)** `setUserTypeForUser` (Telegram /onboard business path) inserted into `entities` but not `entityMembers`, breaking `getActiveEntityForUser`'s membership join when users have multiple admin entities. Wrapped both inserts in `db.transaction` matching the canonical `entity.createEntity` pattern; also widened the active-entity helper to accept admin-OR-member so the pre-existing tRPC `setUserType` (which lacks the member insert) still routes correctly.
  • **F5 (P1 — Opus)** `notifyDepositReceivedTelegram` / `notifyKycApprovedTelegram` / `notifyKycRejectedTelegram` ignored `user_notification_preferences` — users could not opt out via `/notifications`. Added `deposit/received`, `kyc/approved`, `kyc/rejected` to `NotificationEventType` + `ALL_EVENT_TYPES` + `DEFAULT_CHANNELS` + zod widening on `setNotificationPreference` / `resetNotificationPreference`; routed all three notifiers through `shouldFireChannelFor`.
  • **F6 (P1 — Opus, defensive)** `/undo` CAS UPDATE outer WHERE relied solely on `id = (subquery)` for status filtering. Added explicit `AND status IN ('pending', 'approved')` to the outer predicate as defense-in-depth on top of Postgres EvalPlanQual subquery re-evaluation.
  • **F7 (P1 — Opus)** `/pay` and `/send` and step-up `/confirm` performed `bankAccounts` hold + `transactions` insert as two separate statements. A partial DB failure between them stranded cents in `balanceHeld` with no audit row. Wrapped all four code paths (pay.ts, send.ts, confirm.ts executeSendUsdc, confirm.ts executePayEmail) in `db.transaction`.
  • **C1 (P1 — Codex)** Telegram `/send` debited balance + inserted a pending `transactions` row but never emitted `withdrawal/initiated` Inngest event, so `process-withdrawal` never fired and money sat in `balanceHeld` indefinitely. Added pre-broadcast `requireCleanScreen` + `inngest.send('withdrawal/initiated', ...)` to both inline (send.ts) and step-up (confirm.ts executeSendUsdc) paths. Matches the canonical `transfer.sendUsdc` posture.
  • **C2 (P1 — Codex)** Telegram `/pay` debited the sender but never credited the recipient. External-recipient (no Glide account match) path had no settlement at all — money lost. Internal-recipient path was missing the recipient `transactions` row + the two `journal_entries` from `consumer-pay.sendByEmailOrPhone`. Inline (pay.ts) and step-up (confirm.ts executePayEmail) now perform the canonical book transfer for Glide recipients (debit + credit + sender tx + recipient tx + 2 journal entries, all in one `db.transaction`). External recipients refuse with a clear "use glide.co/pay for now" message until the paylink-from-Telegram helper extraction lands as a P2 follow-up.
  • **C3 (P1 — Codex)** "Approve with Face ID" sigil redemption only verified the existing Privy session token — no fresh-auth gate, so a stolen / unattended session could approve high-value sends. Added a server-side `MAX_TOKEN_AGE_MS = 15 min` check on `claims.issuedAt`; stale sessions return 401 with `code: 'fresh_auth_required'` so the client can prompt for re-login. P1 follow-up filed for true Privy step-up reauth API integration.
  • **C4 (P1 — Codex)** `listApprovedForChat` and `claimApprovedForExecution` filtered only by `chat_id`, not `user_id`. After unlink + re-pair, user B's `/confirm` could execute user A's approved row against A's funds. Both helpers now accept an `expectedUserId` parameter; `/confirm` passes the currently-paired `ctx.userId`. New `revokePendingForUser` helper revokes pending/approved rows on `unlinkTelegram` and on chat-collision in the magic-link completion path.
  • **C5 (P1 — Codex)** DELETE `/api/telegram/sigil-redeemed` unconditionally flipped status to `'rejected'`, even on `'executed'` rows — audit-trail rewrite. CAS predicate now restricts the flip to `status IN ('pending', 'approved')`; `'executed'` rows return 409. Terminal states are immutable.

0.16.0.0

Altitude-inspired information-architecture overhaul layered on top of the Defx glass design system that shipped in v0.14.0.0. Seven user-visible features ship together behind feature flags (default OFF in production, default ON in dev/preview): sticky-login profile chip, glass entity switcher, ⌘1–⌘9 entity hotkeys, unified `/settings` two-pane shell, ⌘K command palette, simplified home (balance + three actions), and grouped sidebar with uppercase mini-headers. Every old settings URL 308-redirects to the new path so email-link deep-links from pre-overhaul send dates continue to resolve.

Added

  • **Sticky-login profile chip on `/login`.** Last three signed-in users are remembered locally (`localStorage` key `glide-recent-profiles`, schema v1, capped at 3 LIFO). Click a chip to prefill the email and auto-fire `sendCode` — OTP still required, no auto-auth. Stores only `{email, displayName, initials, lastLoginAt}`; no tokens, no Privy session refs. Adversarial-review hardened: email regex + 254-char cap + display-name 60-char cap on both read and write paths to bound blast-radius of a tampered `localStorage` entry. New helper at [apps/web/src/lib/recent-profiles.ts](apps/web/src/lib/recent-profiles.ts).
  • **Glass entity switcher** replaces the native `<select>` in `dashboard-shell.tsx` (and gains the same surface in `consumer-shell.tsx`). Workspace pill with avatar + name + balance + chevron, Radix `DropdownMenu` dropdown styled as `.glide-pane` / `.drop-content`, ⌘N hint chips on the first three business-entity rows, "Create workspace" CTA. Mobile collapses to a bottom-sheet trigger in the top header. New `<EntitySwitcher>` at [apps/web/src/components/EntitySwitcher.tsx](apps/web/src/components/EntitySwitcher.tsx).
  • **⌘1–⌘9 entity hotkeys, ⌘K palette, ⌘, settings, Shift+? shortcuts modal.** New `useGlobalHotkeys` hook mounted in both shells. Recency-indexed (most-recently-used entity = ⌘1; falls back to alphabetical for first-time users). Personal context has no hotkey (Linear / Slack convention). Focus-aware: hotkeys no-op when the user is typing in an input, textarea, `[contenteditable]`, or `[role="combobox"|"searchbox"|"textbox"]`, so ⌘1 inside the cmdk search input still does normal-text-input behavior. New `apps/web/src/hooks/use-global-hotkeys.ts` + `apps/web/src/hooks/use-recent-routes.ts` + `apps/web/src/hooks/use-dirty-form.ts`.
  • **⌘K command palette.** `cmdk`-backed overlay with Suggested actions (Send · Deposit · Swap · View accounts · View transactions), Recent destinations (from `localStorage.glide-palette-recent`), and Navigation (every grouped sidebar item). Mobile gets a `header-btn` search icon that opens the same palette full-screen. New `<CommandPalette>` at [apps/web/src/components/CommandPalette.tsx](apps/web/src/components/CommandPalette.tsx).
  • **Unified `/settings` two-pane shell** (Altitude / Stripe / macOS System Settings pattern). Vertical side-nav 240px wide, content pane scrolls independently, mobile collapses to a `.bottom-sheet` "Section ▾" trigger. Ten sections: General · My account · Workspaces · Members (business-only) · Security · Notifications · Integrations · Billing (business-only) · KYC (pending-only) · Legal. Business-only sections hide entirely in Personal context. `DirtyFormProvider` context tracks dirty state across all section pages so cross-section nav surfaces an AlertDialog instead of silent data loss. New layout at [apps/web/src/app/settings/layout.tsx](apps/web/src/app/settings/layout.tsx) + ten `apps/web/src/app/settings/<section>/page.tsx` re-export shims.
  • **Active-section visibility override (ISSUE-001 regression fix).** `computeVisibleSections` accepts an `activeSectionId` argument and unconditionally surfaces that section even if filters would otherwise hide it. Closes the orphaned-active-row bug where deep-linking `/settings/billing` in Personal context rendered a page with no nav row to indicate location. Extracted to [apps/web/src/lib/settings-section-visibility.ts](apps/web/src/lib/settings-section-visibility.ts) with 10 regression cases in [apps/web/tests/settings-section-visibility.test.ts](apps/web/tests/settings-section-visibility.test.ts).
  • **Grouped sidebar nav.** Flat 22-item list becomes seven uppercase grouped sections: ACCOUNTS · MOVE MONEY · PEOPLE · TREASURY · OPERATIONS (business-only) · ACTIVITY · SETTINGS. Defx Mono 10px section headers with 0.10em tracking. Active rows use `--bg-primary-wa40h` fill + bold weight — no colored-left-border (removes the AI-slop pattern per design D12). `OPERATIONS` section hides entirely in Personal context.
  • **Simplified home page.** Both `/consumer/home` and `/dashboard` collapse to: greeting · balance hero (48px Defxinter, banking exception to base type scale) · three primary actions (Send · Deposit · Swap, with first as primary CTA) · thin Recent activity table (last five transactions). State banners render ABOVE balance per design D2 (status before affordance). KYC-pending users see a single full-width "Verify identity to send money" CTA instead of the three-action row.
  • **Login page redesign.** Two-column on desktop: brand panel left (56px GLIDE wordmark + tagline + body copy + three-stat strip), glass sign-in card right. Heading is conditional ("Welcome back" with profile chip present, "Welcome to Glide" without). Mobile compresses to single column with wordmark + tagline only above the form card.
  • **Cross-tab active-entity sync.** `UxOverhaulMount` subscribes to the window `storage` event. When `glide-active-entity` changes from a sibling tab, the current tab renders the glass veil and reloads to converge. Adversarial-review hardened: skips when `newValue === oldValue` (Safari spurious events), skips when `newValue` already matches the current in-tab state (avoid double-reload race with the local switcher write), and skips when `newValue` is a non-empty string not in the known entity list (tamper-resistant).
  • **15 new feature flags.** `NEXT_PUBLIC_UX_OVERHAUL_ENABLED` (master), `NEXT_PUBLIC_COMMAND_PALETTE_ENABLED`, `NEXT_PUBLIC_MINIMAL_HOME_ENABLED` (per [packages/shared/src/env/schema.ts](packages/shared/src/env/schema.ts)). All default `false` in production, `true` in dev/preview so QA can exercise the new surfaces without server config.
  • **122 new tests** across the new surfaces. 84 unit tests (recent-profiles, use-global-hotkeys, use-recent-routes, use-dirty-form, entity-recency, settings-section-visibility, palette-recent, app-url helpers). 38 component tests (ProfileChip, EntitySwitcher, ContentVeil, KeyboardShortcutsModal, SimplifiedHome). 10 regression tests for ISSUE-001. Vitest config gained `@vitejs/plugin-react` + `happy-dom` so component tests can mount React with JSX — previous tests-with-JSX limitation documented in `use-trpc-memo.test.tsx` is now resolved.

Changed

  • **Old settings paths 308-redirect to unified `/settings/<section>`.** 13 redirects in `apps/web/next.config.js`: `/dashboard/settings/{kyc,plan,affiliate,gift,gas-tank,cap,integrations,multisig,notifications}` and `/consumer/settings/{banks,security,telegram}` + root `/settings`. Hash anchors preserved on the destinations that use them (`#plan`, `#affiliate`, `#multisig`, `#payment-methods`, `#telegram`). Closes the route migration path for email-link deep-links from pre-overhaul send dates.
  • **Monerium OAuth callback redirects to new path.** [apps/web/src/app/api/auth/monerium/callback/route.ts](apps/web/src/app/api/auth/monerium/callback/route.ts) error / token / config-error / invalid-state redirects updated from `${appUrl}/dashboard/settings?monerium=…` to `${appUrl}/settings/integrations?monerium=…`. The legacy path's 308 would have stripped the query string through the redirect chain — direct routing preserves the success/error telemetry the UI surfaces. Adversarial-review finding from the cross-model QA pass.
  • **Drop eager nav prefetch in dashboard-shell.** Previously, all 22 sidebar `NAV_ITEMS` were prefetched via `router.prefetch()` on shell mount, costing ~200 KB of bundle on every session. Now relies on Next.js `<Link>`'s default hover-prefetch — same UX, far lower idle cost. Per eng-review E8 decision.
  • **`(consumer)/settings/` renamed to `(consumer)/_settings/`** (Next.js private folder convention). The legacy route group accidentally collided with the new top-level `/settings` because both resolved to the same URL path. Renaming the folder with a `_` prefix removes the route from the App Router's tree without deleting any code — the components inside still re-export from the new pages. Required to make the unified-settings consolidation work without breaking the consumer build.

Fixed

  • **ISSUE-001 — orphaned active row on filtered section deep-link** (MEDIUM, found by `/qa` 2026-05-12). When a user in Personal context deep-linked `/settings/billing#plan` (e.g. via 308 redirect from `/dashboard/settings/plan` in an email), the side-nav (filtered to hide business-only sections in Personal context per D15) had no row matching `/settings/billing` — the active highlight had nowhere to land. Fixed via `computeVisibleSections` active-section override. See [.gstack/qa-reports/qa-report-localhost-2026-05-12.md](.gstack/qa-reports/qa-report-localhost-2026-05-12.md) for full QA trail.
  • **Cross-tab veil race** (adversarial review, P1). `UxOverhaulMount`'s storage-event handler previously fired the reload veil even when the new value matched the current in-tab state (the switcher's own write fires a `storage` event in other tabs but ALSO triggers the local write — without the equality check the local tab would queue a redundant reload). Hardened with the three skip-conditions above.
  • **`recent-profiles` localStorage tamper surface** (adversarial review, P2). One-time browser access could plant a crafted profile (e.g. `displayName = "Sign in to recover your locked account…"`) that the chip would render verbatim. Strict validation + length caps + email regex on both read and write paths bound blast radius.

Build / dev tooling

  • **Vitest 4 + happy-dom + @vitejs/plugin-react.** Vitest 4 dropped `environmentMatchGlobs`; component tests now use the `// @vitest-environment happy-dom` docblock per-file. Added `@vitejs/plugin-react` as a vitest plugin so JSX parses correctly in component tests. Component testing is now a working pattern for the codebase — previously documented as a gap.
  • **`npx cmdk` dependency** (~10 KB gzipped) added to `apps/web/package.json` for the command palette fuzzy matcher and keyboard navigation primitives.

Migration notes

  • No database migrations. Feature flags default OFF in production. Flip `NEXT_PUBLIC_UX_OVERHAUL_ENABLED=true` to enable the master surface; sub-flags (`NEXT_PUBLIC_COMMAND_PALETTE_ENABLED`, `NEXT_PUBLIC_MINIMAL_HOME_ENABLED`) gate individual features.
  • Email-link integrity preserved across the cutover via the 13 308-redirects. No external comms changes required.
  • `localStorage.glide-recent-profiles` and `localStorage.glide-entity-recency` are new keys; both forward-compat versioned (schema v1) and tolerant of corrupt JSON (returns `[]`).

0.15.0.0

Closes the v0.2 → v0.14 MCP gap. The agent gateway at `mcp.glide.co` goes from 27 advertised tools to 94 — the full Glide Headless Parity Plan in one PR. Agents using Claude Desktop, Cursor, ChatGPT Apps, or any MCP-compatible client can now drive every consumer-facing money path that shipped between v0.2 and v0.14: wallet-side Send, Convert quotes + execution, Invest buy/redeem, Yield, Stripe billing/subs/paylinks/invoices, full Safe-based multisig propose/approve/execute lifecycle, gas-sponsorship policy, admin convert spreads / fee overrides / webhook replay, compliance DSAR + audit export, and treasury org defaults. Plus the operational scaffolding required to actually ship it to production: per-stage feature flags, vault allowlists, saga-reapers, partner registry update. Production posture: fresh deploys advertise the v1 baseline + Stage 1 + Stage 2 (43 tools — read + preflight). Stages 3-7 (51 tools that move real money or change governance config) stay default-OFF behind `MCP_STAGE_N_ENABLED` env flags. Operators flip them per the runbook at [docs/runbooks/mcp-parity-stages-rollout.md](docs/runbooks/mcp-parity-stages-rollout.md) after staging soak. Vault allowlists (`MCP_STAGE_3_VAULT_ALLOWLIST`, `MCP_STAGE_6_VAULT_ALLOWLIST`) gate Stage 3 + Stage 6 to a soak-set of vaults for the first week.

Added

  • **67 new MCP tools across 7 stages.** Stage 1 (10 read tools — `entities.list`, `vaults.list`, `vaults.detail`, `transactions.detail`, `convert.quote`, `invest.assets.list`, `kyc.status`, `billing.subscription.status`, `cards.list`, `beneficiaries.detail`). Stage 2 (6 preflight/simulate — `transfer.preflight`, `payments.preflight`, `convert.simulate`, `multisig.proposal.preview`, `invest.simulate.invest`, `invest.simulate.redeem`). Stage 3 (12 live-fire money — `transfer.send`, `transfer.send_to_email`, `transfer.confirm_pending`, `transfer.list_scheduled`, `transfer.cancel_scheduled`, `convert.execute`, `invest.execute_buy`, `invest.execute_redeem`, `yield.deposit`, `yield.withdraw`, `crypto_recipient.add`). Stage 4 (7 Stripe/beneficiaries v2). Stage 5 (12 multisig read + governance). Stage 6 (12 multisig proposal lifecycle + gas-sponsorship). Stage 7 (9 admin + compliance + org defaults). Net: 27 → 94 tools advertised.
  • **Per-stage feature flag rollout system** at `apps/mcp/src/feature-flags.ts`. `MCP_STAGE_N_ENABLED` env vars (1-7) gate which stages the catalog advertises. v1 baseline + Stage 1 + Stage 2 default ON; Stage 3-7 default OFF. Plus `MCP_STAGE_N_VAULT_ALLOWLIST` for first-week soak gating on Stage 3/6. The catalog filter runs at boot in `server-prod.ts` so a fresh deploy advertises the safe subset until an operator flips the flag.
  • **runPolicyGatedWrite shared helper** (`apps/mcp/src/tools/_run-policy-gated-write.ts`) — every Stage 3+ live-fire write tool routes through this 8-step contract: vault allowlist check → load policy envelope → evaluate → mint/redeem step-up sigil → idempotency-keyed pending row → enqueue Inngest worker. Encapsulates the F1/F2/F7 money-safety pattern so individual tool handlers shrink to ~5 LOC of executor logic.
  • **3 new pending-row tables** for agent-attributed money paths. Migrations `0119_mcp_pending_crypto_sends_agent_columns.sql` (backs `transfer.send` + `transfer.send_to_email`), `0120_mcp_pending_swap_executions.sql` (backs `convert.execute`), `0121_mcp_pending_invest_orders.sql` (backs `invest.execute_buy` + `invest.execute_redeem`). Each carries CAS-claim columns (`status`, `claimed_at`, `claim_deadline_at`, `worker_id`), per-agent idempotency uniqueness on `(agent_principal_id, idempotency_key)`, partial reaper indexes, and step-up sigil bookkeeping.
  • **3 saga-reaper Inngest functions** (`mcp-pending-{crypto-sends,swap-executions,invest-orders}-reaper.ts`) wired into the Inngest serve route. Every minute, each reaper reclaims rows past `claim_deadline_at` with `LEFT(...,1024)` failure-reason cap to preserve inciting forensics, `LIMIT 1000` safety valve to cap full-table locks under flapping, and step-up sigil reset to prevent stale-redeemed bypass by a fresh worker. The swap and invest reapers intentionally exclude `'executing'` state from auto-reclaim — workers writing to a vendor RPC need manual operator cleanup if they crash mid-call (prevents double-broadcast).
  • **7 new agent scopes** in `packages/schemas/src/agent-scope.ts` — `vault:configure` (Stage 5 multisig governance), `treasury:gas-sponsorship` (Stage 6 policy update), `treasury:org-defaults` (Stage 7), `admin:read`, `admin:write` (Stage 7 convert spreads + fee overrides + webhook replay), `compliance:dsar`, `compliance:audit` (Stage 7 exports). All seven introduced at once via amendment §4 so partner-registry manifests can land the full scope list in one submission.
  • **HTTP-bridge for Stage 1 read tools** (`apps/mcp/src/deps/stage-1-read-deps.ts`) — apps/mcp POSTs to apps/web's `/api/internal/mcp/stage-1-reads` endpoint. Production bridge with Bearer-auth timing-safe compare, `AbortSignal.timeout(5_000)`, typed `VendorUnavailableError` taxonomy for 401/501/5xx, and per-tool dispatch. `entities.list` is the live proof-of-life query (real Drizzle SELECT from `entities` ⋈ `entity_members` with admin precedence + DB→tool enum collapse). 9 other Stage 1 handlers return 501 stubs with bridge skeleton in place — follow-up implementations land tool-by-tool without touching apps/mcp.
  • **Operator runbook** at `docs/runbooks/mcp-parity-stages-rollout.md` covering per-stage flag flip, prereqs, smoke tests, Day-0 → Day-7 staged rollout cadence, killswitches, common incidents, and rollback for all 7 stages.
  • **Anthropic connector manifest update** at `docs/designs/agent-distribution-partner-packs/anthropic/connector-manifest.json` — bumped `1.0.0-alpha` → `1.0.0-beta`, tool counts updated to 94 with category split 36 read / 34 write / 24 treasury, `rollout_posture` block documents the default-on (43) vs max (94) tool counts and links the operator runbook.
  • **Telegram convergence in-process runtime** — `apps/mcp/src/runtime/in-process.ts` lets the Telegram bot (`apps/web/src/lib/telegram/agent.ts`) call MCP tools through the same `buildToolCatalog` machinery the HTTP gateway uses, eliminating the duplicate-implementation drift the v0.4.0.0 Telegram tools had. Per-client rate-limit tiers (Telegram capped 5× lower than OAuth grants at `60 r/min` read).
  • **Inventory-driven disposition.yaml** at `scripts/mcp-disposition.yaml` — 155 entries pinning every tRPC procedure as `exposed: <toolName>`, `excluded: <reason>`, `deferred: <stage>`, `unsafe: <reason>`, or `merged: <reason>`. Replaces the annotation-only drift check; CI gate at `scripts/mcp-parity-check.ts` fails when a new procedure isn't classified.
  • **IRON RULE #4 AST regression test** at `apps/mcp/src/__tests__/money-safety-regression.test.ts` — pins 33 Stage 3+ live-fire write files as required-to-call-runPolicyGatedWrite. New money-moving tools that skip the helper fail CI.

Changed

  • **Stage 6 `multisig.proposal.execute_via_relayer` is async-by-default** (amendment §20). Output schema enforces `tx_hash: z.null()` and `state: 'pending'` at request time. Agents poll `multisig.proposal.detail` for the broadcast outcome — no synchronous wait on Pimlico/treasury-relayer receipts. Closes the silent-drop-pending race that v0.11.1's gas sponsorship surfaced.
  • **Sigil-binding fields expanded** (amendment §17). Step-up sigils now bind `(tool_name, principal, vault_id, target_id, expected_amount_cents, counterparty_address, counterparty_chain, counterparty_token, quote_id, quote_expiry_unix_seconds, slippage_bps)`. A sigil minted for one action can't be redeemed for any other — cross-tool replay rejected at the binding layer.
  • **Money-unit canonicalization** (amendment §18, Codex #14). On-chain token amounts in MCP tool inputs are `amount_raw: z.string()` (string-encoded BigInt — never `z.bigint()` which is invalid in JSON). Fiat uses `amount_cents: z.number().int()`. The helper service-layer normalizes to `amount_usd_cents` server-side before policy evaluation.
  • **15 new env vars in `turbo.json`** declared for Vercel deploy: `MCP_STAGE_1..7_ENABLED` + `MCP_STAGE_1..7_VAULT_ALLOWLIST`. Operators flip these in the `glide-mcp` Vercel project settings to roll out stages per the runbook.

Fixed

  • **Pre-landing review fixes (12 cross-model-confirmed critical findings).** Specifically: `entities.list` route was returning a shape that failed Zod parsing (missing `status` + `kyb_status`; member_role enum returned DB values incompatible with tool schema) — every call would have crashed. Bridge `fetch()` had no timeout — slow apps/web hangs every MCP read indefinitely. Swap + invest reapers included `'executing'` state in reclaim predicate, enabling double-broadcast races against Jupiter. Bridge errors threw plain `Error` instead of typed `VendorUnavailableError` so 401/501/5xx surfaced as -32603 internal_error instead of the documented -32004 vendor_unavailable. Token-length leak via early-return in timing-safe auth compare. `RIGHT(...,1024)` dropped oldest forensics under reaper flapping (switched to `LEFT(...,1024)`). Stale `step_up_sigil_redeemed_at` survived reclaim, bypassing the §22 dual-gate. Full-table UPDATE could lock for minutes under flapping (added `LIMIT 1000` batched sweep). Plus 5 user-approved fixes: production-DOA `grantLookup/tenantLookup` stubs got a boot-time WARN log, `vaultAllowlistForStage` wired into `runPolicyGatedWrite` (was dead code), and 4 critical missing tests written.
  • **Cross-model review:** Codex adversarial returned BLOCK, Claude adversarial returned HOLD — both verdicts triggered fix work before merge. Cross-model agreement on the reaper double-broadcast race + `entities.list` shape mismatch + `vaultAllowlistForStage` dead code raised confidence to ship-ready.

0.14.0.0

Drops the "Superhuman Mysteria Purple" identity in favor of the Defx glass + smoky design system. Every surface — consumer, dashboard, admin, auth, legal, marketing, mobile — moves to the same letter-suffixed token vocabulary (`--color-theme-{a,b,c,d,e,p,pl,ph,g,r,y,s,w,q}` plus alpha suffixes), 12 / 16 radii, Defxinter wordmark + Inter body + Defx Mono for IDs, and a frosted-grey primary CTA (intentional departure from Defx's electric blue). The previous Playfair Display + lavender accent + opaque dark surfaces are gone. Alongside the design system, four production bugs surfaced during a Chrome MCP walk of `glide.co` are fixed in the same drop.

Added

  • **Defx glass + smoky design system.** Smoke-photograph backdrop fixed to the body across every shell. Every pane, card, modal, and dropdown follows the same `@utility glide-pane` recipe (`bg-primary-wa40` + `1px alpha-white border` + `backdrop-filter: blur(16px)` + inset shadow). Modals add an `inner-shadow-border`. Vendored at `apps/web/src/styles/{defx-tokens,glide-tokens}.css`.
  • **Frosted-grey primary CTA** (`@utility glide-btn-primary`). Composes `bg-primary-wa40h` over the smoke backdrop with an inset-shadow border, warm-soft white text, and ghost / success / danger / pill variants for the rest of the action vocabulary.
  • **Defxinter wordmark** on every shell (consumer / dashboard / admin / auth / legal / marketing / mobile). Uppercase, 0.14em tracking. Replaces the legacy `/glide-logo.svg`.
  • **Playwright visual regression suite.** `apps/web/e2e/design-system.spec.ts` captures 5 marquee surfaces (consumer home, consumer send, sign-in, vault controls, settings/multisig) as `toHaveScreenshot` baselines. Future PRs that regress smoke uniformity, wordmark treatment, or glass recipe fail CI.
  • **Compliance warning banner on `/admin/rules`** when any rule with `severity=critical AND action=block` is currently disabled. Lists the offending rule codes + names and offers a one-click "Re-enable all" via the existing `admin.toggleRule` mutation. Closes PROD-OBS-001 from the Chrome MCP walk.

Changed

  • **Tailwind v4 `@theme` token rebind.** Every existing utility (`bg-background`, `text-primary`, `rounded-card`, `font-display`, etc.) continues to compile, but the resolved value is now the Defx equivalent. Migrated via `scripts/codemod-design-tokens.mjs` across ~120 files; 141 hardcoded hex literals (`#1b1938`, `#cbb7fb`, etc.) replaced with `var(--color-*)` references.
  • **Font loading.** `apps/web/src/app/layout.tsx` swapped to `next/font/local` for Defxinter + Defx Mono and `next/font/google` for Inter. Playfair Display, DM Sans, and JetBrains Mono dropped from the font stack.
  • **Mobile parity.** `apps/mobile/src/lib/colors.ts` mirrors the Defx letter palette as hex literals (RN `StyleSheet` doesn't resolve `var()`). New `<GlassPane>` + `<PressableGlassPane>` wrap `expo-blur`'s `<BlurView>` with the recipe. Smoke `ImageBackground` wraps every Stack. 24+ components swept (VaultCard, ProposalCard, TreasuryHome, Chip, RiskChip, all bottom sheets, etc.) — `apps/mobile/assets/fonts/Defx/` vendored + loaded via `expo-font` `useFonts`.
  • **Background behavior.** All shell containers (`consumer-shell.tsx`, `dashboard-shell.tsx`, `admin-shell.tsx`, `(public)/layout.tsx`, `(legal)/layout.tsx`, `(legal)/layout-header.tsx`, root landing header) drop their opaque `bg-background` / `bg-surface` so the photographic smoke bleeds through uniformly across signed-in, public, legal, and root surfaces.

Fixed

  • **`/admin/users` balance column rendered `$NaN`** for accounts with a `bank_accounts` row joined and `--` for accounts without. The root cause is that `account.balanceCents` is not a column on `bank_accounts` — the schema has `balance_available` / `balance_pending` / `balance_held`. Fixed by reading `account.balanceAvailable` and hardening `formatCurrency` to return `--` for any non-finite input. Closes PROD-001.
  • **`/dashboard/settings/multisig` hung indefinitely** on "Loading defaults…" for accounts in Personal (no-entity) context. The defaults-fetch effect was gated on `activeEntityId` which was never set, so `loading` stayed `true`. Fixed by plumbing an `entitiesLoaded` flag through both effects and rendering a glass-pane empty state with a `Create an entity` CTA when there's nothing to load. Closes PROD-002.
  • **`/dashboard/settings/gas-tank` stuck on the skeleton loader** for every user — independent of personal-vs-business context. The page was unwrapping `entity.listEntities` as `{ entities: [...] }` but the server returns a bare array, so `list.length === 0` was always true. Fixed by unwrapping the result as `Entity[]` directly and dropping `loading` in the genuinely-no-entity case so the empty-state can render. Closes PROD-003.
  • **Compliance monitoring rules appeared OFF regardless of true DB state.** `/admin/rules` was reading `rule.enabled` and `rule.code` but the schema columns map to `isEnabled` and `ruleCode` — every UI toggle showed disabled, every rule code badge was blank, and the new PROD-OBS-001 banner filter was a no-op. Fixed by updating the `Rule` interface and every read site to use the schema field names. Closes PROD-OBS-001.

Removed

  • Legacy `--color-bg-*`, `--color-text-*`, `--color-input-*`, `--color-accent-cyan-*`, and `--color-border-hover` aliases from `globals.css`. The `--font-playfair` alias is gone. The previous body grain overlay (`body::after`) and `.gold-rule` decoration are removed — the smoke backdrop is the new texture and 1px alpha-white hairlines are the new rule.

0.13.0.0

**OSS Cathedral full parity expansion.** Lifts ~5,000 lines of swap-fees + gas-sponsorship + foundational infrastructure out of `apps/web/` into 19 publishable `@glideco/*` workspace packages. Closes PR #328 (stale 11-package extraction) and bundles the missing engine code (v0.11.1 gas sponsorship from PR #329 + v0.12.0.0 swap fees from PR #332) that PR #328 never reached. OSS Cathedral count: **24 → 43 packages.**

Added

  • **`@glideco/shared`** — replaces private `@repo/shared`. Now publishable.
  • **`@glideco/chain-utils`** — explorer URL + address truncation + chain canonicalization. Zero deps.
  • **`@glideco/price-feed`** — server-side USD oracle. Stablecoin shortcut + Coingecko-cached spot for 12 high-cap assets; bigint-safe rawToUsdCents conversion.
  • **`@glideco/tokenized-assets`** — 12 RWA + xStocks asset categories (Backed Finance Solana SPL: AAPLx/NVDAx/TSLAx/etc., T-bills, pre-IPO).
  • **`@glideco/wallet-broadcast`** — chain-agnostic Send dispatcher with EvmSigner/SolanaSigner/CrossChainSigner interfaces. Solana SPL+native, EVM ERC-20+native, cross-chain via Particle UA + Relay fallback. Plus pure encoders for ERC-20 calldata, SPL TransferChecked, and ATA CreateIdempotent.
  • **`@glideco/webhook-framework`** — vendor-pluggable handler factory with idempotency claim + IPv4/CIDR allow-list + payload normalizer hook. Powers Noah/Avenia/Aeon/Manteca/Monerium/Due webhook routes.
  • **`@glideco/safe-encoder`** — pure encoders for Safe v1.4.1 multisig (canonical 10-field tx shape, MultiSendCallOnly bundle encoder, EIP-712 SafeTxHash digest, canonical contract-address registry).
  • **`@glideco/squads-encoder`** — Squads v4 multisig instruction encoders (propose/approve/execute/config-action).
  • **`@glideco/solana-tx-verify`** — VersionedTransaction v0 sponsored-broadcast verifier (slot-0 fee-payer guard, semantic ix-equality, ed25519 user-sig verify). Owns its own error taxonomy per D6.
  • **`@glideco/tradeoff-panel`** — Casa-style trust-altering React modal (8 copy bundles × 3 severity tiers). `/server` subpath for the validator (no React).
  • **`@glideco/telegram-bot-router`** — dep-free Telegram Bot API client + slash-command dispatcher.
  • **`@glideco/value-rules`** — Fireblocks TAP-style rule templates + 3 entity baselines.
  • **`@glideco/paymaster`** — vendor-neutral EIP-4337 PaymasterProvider interface + registry.
  • **`@glideco/gas-sponsorship-schema`** — shared Drizzle schema for `gas_sponsorship_ledger` + `gas_sponsorship_reservations` tables, consumed by both vendor impl packages (D2).
  • **`@glideco/paymaster-pimlico`** — full Pimlico EVM gas-sponsorship engine: bundler client, UserOp pipeline, Safe 4337 enable proposal flow, advisory-locked Safe nonce claim.
  • **`@glideco/paymaster-alchemy-solana`** — full Alchemy Solana fee-payer: REST adapter + race-free advisory-locked cap reservation + sponsor-pubkey-first signing order.
  • **`@glideco/swap-fees`** — per-entity swap-fees engine: resolver chain (stable_carveout → entity_override → entity_pricing_rules → global_default → prepay_discount → gas_tank_discount → floor), Upstash-cached rules engine, swap_fee_ledger audit table, preview, refund, charge.
  • **`@glideco/hd-deposit`** — HD-derived per-entity deposit-address generator. EVM xpub (BIP32 non-hardened) + Solana SLIP-0010 ed25519 hardened-only.
  • **`@glideco/admin-pricing-ui`** — placeholder for v0.13.1 component extraction (engine ships in `@glideco/swap-fees`; UI components lift in v0.13.1).

Changed

  • **Publish-flip status — partial.** 10 of 19 new packages truly publishable today (`chain-utils`, `price-feed`, `tokenized-assets`, `safe-encoder`, `squads-encoder`, `solana-tx-verify`, `tradeoff-panel`, `telegram-bot-router`, `paymaster`, `shared`). The other 9 still import from apps/web's `@/...` workspace path alias or transitively depend on a package that does — they're `private: true` until D14 (DbHandle abstraction) lands. Their tarballs build, but `npm install` would 500 at consumer runtime. The publish-flip claim originally covered all 19 — /review's adversarial pass caught the standalone-runtime gap and the 9 affected packages were flipped back to `private`.
  • **`@repo/shared` renamed to `@glideco/shared`** — 43 consumers across the monorepo updated to import from the new name.
  • **`apps/web` becomes a thin consumer** of the new packages — ~100 callsites rewritten across source + tests + Inngest workers (D7 — no shim files left behind).
  • **4 new regression tests** under `apps/web/tests/oss-extraction-*.test.ts`: schema parity, error adapter at seam, publish-flip lock, no-orphan source-of-truth.

Deferred to v0.13.x

  • **`@glideco/admin-pricing-ui` component extraction** — 1500+ lines of TSX coupled to Next.js app-router + apps/web's Tailwind globals.css design tokens. Engine ships; component lift is its own effort.
  • **D14 DbHandle abstraction** — engine packages currently import `@/db` via workspace path alias. Cleanup at proper-distribution time.
  • **D12 per-package zod env schemas** — engine packages currently read `process.env.*` directly. Same cleanup window.
  • **Alchemy Solana Wave-9 E2E test against Solana devnet** — hard merge gate per codex finding 13. Tracks via Inngest cron testbed.

0.12.3.3

Closes the pivot loop between `/admin/users` and `/admin/entities`. Two additive columns + a deep-link so an admin can navigate user → owned entities (and back) without DB queries or click-through guessing.

Added

  • **Admin email under entity name on `/admin/entities`.** Server `adminEntity.list` now LEFT JOINs `users` and returns `adminUserEmail` per row. Client renders the email in place of the truncated UUID line (UUID is still in the row's detail-page URL). Both Personal and business rows now show whose entity it is.
  • **Search box on `/admin/entities` matches admin email** in addition to entity name + id. Placeholder updated.
  • **`/admin/entities` reads `?q=` and `?type=` URL params on mount** to pre-seed the search input + Business/All toggle. Makes the listing deep-linkable.
  • **"Entities" column on `/admin/users`** between KYC Status and Balance. Server `admin.listUsers` adds an `ownedEntitiesCount` correlated subquery (`SELECT COUNT(*) FROM entities WHERE admin_user_id = users.id`) — one round-trip, no N+1, uses the existing FK index. Client renders the count as a clickable link to `/admin/entities?q=<email>&type=all`. Clicking 3 lands on the entities page filtered to that user's three entities.

Changed

  • Skeleton loader on `/admin/users` updated from 6 to 7 cells per row to match the new column. `colSpan` on the "No users found" empty state bumped from 6 to 7.

0.12.3.2

Filters `/admin/entities` to business entities by default. Every Glide signup auto-creates an entity (v0.9.0.0 unified data model), so the listing was showing consumer-flavored `Personal` rows that have no per-entity pricing surface — they always pay default 1% + stable carveout. Admin can flip a Business / All toggle for diagnostic visibility.

Changed

  • **`adminEntity.list` accepts a `type` filter** — `'business' | 'individual' | 'all'`, defaults to `'business'`. Server-side `WHERE entities.type = ?` short-circuits the query before the per-row `previewEffectiveFee` fan-out, so the listing is faster too.
  • **Business / All toggle** on `/admin/entities` next to the search box. Defaults to Business. Flipping refetches; the search box keeps client-side filtering on top.
  • **KYB → KYC label** on the per-row pill (listing) and the entity-detail header. Individual entities go through KYC, not KYB. Previously rendered "KYB not_started" on every `Personal` row, which was semantically wrong.

0.12.3.1

Surfaces the v0.12.3.0 admin entity pages in the admin sidebar. Trivial UI tweak — no schema, no behavior change.

Added

  • **"Entities" link in admin sidebar nav**, slotted between Users and Transactions. `/admin/entities` was reachable only by typing the URL until now; surface parity restores. New `building` icon case in the `NavIcon` switch (Heroicons office-building outline).

0.12.3.0

Admin per-entity fee control UX — closes the gap between "we have the override mutation" and "an admin can confidently see and change what an entity pays." No customer-facing change.

Added

  • **`adminEntity.previewEffectiveFee` tRPC query** — runs the same resolver chain (`stable_carveout` → `entity_override` → `entity_pricing_rules` → global default) the charge path uses, and returns the bps the entity is paying RIGHT NOW for a given `(rail, fromAsset, toAsset, callerKind)`. Includes matched rule name + 30d rolling volume for diagnostics. Discounts (prepay credit / gas-tank) intentionally excluded — they're per-swap-volume-dependent.
  • **`adminEntity.endRailOverride` tRPC mutation** — sets `valid_to=now()` on an active override. Advisory-locked per (entity, rail), audit-logged via `recordAdminAction`, invalidates the per-entity rules-engine cache. Closes the gap where the DB had the supersession mechanism but the admin UI didn't expose it.
  • **`adminEntity.getEntity` tRPC query** — returns entity meta (name + type + KYB status + status) for the detail-page header.
  • **"Current effective rate" preview row** in `/admin/entities/[id]` → Swap pricing section. Shows finalBps, fee source (with matched rule name when applicable), and 30d gross volume — the three things an admin needs to verify a pricing decision.
  • **Listing fee column** in `/admin/entities` — each row now shows the entity's current swap-fee bps at a glance, with the fee source as a hover tooltip.
  • **"End" button per active override** — calls `endRailOverride`, with a confirm dialog. Updates render immediately.
  • **Live `=X.XX%` annotations** on the bps + floor input fields when adding an override. Cuts the bps-vs-percentage mental conversion an admin had to do in their head.

Changed

  • **Rail input is a dropdown, not free text.** Eleven canonical rails (`swap`, `invest`, plus all per-(symbol, network) crypto rails from `RAIL_CODES`) instead of an unconstrained string field that was typo-prone.
  • **Entity detail page header now shows the entity name + type + KYB status** instead of just the UUID. Operators stop flying blind on which entity they're editing.

0.12.2.0

Lights up cross-chain gas-tank top-up via HD-derived deposit addresses (EVM + Solana). The plan F1.1 trade-off finally lands operationally — env vars defined, derivation logic real, backfill cron in place. Operator runs the one-shot generator, pastes two env vars, redeploys. Existing placeholder rows clear within 5 min. No customer-facing change at deploy time; cross-chain top-up surface activates per-entity as the operator configures it.

Added

  • **`apps/web/scripts/generate-glide-deposit-keys.ts`** — one-shot operator script. Generates a fresh BIP39 24-word mnemonic, derives an EVM xpub at `m/44'/60'/0'`, and derives a 64-byte Solana master seed. Prints all three to stdout (mnemonic stays offline, xpub goes in `GLIDE_DEPOSIT_MASTER_XPUB`, seed goes in `GLIDE_DEPOSIT_MASTER_SOLANA_SEED`). Includes a sample derivation for `entityId=00000000-…` so operators can sanity-check the env round-trip after deploy.
  • **`docs/runbooks/deposit-master-xpub.md`** — operator runbook covering generation, env-var paste, backfill expectations, sweep flow (P1 followup), rotation, and the threat-model summary table.
  • **`backfill-deposit-addresses` Inngest cron** (`*/5 * * * *`). Sweeps `entity_deposit_addresses` rows still at `PLACEHOLDER:%` and re-derives via the standard helper. Conditional UPDATE (`WHERE address LIKE 'PLACEHOLDER:%'`) so concurrent UI requests can't race the backfill into a corrupted state. Self-tapers to a no-op when no placeholders remain.
  • **Solana derivation in `derive-deposit-address.ts`** — inline SLIP-0010 ed25519 implementation (~30 LOC, no new dep) + `Keypair.fromSeed` for the public-key encoding. Path: `m/44'/501'/{entityComponent}'/0'` (Phantom-compatible). Hardened-only because ed25519 SLIP-0010 mandates it.

Changed

  • **EVM derivation in `derive-deposit-address.ts`** — fixes the v0.12.0.0 bug where any xpub set in env would silently fail derivation (xpub-rooted nodes can't derive hardened children, and the prior path had three hardened components). New full path: `m/44'/60'/0'/{entityComponent}/{chainIndex}` — hardened components live SEED-side (baked into the xpub root operator generates), code derives only non-hardened children from the xpub. Verifies the xpub depth matches `EVM_XPUB_ROOT_DEPTH=3` before deriving — operator regeneration with a wrong-depth xpub fails loudly.
  • **`derivation_path` column shape** for both chains. EVM rows now record `m/44'/60'/0'/{X}/{Y}`; Solana rows record `m/44'/501'/{X}'/0'`. Existing placeholder rows are re-derived in place by the backfill cron.

Fixed

  • **xpub-with-hardened-path silent failure (v0.12.0.0 carryover).** Setting `GLIDE_DEPOSIT_MASTER_XPUB` to any xpub previously fell through to placeholder via the try/catch, making the env var effectively non-functional. Now derivation succeeds against an xpub generated at `m/44'/60'/0'`.

0.12.1.0

Lights up the F8.1 daily revenue reconcile cron + closes the v0.11.1 Solana sponsorship backfill gap. Both were carried over from the v0.12.0.0 launch as deferrals; this release ships the operational completion. No customer-facing change. Activates on deploy.

Added

  • **`getTokenAccountBalance` Solana RPC helper** at `apps/web/src/server/lib/solana/get-token-account-balance.ts`. Helius primary (`HELIUS_API_KEY`) → Alchemy fallback (`ALCHEMY_API_KEY`) for the standard `getTokenAccountBalance` JSON-RPC method. 10s per-provider timeout. Returns `ok=false / all_providers_failed` when both fail so the cron skips the mint instead of alerting on a network blip. Returns `ok=false / no_provider_configured` when both keys are unset so dev environments no-op cleanly. 9 vitest cases cover happy path, 429 fallback, JSON-RPC error fallback, malformed response, BigInt-precision parse, timeout, and per-provider key-only modes.
  • **Migration 0118 — `glide_swap_fee_account_snapshots`** (`apps/web/drizzle/0118_glide_swap_fee_account_snapshots.sql`). Daily ATA balance baseline keyed by `(mint, as_of)`. PRIMARY KEY rejects duplicate cron-tick writes. `glide_swap_fee_account_snapshots_mint_recent` index for the resolver's "latest snapshot per mint" lookup. Closes the Round 2 Opus P0 #2 unit/scope mismatch (today's ledger SUM vs lifetime ATA balance) by giving the cron a per-tick prior baseline.
  • **`backfill-alchemy-solana-policy.ts` Inngest cron** (`*/5 * * * *`). Sweeps any entity with `alchemy_solana_policy_id IS NULL` OR `alchemy_solana_policy_status IN ('pending','mint_failed')` and fires the existing `alchemy.solana.policy.mint` event so the v0.11.1 `alchemy-solana-mint-policy` worker stamps them. Closes the gap where entities created before the mint event firing site landed (PR #329) never got stamped, leaving Solana sponsorship effectively dark for all 2 of prod's pre-v0.11.1 entities. Self-tapers (returns `skipped: 'no_stale_entities'` when sweep is empty). Gated behind `NEXT_PUBLIC_GAS_SPONSORSHIP_ENABLED=true` AND `SOLANA_GAS_SPONSORSHIP_PAUSED !== 'true'` AND `ALCHEMY_GAS_MANAGER_SOLANA_POLICY_ID` set.

Changed

  • **`reconcile-glide-fee-revenue` cron** (`apps/web/src/inngest/functions/reconcile-glide-fee-revenue.ts`) — replaces the `fetchAtaBalance` stub with the new Helius/Alchemy helper AND the day-bounded comparison model. Per-mint loop: read prior snapshot → fetch current balance → compute ledger SUM since prior → drift = ledgerSumSincePrior − (current − prior) → alert if `|drift| > tolerance` → write today's snapshot. First-tick-ever per mint is treated as baseline (no alert, snapshot written). Skips mints when the RPC helper returns `ok=false` so transient outages don't fire spam alerts.
  • **`RECONCILE_FEE_REVENUE_LIVE` semantics flipped to opt-out**. Was opt-in (default OFF, no-op until v1.1) — now defaults ON since the proper RPC + snapshot model is live. Operators set `RECONCILE_FEE_REVENUE_LIVE=false` to silence the cron during an incident; the snapshot table needs continuous writes for baseline drift to stay accurate.
  • **`turbo.json`** — added `HELIUS_API_KEY` to the lint-tracked env-var list so the new RPC helper compiles clean.

Fixed

  • **2 pre-v0.11.1 entities permanently dark for Solana sponsorship.** The mint event firing site was added to `entity.createEntity` in PR #329 but no backfill cron was shipped, so existing prod entities (`alchemy_solana_policy_status='pending'`) never got stamped. The new backfill cron clears this on the first tick after deploy.

0.12.0.0

Per-entity swap fees + cross-chain gas tank. Entities can now be charged a tunable platform fee on Convert, Consumer Swap (Jupiter), Invest, and multisig vault swaps; stable↔stable pairs remain free. Sales-negotiated overrides, KYB / volume / geography rules, pre-pay credits with volume + time expiry, and a USD-denominated gas tank that prepays Pimlico (EVM) + Alchemy (Solana) sponsorship before Glide's monthly cap kicks in. The gas tank funds via pull-debit OR per-entity HD-derived deposit addresses across 4 EVM chains; a 5-min discover cron credits inflows log-by-log so multi-recipient batch transactions don't drop entities. Two rounds of in-session adversarial review (Claude Opus on max effort + Codex CLI) closed 22 critical findings before this PR was opened — settle-time gas-tank refunds on every Pimlico cancel-race + abandon path, log-granular dedupe on cross-chain inflows, server-resolved entityId throughout, deterministic rules-engine + override resolution, atomic credit-volume decrement, idempotent re-tries, and a migration `now()`-in-partial-index deploy blocker. The cathedral plan + every adversarial-review trail is recorded in `~/.claude/plans/introduce-swap-fees-of-stateless-lagoon.md`. Activation gated behind `SWAP_FEE_ENABLED` (server-only env var, default false). Admin pages gated behind `ADMIN_ENTITY_PRICING_ENABLED`. F8.1 daily revenue reconcile gated behind `RECONCILE_FEE_REVENUE_LIVE` until the on-chain RPC integration lands.

Added

  • **Pluggable rules engine** (`entity_pricing_rules`, migrations 0105+0114). Priority-first matching across KYB status, entity type, geography, committed volume, 30-day rolling volume, and caller kind (human vs MCP agent). D8 volume tiers seeded at 80/90/100 priorities ($50k/$250k/$1M+ thresholds). Resolver in `apps/web/src/server/lib/swap-fees/resolve-rules-engine.ts` returns deterministic results across deploys: DB ORDER BY priority+updatedAt+id, JS sort with explicit tie-break.
  • **Per-(entity, rail) overrides** (`entity_rail_overrides`, migration 0104). Sales-negotiated bps + floor that REPLACES rules-engine result. Multi-active-overrides resolved deterministically via ORDER BY validFrom DESC + id DESC.
  • **Pre-pay credits** (`entity_credit_purchases` + `entity_credit_reservations`, migrations 0106+0107). Volume + time expiry, FIFO highest-bps stack rule, two-phase (quote-time advisory reservation + execute-time SELECT FOR UPDATE). Refund decrements `consumed_volume_cents` AND reactivates an exhausted credit when refund un-exhausts it.
  • **Cross-chain gas tank** (`entity_gas_tank_ledger` + `entity_gas_tank_balances`, migration 0108 + 0117). USD-cent balance, signed-delta append-only journal. Funded via pull-debit (EVM only — Solana pull-debits reject at confirm path) OR per-entity HD-derived deposit addresses (4 EVM chains, Solana stays placeholder until ed25519/SLIP-0010 derive lands). Consumed on Pimlico EVM broadcast + Alchemy Solana broadcast in lock-ordered transactions (gas-tank namespace 1547912837/8 ALWAYS before cap-reserve namespace 1547912835/6 to prevent dual-lock deadlock).
  • **Settle-time refund estimate→actual** on every Pimlico path: orchestrator Phase 3a happy path, cancel-race-landed branch, treasury-fallback Phase 2a, AND `pimlico-pending-poll.ts` 6h-cancel + late-landing paths. The poll worker now reads `actualGasCostWei` from `eth_getUserOperationReceipt` to compute the real cost; reservation gets resized to actual so cap counting reflects truth. Solana side mirrors via `refundGasTankConsumeSolana`.
  • **HD-derived deposit addresses** (`entity_deposit_addresses`, migration 0109). Per-(entity, chain) cached row, real EVM derivation via ethers `HDNodeWallet.derivePath` when `GLIDE_DEPOSIT_MASTER_XPUB` is set, placeholder fallback otherwise. Discover cron at `apps/web/src/inngest/functions/discover-gas-tank-deposit.ts` polls every 5 min using Alchemy `alchemy_getAssetTransfers` with `'finalized'` block tag (reorg-safe), tracks `last_polled_block` per (entity, chain), holds back the cursor when ANY inflow can't be processed cleanly OR when the response was paginated. Wrong-token deposits land in the `/admin/entities/unclaimed` queue (zero-cent ledger row with `[unclaimed]` notes prefix) for manual recovery.
  • **Log-granular dedupe** on cross-chain inflows (migration 0117). Replaces the single-column `source_tx_hash` UNIQUE that dropped batched transactions where one tx hash credits multiple entities. New `source_log_id` column carries Alchemy's `uniqueId` (`{txHash}:log:{logIndex}`); the partial UNIQUE moves there. Defense-in-depth: also adds UNIQUE on `source_pull_debit_id` for the topup_pull_debit + topup_prepay_perk paths.
  • **Quote cohort freeze** (`quote_snapshots.fee_cohort jsonb`, migration 0113). Plan B.7 + Codex T8/T10. Persists rolloutEnabled / feeBpsAtQuote / sourceUsed / matchedRuleId / callerKind / frozenGasEstimateCents / gasEstimateAt at quote time; `chargeConvertFee` + `chargeJupiterFee` charge LOWER OF (frozen-at-quote bps, execute-time bps) so customers never pay more than they signed against. `lockPrepayDiscountForExecute` returns `bpsToApply = min(quoteBps, executeBps)` and the caller now actually applies it (Round 2 fix).
  • **Jupiter platform fee plumbing** (`apps/web/src/server/adapters/jupiter.ts`). `platformFeeBps` on `/quote`, `feeAccount` on `/swap`. Per-mint Glide fee accounts (`glide_swap_fee_accounts`, migration 0111+0115) for Solana ATA capture. Fee resolver returns `platformFeeBps: 0` when `feeAccount` is null so users don't pay without Glide collecting.
  • **Migrations 0102–0117** (15 new): enum extensions for gas_sponsorship_source + pull_debits.kind; entity_rail_overrides; entity_pricing_rules + idempotent seed; entity_credit_purchases; entity_credit_reservations; entity_gas_tank_ledger + balances; entity_deposit_addresses; swap_fee_ledger; glide_swap_fee_accounts + idempotent seed; admin_audit target_type widen; quote_snapshots.fee_cohort; announcement_log; log-granular dedupe + source_pull_debit_id UNIQUE.
  • **Admin pages**: `/admin/pricing-rules` (rule CRUD + matching-rule preview), `/admin/entities` (paste-textarea bulk pricing, audit-logged), `/admin/entities/[id]` (per-entity override + credit + gas-tank surface), `/admin/entities/unclaimed` (wrong-token deposit recovery queue). All mutations advisory-locked + audit-logged.
  • **Customer UI**: `/dashboard/settings/gas-tank` continuous-amount top-up flow with pull-debit + cross-chain modes; per-entity HD deposit address with copy + "still provisioning" affordance; `/dashboard/settings/plan` extended with effective-rate hero + active-credit countdown + matching-rules transparency. `<EntitySpendBanner>` replaces the dashboard `<FxAllowanceBanner>` mount with two-column FX-allowance + gas-tank state. `<FeeBreakdown>` extended to three densities (compact / default / expanded). Public `/q/[id]` redacts the per-entity discount chain server-side.
  • **Inngest crons**: `discover-gas-tank-deposit` (5 min), `pimlico-pending-poll` extended with gas-tank settle on landed paths, `reconcile-glide-fee-revenue` (daily, gated), `monitor-stable-carveout-abuse` (daily), `announce-swap-fee-rollout` (manual trigger 7 days pre-flag-flip — pre-filters already-sent recipients to survive Inngest step retries without duplicate emails).
  • **Mobile**: `<GasTankBanner>` shared via `lib/format-fee-breakdown.ts` (web + native parity).
  • **Telegram**: `/balance` shows gas-tank line ONLY when ≤ $10 (running low, actionable). `/send` for stable→stable shows "Free swap · stable↔stable" instead of the standard fee line.

Changed

  • **`loadAdminSpread`** (apps/web/src/server/routers/fx-rate.ts:233-259) widened to take `accountContext: { entityId?, userId? }`. Resolution order: stable carve-out → entity_rail_overrides REPLACE → rules engine priority-first → global default 1% (gated by `SWAP_FEE_ENABLED`). Wraps lookup steps in try/catch so a DB failure falls through to default and the audit row records `fallbackReason='entity_pricing_lookup_failed'` instead of blocking the swap.
  • **`recordCryptoSend`** drops `amountCents` from the public input schema. Server now derives USD cents from `(amountRaw, decimals, asset)` via `lookupUsdCents` so a malicious caller can no longer inflate the audit row's USD value while submitting a small on-chain transfer.
  • **`consumer-swap.confirmSwap`**: server-resolves entityId from `ctx.user.id` (no client trust on `platformFee.entityId`), derives `swapAmountCents` via `lookupUsdCents` from on-chain `inputOutflow` (not raw token units), wraps `transactions` insert + `swap_fee_ledger` write in a single tx with fail-soft on the fee write so the swap row still commits if the fee path throws.
  • **`consumer-invest.confirmInvestment`**: gates the fee write on real on-chain verification (tx exists + succeeded + signed by user wallet + USDC outflow > 0). Investment row + fee ledger row commit in one tx.
  • **Stable carve-out** keys are uppercase (`USDe` → `USDE`) to match the `STABLECOIN_SET` lookup.
  • **Server-side feature flags** (Codex T2). `NEXT_PUBLIC_SWAP_FEE_ENABLED` → `SWAP_FEE_ENABLED` and `NEXT_PUBLIC_ADMIN_ENTITY_PRICING_ENABLED` → `ADMIN_ENTITY_PRICING_ENABLED`. Client renders flags via a new `featureFlags.get()` tRPC procedure; server is authoritative on fee enablement.
  • **Upstash Redis cache** for the rules-engine resolver (Codex T1). Replaces in-process Maps that drifted across Vercel serverless instances. Cache key keys on entity + rail + caller + a context hash that captures KYB / entity type / geography / volume bucket so a tier change invalidates only the affected entries (Round 2 P2-G).
  • **Rules engine eval order** (Codex T3): priority-first, not best-rate-wins. Rule with highest priority among matchers wins; tie-break by most-recently-updated then ruleId.

Fixed

  • **`consumed_volume_cents` UPDATE failure** in `chargeConvertFee` + `chargeJupiterFee` no longer swallowed. The prior fail-soft committed the ledger row but left the volume counter un-incremented — customer kept the discount AND the credit volume, enabling re-use until the daily reconciler backfilled. The UPDATE failure now propagates so the caller-owned tx rolls back the ledger insert too. Atomic ledger ↔ consumed-decrement is the load-bearing accounting invariant.
  • **`refundSwapFee`** reactivates an exhausted credit when the refund un-exhausts it (status `'expired'` → `'active'` when post-refund `consumed_volume_cents < discount_max_volume_cents AND now() < discount_ends_at`).
  • **EVM cap fallback drift**: `pimlico-evm-broadcast.ts` now imports `FALLBACK_EVM_GAS_MONTHLY_CAP_CENTS` from the settings resolver instead of an inline `5000n` literal that could drift.
  • **`getGasSponsorshipUsage` tz bug** (multisig.ts): swapped `date_trunc('month', now() AT TIME ZONE 'UTC')` for the shared `monthStartUtc()` helper. Under non-UTC session tz the monthly cap was rolling hours late.
  • **`derive-deposit-address`** no longer hardcodes `isPlaceholder: true` when real EVM derivation succeeded. The router would otherwise mask the real address as null on the FIRST query.
  • **Migration 0105 `now()` in partial-index predicate** removed. Postgres rejects non-IMMUTABLE functions in CREATE INDEX predicates ("functions in index predicate must be marked IMMUTABLE"). Without the fix, 0105 aborts under BEGIN/COMMIT and every downstream migration in this PR rolls back.
  • **`entity_gas_tank_ledger.source_tx_hash` UNIQUE** widened to log-granular `source_log_id` UNIQUE (migration 0117). One ERC20 tx with multiple Transfer events crediting different Glide entities used to drop all but the first; each log is now uniquely identified via Alchemy's `uniqueId`.
  • **Pimlico cancel-race-landed branch** + **pending-poll late-landing branch** now mirror Phase 3a's settle logic. Estimates over actual cost get refunded to the gas-tank via `refundGasTankConsume` (UNIQUE on `original_ledger_id` makes the refund idempotent across retries).
  • **`announce-swap-fee-rollout`** pre-filters already-sent recipients at the top of the broadcast step. Prior order (send → INSERT log) duplicated emails on every Inngest step retry; now an in-step Set tracks recipients sent in this run and a head-of-step lookup against `announcement_log` skips ones that succeeded in any prior run.
  • **Discover-cron pagination guard** moved from `inflows.length >= 50` (post-filter, breaks when transfers are dropped for missing fields) to a fetcher-returned `paginated` flag plus raw `rawTransferCount >= 50` check.
  • **Wrong-token deposits** now persist as zero-cent `topup_crosschain` ledger rows with `[unclaimed]` notes prefix so the `/admin/entities/unclaimed` page can surface them. Idempotent via `${uniqueId}:wrong-token` suffix.
  • **`reconcile-glide-fee-revenue`** hard-gated behind `RECONCILE_FEE_REVENUE_LIVE` until the Helius `getTokenAccountBalance` integration lands. Comparison switched to per-mint raw-unit tolerance (matches `capturedTokenAmount` units).
  • **Postgres trigger search_path hardening**: `sync_users_primary_wallet()` pinned to `pg_catalog, public` with explicit `SECURITY INVOKER`, blocking session-level search_path shadowing attacks.

0.11.2.0

`/review` post-merge hardening pass on PR #329. Twelve critical findings closed before launch — plus a second adversarial pass (Claude opus max-effort + Codex high-reasoning) that surfaced 5 additional ship-blockers, ALL closed in this release. Same release-train flag-rollout posture (Day 0 / 1 / 7 / 14); migrations land additively on top of v0.11.1.

v0.11.2 pass-2 adversarial hardening (additional fixes after pass-1)

  • **Solana ALT bypass on the Squads PDA gate** (CRITICAL — codex + opus). The static-key-only `requireSquadsCallToMultisigPda` could be tricked: an attacker submits a tx where the legitimate multisig PDA is a harmless static extra account but the actual Squads ix's PDA target is hidden in an Address Lookup Table. Fix: reject ANY sponsored Solana broadcast that uses ALT (`addressTableLookups.length > 0`) at both the pre-broadcast verifier (`verifySponsoredBroadcastTx`) AND the on-chain re-verifier (`verifyOnChainBroadcast`). Squads multisig txs we sponsor have small account counts and don't need ALTs; legitimate clients are unaffected.
  • **Cancel-success post-receipt race** (CRITICAL — codex + opus). Pimlico's `cancelUserOp` returning `{cancelled: true}` only proves the bundler's mempool entry is gone — if the bundler had already submitted the UserOp BEFORE our cancel arrived, the on-chain handleOps tx still mines. Pre-fix, releasing the Safe nonce here let treasury fallback claim a fresh nonce while the original UserOp landed → DOUBLE EXECUTION. Fix: after `cancelled: true`, do ONE more receipt poll (with a 1.5s backoff in the worker, immediate in the orchestrator). If the receipt is now visible AND verifies as a real EntryPoint v0.7 handleOps, treat the proposal as LANDED (write ledger + flip to pimlico_landed) instead of releasing. New optional `getReceipt` method on `PaymasterProvider` for the orchestrator's race-close.
  • **`tx.meta.err` not checked in on-chain verifier** (CRITICAL — opus). A reverted on-chain tx (e.g., Squads program rejecting a duplicate transactionIndex execute) passed all four downstream checks (relayer slot, user pubkey, PDA touch, ALT-rejection). Vendor still bills for reverts → ledger drift + misleading "success" reported to user. Fix: reject if `tx.meta.err !== null` immediately after the retry block.
  • **`submitSolanaBroadcast` proposal idempotency** (CRITICAL — opus). EVM is protected by the `claimSafeNonce` CAS (nonce_claim_status guard); Solana had no equivalent. Two sequential calls (UI double-click, mobile retry-on-network-error) both passed `proposal.status='pending'`, both reserved cap, both broadcast via Alchemy. The second Squads execute reverted on chain (transactionIndex idempotency at the program level) but Alchemy still charged the policy. Fix: new `SolanaSponsorshipAlreadyActive` sentinel error class. Inside the entity-scoped advisory lock, the cap-reservation helper now SELECTs for an existing reservation in `('reserved', 'confirmed')` status for `(proposal_id, source='alchemy_solana')` and refuses with CONFLICT (`sponsorship_already_active`) if found.
  • **`verifyOnChainBroadcast` retry budget too short** (codex + opus). 3 attempts × 500ms = 1.5s would false-reject under sustained Solana mainnet finality lag, releasing the reservation while Alchemy already billed. Bumped to 8 attempts with exponential 500ms→4s backoff (~15s budget).
  • **Postmerge verify script gaps**. `scripts/v0.11-postmerge-verify.sh` now also checks (a) `entities_alchemy_solana_policy_status_check` CHECK constraint exists (migration 0099) and (b) `gas_ledger_proposal_source_unique` partial UNIQUE index is `indisvalid='t'` (migration 0100). A CONCURRENTLY UNIQUE build that fails leaves an INVALID index — without the indisvalid gate, ON CONFLICT clauses silently degrade with no idempotency.
  • **Tests added (12 new vitest cases)**: ALT rejection on `verifyOnChainBroadcast`, `tx.meta.err` rejection (both InstructionError shape + string-typed shape), pre-broadcast ALT regression guard, post-cancel-receipt landed → flips to landed (orchestrator), post-cancel-receipt null → release path preserved, `SolanaSponsorshipAlreadyActive` class export check, idempotency SELECT-then-INSERT order check, idempotency status-filter check, abandon-CAS guard.

Added

  • **Migration 0099 — `entities_alchemy_solana_policy_status_check` CHECK constraint** (`apps/web/drizzle/0099_alchemy_solana_status_check.sql`). The parallel EVM column uses an enum; the Solana column shipped as `text DEFAULT 'pending'` for the v0.12 generic-table consolidation but had no DB-side validation. Future typos (`'minited'`, `'mint_falied'`) are now rejected at INSERT/UPDATE. Idempotent via `pg_constraint` guard. Applied via TXN.
  • **Migration 0100 — `gas_ledger_proposal_source_unique` partial UNIQUE index** (`apps/web/drizzle/0100_gas_ledger_unique_proposal_source.sql`). Closes the ledger-double-write race: pimlico-pending-poll worker racing with the orchestrator's happy-path could produce two rows for the same `(proposal_id, source)`. Partial WHERE `proposal_id IS NOT NULL` to avoid retroactively constraining legacy treasury-fallback rows. Applied via AUTOCOMMIT (CONCURRENTLY).
  • **`runSolanaDriftCheck`** (`apps/web/src/inngest/functions/reconcile-gas-sponsorship.ts`) — Solana drift parity with Pimlico. Calls `adapter.getUsage` against the Alchemy Gas Manager policy + compares to `SUM(sponsored_native_wei) WHERE source='alchemy_solana'` for the current month. Threshold: 100,000 lamports (~$0.01 at $100/SOL). Fail-soft on vendor outage. `AlchemySolanaFeePayerAdapter.getUsage` added (`apps/web/src/server/lib/gas-sponsorship/alchemy-solana-fee-payer.ts`).
  • **`verifyOnChainBroadcast`** (`apps/web/src/server/lib/user-multisig/solana-broadcast-verify.ts`, ~80 LOC) — post-Alchemy on-chain content verification for `submitSolanaBroadcast`. Re-fetches the tx via Solana RPC and verifies (a) it exists + confirmed, (b) `staticAccountKeys[0]===expectedRelayerPubkey`, (c) user pubkey is in required-signer slots, (d) at least one Squads v4 ix targets the proposal's multisig PDA. Closes the v0.6 trust-boundary regression: a misbehaving Alchemy response can no longer poison the audit ledger with a fabricated or unrelated `txSignature`.
  • **`requireSquadsCallToMultisigPda` gate** on `verifySponsoredBroadcastTx`. Pre-broadcast verifier now requires at least one Squads v4 ix touching `wallet.address` (the multisig PDA) when the caller passes `expectedInstructions: []`. Closes the sponsorship-griefing vector where a vault manager could broadcast ComputeBudget+Memo-only no-ops and burn the entity's monthly Solana cap.
  • **Cancellation-confirmed gate at 6h abandon** (`apps/web/src/inngest/functions/pimlico-pending-poll.ts`). Before flipping `nonce_claim_status='released'`, `tryCancelWithRetries` (3 attempts, ~1s backoff) MUST succeed. If cancel fails, the row stays `pimlico_pending` indefinitely and emits a P0 `pimlico.pending_poll.abandon_blocked` event for on-call. Closes the EVM double-execution hazard: ERC-4337 EntryPoint nonce key (Safe 4337 module) is independent of `Safe.nonce` (treasury path), so a pending Pimlico UserOp could otherwise land alongside a treasury-fallback retry.
  • **Finality + EntryPoint check** in pimlico-pending-poll (`verifyOnChainHandleOps`). `tx.to` MUST equal `ENTRY_POINT_07_ADDRESS` AND tx must have ≥1 confirmation before the worker flips a row to `pimlico_landed`. Closes the bundler-trust window: a bundler returning an unrelated 0x… hash can no longer poison the ledger.
  • **Paymaster ops alert handlers** (`apps/web/src/inngest/functions/paymaster-ops-alerts.ts`, 3 functions). Event-driven Inngest functions for `pimlico.cap_exceeded`, `pimlico.pending_poll.abandon_blocked`, and `alchemy.solana.cap_exceeded`. Closes the "fired but no consumer" gap on `pimlico.cap_exceeded`. Pattern matches `alert-stuck-webhooks`: structured `console.warn`/`console.error` for log-based alerting (Sentry / Axiom).
  • **Test coverage** — 4 new test files / 49 new vitest cases: - `multisig-controls-router-propose-enable-safe-4337.test.ts` (11 cases): every auth gate + insert path on the brand-new tRPC procedure. - `multisig-controls-paymaster-cap-exceeded-unit.test.ts` (9 cases): unit discriminator behavior. - `multisig-controls-cap-window.test.ts` (6 cases): UTC month-boundary helper. - `multisig-controls-solana-broadcast-verify.test.ts` extended (10 new cases): `requireSquadsCallToMultisigPda` gate + `verifyOnChainBroadcast` content verification. - `multisig-controls-alchemy-solana-fee-payer.test.ts` extended (6 new cases): `getUsage` drift adapter. - `multisig-controls-reconcile-gas-sponsorship-drift.test.ts` extended (5 new cases): per-vendor drift check shape. - `multisig-controls-pimlico-pending-poll.test.ts` extended (4 new cases): cancellation-confirmed gate, finality, ON CONFLICT, EntryPoint check.

Changed

  • **PIMLICO_API_KEY moved from URL query string to `Authorization: Bearer` header** (`apps/web/src/server/lib/gas-sponsorship/chain-config.ts`, `pimlico-bundler-client.ts`). Closes the leak vector: fetch errors, Vercel logs, Sentry breadcrumbs, undici tracing, and APM HTTP-client instrumentation all capture full URLs by default — header-only keeps the key out of every log surface. Defense-in-depth: `redactApiKey()` in `jsonRpc` strips any `apikey=...` fragments that may have leaked through a misconfigured `PIMLICO_BUNDLER_URL_<CHAIN>` override or a vendor error message echoing the request URL.
  • **Cap-window queries fixed to use `monthStartUtc()`** at three callsites (`pimlico-evm-broadcast.ts`, `alchemy-solana-cap-reserve.ts`, `reconcile-gas-sponsorship.ts`). The previous SQL `date_trunc('month', now() AT TIME ZONE 'UTC')` returned a naive timestamp that re-cast at session timezone for the timestamptz comparison — under non-UTC sessions, the monthly cap rolled hours late (cap-bypass window every month). The new helper computes the boundary in TS and passes as a typed parameter; no session-tz dependency.
  • **`PaymasterCapExceeded` gains `unit: 'usd_cents' | 'lamports'` discriminator** (`apps/web/src/server/lib/user-multisig/controls-errors.ts`). Default unit = `'usd_cents'` so EVM callers don't need a migration; Solana callers pass `'lamports'` and the formatter renders SOL with proper precision. Closes the unit-drift bug where 1,000,000 lamports rendered as `$10000.00` in operator alerts.
  • **Pimlico provider per-chain cache** (`pimlico-init.ts`). v0.11.1 had a single `_cachedProvider` whose first call bound the bundler to one chain forever — silent_drop_pending on every other chain. Now `Map<SponsorshipChain, PaymasterProvider>` keyed per chain. (Already shipped in PR #329's first /review pass; this v0.11.2 release just retains the fix.)
  • **Pimlico orchestrator no longer holds a DB transaction across the 30s vendor receipt poll** (`pimlico-evm-broadcast.ts`). Three discrete phases: tx1 reserve+claim (ms), no-tx vendor call (up to 30s), tx2 finalize (ms). Eliminates connection-pool starvation under concurrent broadcasts AND the unknown-error-after-Pimlico-accepted double-spend race (treasury fallback can no longer claim the same Safe nonce because the cap-reservation row is durably committed before the vendor call). (Shipped in PR #329's first /review pass; retained.)
  • **Ledger INSERT sites all use `ON CONFLICT (proposal_id, source) WHERE proposal_id IS NOT NULL DO NOTHING`** (orchestrator + pending-poll worker + submitSolanaBroadcast). Idempotent retries; matches the new partial UNIQUE index from migration 0100.
  • **`submitSolanaBroadcast` fires `alchemy.solana.cap_exceeded`** Inngest event when reservation throws `PaymasterCapExceeded` — parity with the EVM `pimlico.cap_exceeded` event so the new ops-alert handler can fan out.
  • **Migrations re-split for unambiguous classifier matching**: `0097_alchemy_solana_sponsorship.sql` (enum-only, AUTOCOMMIT) + `0097_b_alchemy_solana_columns.sql` (entity columns, TXN); `0098_multisig_proposals_claimed_tx_hash.sql` (columns, TXN) + `0098_partial_idx_pimlico_pending.sql` (CONCURRENTLY index, AUTOCOMMIT). The custom runner's regex-based classifier is now unambiguous per file. (Migration split shipped in PR #329's first /review pass.)

Pre-flight changes

  • Ordered apply table for the six migration files (0097 → 0100)
  • Verify script's new `pg_index.indisvalid` gate on the partial index
  • Solana drift cron now ships (was deferred to v0.11.2; closed in this release)
  • Killswitch list on the `Killswitches` section + the new `pimlico.pending_poll.abandon_blocked` P0 event

0.11.1.0

Gas sponsorship activation. v0.10 shipped the full Pimlico scaffolding to prod (PR #323) but the actual sponsorship hook in `executeReadyViaRelayer` was a comment block + the mint worker was a stub + the Solana fee-payer plan called for a self-hosted relayer that was never deployed. This release makes everything real. Two-vendor split: **Pimlico** for EVM (4 chains: ethereum, base, arbitrum, polygon — BSC deferred to v0.12 per CEO D44) + **Alchemy Gas Manager** for Solana (replaces the v0.10-planned self-hosted fee-payer per CEO D47, dropping the operational burden of running our own keypair + balance monitoring).

Added

  • **Pimlico bundler client** (`apps/web/src/server/lib/gas-sponsorship/pimlico-bundler-client.ts`, ~280 LOC) — direct JSON-RPC over fetch + REST usage endpoint with Bearer auth. No SDK type-coupling at the seam (UserOp construction lives in the activation hook). Per-chain bound via `bindBundlerClientToChain`.
  • **Pimlico chain config** (`chain-config.ts`, ~200 LOC) — `PIMLICO_CHAIN_CONFIG` source-of-truth map for the 4 launch chains. Reads Safe 4337 module addresses from `@safe-global/safe-modules-deployments` npm package with hardcoded LAST_KNOWN_GOOD fallback (per ENG D49). `isSponsorshipChain` type guard for activation hook gating.
  • **Pimlico activation hook in `executeReadyViaRelayer`** — Pimlico-then-treasury fan-out. On flag-on + minted policy + chain in SponsorshipChain set → call `broadcastEvmViaPimlicoSponsored`. Outcomes: `sponsored` returns immediately with `source='pimlico_paymaster'`; `silent_drop_pending` returns 503 + 6h poll worker resolves (NO treasury fallback per CEO D20 to prevent double-spend); `PaymasterCapExceeded`/`PaymasterUnavailable`/`PaymasterPolicyNotFound`/`PaymasterInvalidSafe` → log + treasury fallback (CEO D45 silent treasury fallback); `PaymasterSimulationRevert` → 400 (treasury would also revert).
  • **Safe 4337 module enable proposal flow** (`proposeEnableSafe4337Module` tRPC + `safe-4337-enable-proposal.ts` composer, ~95 LOC) — one-time `enableModule(SAFE_4337_MODULE)` call required before Pimlico can sponsor a Safe (per v0.11 D62). Goes through standard `kind='enable_module'` M-of-N pipeline.
  • **Pimlico mint worker** (Wave 3) — `pimlico-mint-policy.ts` reads `PIMLICO_SHARED_POLICY_ID` from env and stamps every entity with the same value (per v0.11 D60: Pimlico's REST policy-CREATE endpoint returns 401 on the current tier, so we use a single shared policy created manually in the Pimlico dashboard with `chain_ids=[1, 8453, 42161, 137]`). Per-entity caps enforced at the `gas_sponsorship_reservations` layer.
  • **Reconcile drift-check activation** — `reconcile-gas-sponsorship.ts` cron now actually calls `provider.getUsage()` and compares vendor's view against ledger sum (current month, source='pimlico_paymaster'). Alerts > $1 drift (`ALERT_THRESHOLD_CENTS=100n`). Fail-soft on vendor outage.
  • **Entity-create mint events** — `entity.createEntity` fires `pimlico.sponsorship_policy.mint` + `alchemy.solana.policy.mint` events AFTER the DB transaction commits. Both wrapped in try/catch (non-fatal).
  • **6h pending poll worker** (`pimlico-pending-poll.ts`, Wave 3.5) — every 10 min, polls in-flight UserOps in `nonce_claim_status='pimlico_pending'`. Two paths: `eth_getTransactionByHash(claimed_tx_hash)` via chain RPC AND `eth_getUserOperationReceipt(userOpHash)` via bundler RPC. Receipt landed → flips to `pimlico_landed` + writes ledger row + confirms reservation in ONE db transaction. 6h elapsed without receipt → flips to `released` + releases reservation so a treasury-pays retry can claim a fresh nonce.
  • **Migration 0097**: `gas_sponsorship_source` enum gains `'alchemy_solana'` value (autocommit-classified per the runner's `ALTER TYPE ADD VALUE` regex). `entities.alchemy_solana_policy_id` + `alchemy_solana_policy_status` columns (parallel to the Pimlico EVM columns; v0.12 generic refactor will consolidate).
  • **Migration 0098**: `multisig_proposals.claimed_tx_hash` + `claimed_user_op_hash` text columns + partial index `multisig_proposals_pimlico_pending_idx` on `(updated_at) WHERE nonce_claim_status='pimlico_pending'` (Wave 3.5 hot query path).
  • **Alchemy Solana fee-payer adapter** (`alchemy-solana-fee-payer.ts` + `alchemy-solana-config.ts` + `alchemy-solana-init.ts`, ~310 LOC total) — direct REST adapter against Alchemy Gas Manager. Two methods: `getFeePayerPubkey()` (cached 5 min) + `sponsorAndSubmit(serializedTxBase64)`. Error mapping per ENG D29 controls-errors taxonomy.
  • **Solana fee-payer wired into `prepareSolanaSign` + `submitSolanaBroadcast`** (Wave 6) — `resolveSolanaFeePayerPubkey()` calls Alchemy adapter (replaces v0.10 env-var keypair). `submitSolanaBroadcast` now: verifySponsoredBroadcastTx → reserve cap → sponsorAndSubmit → snapshot lamports→USD → ONE db tx (ledger row + reservation confirm + proposal txHash update) → return `source='alchemy_solana'`. D58 sponsor-pubkey-first signing order.
  • **Solana cap reservation** (`alchemy-solana-cap-reserve.ts`, Wave 7.5, ~150 LOC) — race-free per-entity advisory-locked reservation pattern mirroring the EVM `reserveSponsorshipUsd`. Stable namespace constant 1547912836 (distinct from EVM's 1547912835). Falls back to 1_000_000 lamports/mo (~0.001 SOL) when entity defaults missing. Throws `PaymasterCapExceeded` with cap + current usage if estimate would breach the cap. `confirmSolanaReservation` + `releaseSolanaReservation` helpers for post-broadcast.
  • **Alchemy Solana mint worker** (`alchemy-solana-mint-policy.ts`, Wave 7) — mirror of `pimlico-mint-policy`. Reads `ALCHEMY_GAS_MANAGER_SOLANA_POLICY_ID` from env + stamps every entity. Honors `SOLANA_GAS_SPONSORSHIP_PAUSED` killswitch. Inngest retries: 4.
  • **v0.11 deploy runbook** (`docs/runbooks/v0.11-gas-sponsorship-rollout.md`, ~270 lines) — pre-flight env + dashboard setup, staged rollout via feature flags, smoke tests, per-error-class debugging, rollback steps, killswitches.
  • **v0.11 verify script** (`scripts/v0.11-postmerge-verify.sh`) — 9 checks for migration applied state, mint-worker stamping, ledger row presence (post-Day-0 smoke), stuck pimlico_pending watchdog, leaked-reservation watchdog.

Changed

  • **`resolveSolanaFeePayerPubkey()`** is now async — calls the Alchemy adapter's `getFeePayerPubkey()` (HTTP). Returns null on adapter throw so client routes to user-pays via `submitSolanaSignature`.
  • **`prepareSolanaSign`** awaits the now-async fee-payer resolver.
  • **`pimlico-evm-broadcast.ts:BroadcastResult`** drops the unused `'silent_drop_released'` literal — orchestrator THROWS `PaymasterSilentDrop` on cancel-success (caller falls through to treasury). Wave 2 cleanup.
  • **Multisig settings page** — Solana monthly cap `helperText` updated from "self-hosted fee-payer" → "Alchemy-managed fee-payer".

Removed

  • **`monitorSolanaRelayerBalance` Inngest worker** (Wave 7.5) — removed from `inngest/functions/index.ts` exports + the Inngest API route's import + functions array. The file at `./monitor-solana-relayer-balance.ts` is preserved on disk for archeological reference but no longer registered or invoked. Full file deletion deferred to a separate cleanup PR after ≥30 days of Alchemy verification.

Security / posture

  • Pimlico API surface verified pre-implementation via `apps/web/scripts/spikes/v0.11-pimlico-evm-spike.ts`. 4 probes: bundler RPC + EntryPoint v0.7 (PASSED), REST sponsorship policy create (FAILED 401 — Account APIs disabled on tier; informed D60 shared-policy posture), permissionless.js `toSafeSmartAccount` against existing Safe (PASSED), EntryPoint `getNonce` read (false-negative on probe address; not a plan-breaker).
  • **D60 — Pimlico shared-policy model**: cannot programmatically mint per-entity policies on the current Pimlico tier. Single policy (created manually in Pimlico dashboard with `chain_ids=[1, 8453, 42161, 137]`) is shared across all entities. Per-entity caps enforced at the `gas_sponsorship_reservations` layer (race-free advisory-locked reservation per CEO D45 + ENG D30 #10/#11).
  • **D62 — 4337 module enable is non-skippable**: spike Probe 3 confirmed `toSafeSmartAccount({safeModules: [SAFE_4337_MODULE_ADDRESS]})` works against existing Safes, but the Safe must have actually run `enableModule(...)` on-chain or Pimlico returns `paymaster_invalid_safe`.
  • **CEO D20 double-spend prevention**: orchestrator's `silent_drop_pending` state holds the Safe nonce. Treasury fallback NEVER fires while `nonce_claim_status='pimlico_pending'`. The 6h poll worker is the only resolution path.

Test coverage

  • `multisig-controls-pimlico-bundler-client.test.ts` — 13 cases, wire-protocol seam.
  • `multisig-controls-chain-config.test.ts` — 12 cases (4 new in v0.11 for `isSponsorshipChain`).
  • `multisig-controls-safe-4337-enable-proposal.test.ts` — 9 cases, composer purity + drift-catch.
  • `multisig-controls-pimlico-mint-policy.test.ts` — 9 cases, source-grep regression guard for shared-policy stamper.
  • `multisig-controls-reconcile-gas-sponsorship-drift.test.ts` — 10 cases, source-grep guard for reservation sweep + drift-check activation.
  • `multisig-controls-entity-create-mint-events.test.ts` — 5 cases, mint event firing pattern.
  • `multisig-controls-pimlico-pending-poll.test.ts` — 12 cases, source-grep guard for the 6h resolver.
  • `multisig-controls-alchemy-solana-fee-payer.test.ts` — 13 cases, REST adapter unit tests.
  • `multisig-controls-alchemy-solana-mint-policy.test.ts` — 10 cases, mint stamper guard.
  • `multisig-controls-alchemy-solana-cap-reserve.test.ts` — 13 cases, cap-reserve primitives + UI copy + cron unregistration.

0.11.0.0

Documentation cathedral. Top-1%-of-the-industry overhaul targeting human developers and AI agents alike, delivered as two coordinated PRs — one against this repo (code-side), one against [darshanbathija/docs](https://github.com/darshanbathija/docs) (the Mintlify content source proxied at `glide.co/docs/*`). Recursive reviewer mechanic ("Marit Selberg" persona, 8-dimension rubric, real closed-loop AI-agent simulator validation against Zod + JSON Schema + tsc) drove convergence per page. Plan: `~/.claude/plans/glide-oss-is-out-mighty-valley.md` — 6 waves W1–W6, scoped to fill the gaps identified in the pre-cathedral audit (22 of 24 package pages missing, 3 of 10 schemas with no walkthrough, all 22 MCP API pages lacking runnable code examples, no cookbook section, no `examples/` directory, no JSDoc).

Added (Track A — `darshanbathija/docs` content)

  • **22 new package pages under `oss/packages/`** — every `@glideco/*` package now has a Mintlify page at the calibrated quality bar (mean 150L, range 107–231L). Includes new pages for `agent-events`, `agent-identity`, `a2a-resolver`, `acp-merchant`, `ap2-adapter`, `compliance-export`, `connectors-coinbase-x402`, `crosschain-bridge`, `dsar`, `grant-wrapper`, `kms-signer`, `kya-vc`, `mpp-adapter`, `ows-react-native`, `parley-tiers`, `policy-engine`, `recovery`, `schemas`, `secrets-scan`, `smart-router`, `spend-export`, `ucp-profile`, `x402-facilitator` (existing `anomaly` and `explainer` pages light-polished). Each page: 1-paragraph hook, install + npm link, "Why" rationale, API surface table, 2-4 worked code examples (5-15 lines each, realistic values), edge cases, extension points, reading list with cross-links.
  • **3 new schema walkthroughs under `oss/standards/`** — `agent-activity-event.mdx`, `scoped-grant-claims.mdx`, `_types.mdx`, plus a synthesis page `lifecycle.mdx` with a Mermaid sequence diagram tracing one agent payment from `AgentPolicyEnvelope` → `Grant` → `ScopedGrantClaims` → MCP tool call → `Receipt` → `AgentActivityEvent` audit row. Each schema page: canonical URL, required-fields table, optional-fields table, example with Zod parse, 3-path validation (Zod / `validate-manifests.mjs` / `ajv-cli`), common pitfalls.
  • **22 MCP API ref pages augmented with runnable code examples** — every page under `agents/api/*.mdx` now ships a `<CodeGroup>` with curl + TypeScript + Python tabs (realistic non-foo values, idempotency keys where applicable), success + error response bodies, an Errors table tailored to what each tool actually throws (sourced from `apps/mcp/src/tools/error-taxonomy.ts`), and a Step-up flow walkthrough on the 4 step-up-requiring tools (`agent-grant-issue`, `cards-issue`, `payments-initiate`, `transfer-schedule`, `vault-rotateSigner`, conditionally `yield-allocate`).
  • **`agents/api/_clients.mdx`** — single-source reference for the curl auth-header construction, full TypeScript `GlideClient` class (~90 lines, generic `call<T>` + typed `StepUpRequired` discriminator + `GlideApiError`), full Python equivalent (~65 lines using `httpx`, `StepUpRequired` NamedTuple). Each per-tool page now `import`s from `./glide-client` consistently.
  • **New `Cookbook` tab in `docs.json`** with 9 MDX recipes (`cookbook/index`, `curl-quickstart`, `typescript-quickstart`, `python-quickstart`, `agent-pays-vendor`, `build-a-connector`, `build-a-skill`, `embed-ows-react-native`, `x402-paid-api`). Each recipe links to a runnable mini-project in `axtior-neobank/examples/`.
  • **`docs.json` Standards group extended** to include `scoped-grant-claims`, `agent-activity-event`, `_types`, and `lifecycle` in nav order.

Added (Track B — this repo, code-side)

  • **`examples/` directory** with 8 standalone runnable mini-projects: `curl-quickstart`, `typescript-quickstart`, `python-quickstart`, `agent-pays-vendor` (full lifecycle — AgentPolicyEnvelope to AgentActivityEvent), `build-a-connector-stripe-clone`, `build-a-skill-spend-export`, `embed-ows-react-native`, `x402-paid-api`. Each project: minimal `package.json` (or `pyproject.toml`), `README.md` (3-paragraph: what / run / extend), runnable `pnpm dev` (or `python src/main.py`), smoke test.
  • **JSDoc/TSDoc on every public export** of all 24 `packages/*/src/index.ts` (and key re-exported files). Zero coverage today → near-100% on the public API surface. `@param`, `@returns`, `@example`, `@see` cross-link to the Mintlify package page. Unblocks IDE hover, typedoc generation, and machine-readable surfaces.
  • **22 lagging `packages/*/README.md` leveled to the template** at `docs/oss/_templates/package-readme.md`. Voice consistency: every README hits 80–250 lines with a "what this is / what this is not", install, quickstart, common patterns (3 worked examples), API surface table, safety contract, trust tier, related packages, stability, source link.
  • **`scripts/sim-validators/`** — closed-loop validation infrastructure for the AI-agent simulator: `json-schema.mjs` (ajv strict against `apps/web/public/schemas/agent-banking/v1/*`), `zod-parse.mjs` (imports `@glideco/schemas` and runs `.safeParse()` with structured Zod errors), `ts-compile.mjs` (`tsc --noEmit --strict` on stdin), `yaml-lint.mjs` (skill manifests, with optional Zod hand-off).
  • **`scripts/audit-docs.mjs`** — aggregate health check: package README compliance with template skeleton, JSDoc coverage on `packages/*/src/index.ts`, schema-doc drift detection, vale violation count, lychee broken-link count. Surfaced as `pnpm docs:health`.
  • **`.vale/styles/Glide/`** custom style pack: `Marketing.yml` bans 30+ marketing words (seamlessly, robust, comprehensive, leverage, utilize, etc), `Hedges.yml` bans LLM-rubric-pleasing hedges (comprehensively, carefully crafted, in summary, etc), `Exclamation.yml` enforces no exclamation marks. `.vale/Vocab/Glide/accept.txt` for project-specific terms.
  • **`lychee.toml`** — link checker config with sensible exclusions (mock vendor URLs, your-domain placeholders, localhost) and accepted status codes (200/300/401/403/429 for hostile-to-bots referrers).
  • **`.markdownlint.json`** — light formatting rules (MD013 disabled for prose, MD024 sibling-only for repeated H2s under different parents, MD041 disabled for files with frontmatter).
  • **`typedoc.json`** — root config with `entryPointStrategy: "packages"` for all 24 packages. Output `docs/api/`, ingested by Mintlify.
  • **`docs/oss/_templates/package-readme.md`** — canonical template for the 22 leveled-up package READMEs.
  • **Root README rewrite** — 239L → 95L "lobby" with 4-tile decision matrix (Try the demo / Self-host / Build an agent / Add a connector), Mermaid architecture diagram showing the request lifecycle through MCP gateway + policy engine + audit log, F1–F7 money-safety contracts table, trust-tier table, run-it-locally one-command block.
  • **`SECURITY.md` rewrite** — 58L stub → 120L disclosure policy with severity matrix (Critical / High / Medium / Low with examples and per-severity SLA), explicit safe-harbor language covering CFAA + DMCA + foreign analogues, threat-model summary linking to `glide.co/docs/oss/security/threat-model`, scope in/out, hall of fame placeholder, PGP key on request.
  • **Root `package.json` scripts**: `docs:health`, `docs:health:check`, `docs:lint`, `docs:check`, `docs:api`.

Changed

  • **`apps/web/public/schemas/agent-banking/v1/_types.json` `agentScope` enum** — added `beneficiary:read` and `kyc:start` to bring the published JSON Schema in sync with the Zod source of truth (`packages/schemas/src/agent-scope.ts`). The Zod source is canonical; the published JSON Schema is hand-curated and had drifted to 13 of 15 values. Caught by the AI-agent simulator's first cross-check pass.

Reviewer mechanic

  • **Marit persona reviewer** (general-purpose subagent, Sonnet 4.6 default + Opus 4.7 cycle-3 sanity pass) scored every shipped doc on 8 dimensions (hook, runnable examples, completeness, cross-linking, AI-agent friendliness, multi-audience clarity, voice, visual altitude) with hard convergence at ≥9.0 per dimension and ≤5 minor findings.
  • **AI-agent simulator** runs after dimensions clear ≥9.0: simulator subagent reads ONE doc, produces an artifact (TS integration / Zod schema instance / curl invocation / cookbook scaffold), validators in `scripts/sim-validators/` fail-the-doc on real `tsc --noEmit`, real Zod `.safeParse()`, real `ajv` JSON Schema validation, real `yaml-lint`. The schema drift fix above (agentScope enum) was caught by this pass.
  • **Cost budget**: 4 Agent calls per doc expected, 8 hard cap; with batching across the 22 package pages, 22 API pages, and lagging READMEs, ~245 total Agent calls vs 888 worst case.

Removed

  • Nothing. The cathedral is purely additive on existing docs (no page deletions). Old paths in the docs repo continue to work; the existing OSS section was extended, not restructured.

0.10.0.0

Multi-sig controls v0.10 mega-release + v0.11 polish. The full §A–§G architectural surface — vault Controls page, org defaults, value-rules engine, gas sponsorship, Self-Custody Plus, and the TradeoffPanel — ships in a single PR per CEO D2 mega-release decision. Maintenance window deploy per CEO D32 with feature flags off until QA passes on prod. Old paths (`/members`, `/policies`, `/recovery`, `/settings`) keep working via 308 redirects regardless of flag state.

Added

  • **`/dashboard/vault/[id]/controls/*` consolidated controls page** — Access · Backup · Rules · Operations sub-tabs with `VaultStateHeader` showing vault name, threshold chip, backup tier, pending-action count, and gas status at a glance. Operations container houses Recovery, Gas, and Audit pages.
  • **`/dashboard/settings/multisig` org defaults** — admin-gated typed-table inheritance for new vaults (threshold, recovery, timelock, glide_backup_tier, gas caps). Vault overrides surface "Inherited from org" chips via `<InheritedField>`. Recommendations are advisory (CEO D12 — server NEVER blocks). One-click "Apply defaults to existing vaults" fan-out for admins.
  • **`/consumer/settings/security`** — beneficiaries dual-surface pattern. Same component as dashboard Controls page, slimmer copy, single-vault default.
  • **`/controls/operations/recovery/survival`** — "If Glide vanishes" survival guide with exact Safe Wallet + Squads + Etherscan steps to recover the vault without Glide infrastructure (locked deferred cherry-pick D9).
  • **Self-Custody Plus opt-in flow at `/controls/backup/upgrade`** — 5-step idempotent ceremony binding a Privy passkey-backed wallet, adding it as a Safe owner via M-of-N, and removing Glide Backup (Seat R). Universal flow-start TradeoffPanel per design D41.
  • **Value rules engine** — `multisig_value_rules` + `multisig_value_rule_sets` (Fireblocks TAP-style ordered rules + drafts/publish workflow). 6 action types (allow/deny/require_extra_sig/delay_hours/require_admin_pre_approval). 500ms timeout fail-closed (CEO D15). Wired into `proposeTransfer`, `proposeBatchTransfer`, `proposeSolanaTransfer` (transfer-paths cover the dominant value-movement flows; cross-chain/swap/contract-call propose-time wiring deferred to v0.12 per the codex #7 swap-valuation gap analysis). Three pre-defined templates ship at first visit (Treasury Conservative, Ops Default, Lite — CEO D4).
  • **`<TradeoffPanel>` reusable Casa-style modal** — 8 copy bundles with severity tiers (catastrophic/consequential/soft) plus `assertTradeoffAccepted` server-side validator and a custom `@glide/multisig-controls/no-trust-altering-without-tradeoff-panel` ESLint rule that blocks unattributed weakening calls at lint time.
  • **Pimlico paymaster integration (EVM)** — `PaymasterProvider` interface with Pimlico impl (`pimlico-evm-broadcast.ts`, `permissionless.js` UserOperation pipeline). Safe 4337 module enable proposal kind. Per-entity `pimlico_sponsorship_policy_id` minted asynchronously via Inngest at entity-create. Race-free Safe-nonce CAS state machine + advisory locks for double-spend prevention (CEO D20 + D27).
  • **Solana sponsored broadcast (`submitSolanaBroadcast`)** — VersionedTransaction v0 verifier with semantic instruction equality checks, post-broadcast txHash verification, and a self-hosted fee-payer cap (`default_solana_gas_monthly_cap_lamports`). Solana relayer balance monitoring cron via Inngest.
  • **Glide-recommended baselines** — Solo Operator / Small Team 2-of-3 / Treasury 4-of-7 picker on `/dashboard/entities/new` (CEO D5 cherry-pick; `/select-type` keeps the binary "Just me / Team multisig" picker, named baselines surface deferred).
  • **Quorum guards** — `proposeRemoveMember`, `proposeChangeThreshold`, and Solana siblings reject any change that would orphan signing or exceed owner count (CEO D21 plus D30 off-by-one corrections).
  • **Migrations 0085–0096** (0086 intentionally skipped — no enum value needed): entity_multisig_defaults (0085), multisig_value_rules + sets (0087), entities.gas_sponsorship_provider columns (0088), gas_sponsorship_ledger + reservations (0089), multisig_proposal_kind 'enable_module' (0090), entity_multisig_defaults.default_value_rule_set_id FK (0091), user_signer_wallets + DB-trigger sync + null-aware backfill (0092), multisig_proposals nonce_claim cols + multisig_nonce_claim_status enum + CHECK constraint (0093), value_movement_ledger (0094, ENG D31). v0.11 polish adds multisig_key_health_checks (0095) and drops unused multisig_policies caps (0096).
  • **Casa-style Key Health Check** — quarterly `recordKeyHealthCheck` proc with daily 03:00 UTC Inngest cron sweeping for users with no recent passing check. Surfaces nudge UI on `/controls/operations/recovery` and consumer security page.
  • **Audit log diff (D7 deferred from v0.10)** — `getConfigProposalDiff` reads `payload.preState` snapshots written from v0.11 onward. Proposals from v0.10 lack snapshots and show "Pre-v0.11 proposal" placeholder.
  • **Drift detection** — `listVaultDrift` returns vaults whose settings diverge from org defaults; admin dashboard surfaces them with a one-click apply-defaults action.
  • **Token-pricing oracle** — `lookupUsdCents` in `price-feed.ts` with extended stablecoin shortcut list and Coingecko-backed price for ETH/SOL/MATIC/WBTC/LINK/AAVE/UNI/ARB/OP/AVAX/BNB. Wired into the value-movement ledger writer to value swaps + contract calls (closes the codex #7 v0.10 known limitation for executed-tx audit; propose-time wiring for swap/cross-chain/contract-call still deferred to v0.12).
  • **Gas sponsorship reconciliation** — Inngest crons every 6h reconcile Pimlico usage API + on-chain Solana fees against `gas_sponsorship_ledger`; alert on drift > $1.

Changed

  • **Vault-detail header chip ribbon** — Members, Policies, Recovery, Settings paths consolidated behind a single "Controls" entry pointing at the new sub-tabbed page. The four old paths become 308 redirects to preserve email-link integrity.
  • **`getMyPrimarySendable`, `prepareSign`, `submitSignature`** — accept optional `signerWalletId` to select among multiple wallets (Self-Custody Plus support). Defaults to primary if omitted; existing clients work unchanged.
  • **`recommends_glide_backup` advisory** — vaults attempting more permissive overrides see a TradeoffPanel warning at submit but the writes succeed (per CEO D12 user-agency-over-org-control bias).
  • **`FALLBACK_SOLANA_GAS_MONTHLY_CAP_LAMPORTS`** corrected from `100_000_000_000n` (= 100 SOL ≈ $20k) to `100_000_000n` (= 0.1 SOL ≈ $20). The previous value was 1000× too high vs the plan's documented $20/mo conservative posture. UI conversion at `/dashboard/settings/multisig` paired-fixed from `1_000_000` lamports/SOL to the correct `1_000_000_000`.

Fixed

  • **`applyDefaultsToExistingVaults` CAS conflict handling** — now throws `PRECONDITION_FAILED` on lost CAS so the surrounding transaction rolls back. Previously silently skipped the conflicting row and returned a misleading `changedCount`.
  • **`submitSolanaBroadcast` auth-before-env order** — load proposal + entity-member + vault-manager gate now fires BEFORE the sponsorship-env check. Previously any authenticated caller could probe sponsorship config with arbitrary proposal UUIDs.
  • **Postgres trigger search_path hardening** — `sync_users_primary_wallet()` pinned to `pg_catalog, public` with explicit `SECURITY INVOKER`, blocking session-level search_path shadowing attacks.
  • **OwnerChangeModal threads `acceptedAt`** through every weakening propose call. Lint exemption removed so future trust-altering mutations added to the file get caught.

Removed

  • **Phantom mutations** `multisig.disableTimelock` and `multisig.raisePolicyMaxPerDayCap` removed from `MUTATIONS_REQUIRING_TRADEOFF` — they don't exist as routes. Replaced with the actual conditional procs (`updateVaultSettings`, `updateEntityMultisigDefaults`, `applyDefaultsToExistingVaults`).
  • **Migration 0096 (deferred to post-deploy maintenance window)** drops unused `multisig_policies.max_per_week_cents` + `max_per_month_cents` columns now that the value-rules engine supersedes them for all per-7d / per-30d cases.

0.9.1.0

Email case-normalization sweep — closes the deferred follow-up from PR #319's review trail. Glide now treats provider-style case-insensitive identity as the rule everywhere: `Alice@Acme.com` and `alice@acme.com` are the same Glide user across the entire surface (Send by email, Pay, Split, entity invites, Telegram bot, contact import, paylinks, money requests).

Added

  • `apps/web/src/server/lib/find-user-by-email.ts` — single read-side normalizer used at every email lookup callsite. Lowercases input before the `eq()` so callers can't backslide.
  • `apps/web/drizzle/0084_users_email_lowercase.sql` — idempotent migration that lowercases existing `users.email` + `contacts.email`, adds CHECK constraints enforcing lowercase forever, and creates a functional index `users_email_lower_idx` on `LOWER(email)` for the three PR #319 defensive lookup sites. Pre-flight gate hard-fails on case-only duplicates with a pointer to the new operator runbook (`docs/runbooks/email-case-merge.md`). `LOCK TABLE users + contacts IN SHARE ROW EXCLUSIVE MODE` closes the TOCTOU window between the pre-flight check and the backfill UPDATE so a concurrent signup can't trigger a UNIQUE violation mid-migration.
  • `docs/runbooks/email-case-merge.md` — operator runbook for the rare case the pre-flight finds duplicates. Documents the merge procedure (canonical row selection by `lastLoginAt`, FK repointing across wallets, balances, KYC, multisig membership, audit trail snapshot, tombstone strategy).
  • `apps/web/tests/email-normalization.test.ts` — 24 PGlite-backed integration tests covering the helper, migration phases, CHECK constraints (INSERT + UPDATE rejection), idempotence on rerun, dedupe order in `importContacts`, sentinel uniqueness, and a production-source drift guard.

Changed

  • All four `users.email` writers now lowercase before insert: trpc.ts auto-create (with a per-Privy-id sentinel that prevents two no-email signups from colliding on `users_email_unique`), entity.ts invite-create-user fallback, transfer.ts sendToEmail referee insert, consumer-pay.ts paylink referee insert.
  • Eight lookup callsites refactored to route through `findUserByEmail`: split.ts × 2, entity.ts (invite resolver), transfer.ts (recipient lookup), consumer-pay.ts × 4, plus the two Telegram bot commands (`/pay` and `/confirm`). The three PR #319 defensive `LOWER(${users.email})` sites stay as-is; the new functional index keeps them off the seq-scan path.
  • `consumer-pay.ts:719-727` `importContacts` dedupe order fixed: lowercase before building the `Set` AND before the `.has()` check. Pre-fix, `Alice@x.com` could slip past existing `alice@x.com` and create a duplicate contact row.
  • `split.ts:209` `moneyRequests.toEmail` now stored lowercase + the Inngest notification recipient lowercased to match. Closes the cross-confirmed test+maintainability finding that the same procedure normalized for read but wrote raw.

Fixed

  • `unknown@glide.co` sentinel time-bomb. Two no-email Privy signups would have collided on `users_email_unique` and locked out the second user; sentinel is now `unknown+${privyUserId}@glide.co` so each no-email identity gets a unique row.
  • Stale comment block at `entity.ts:232-261` that claimed `users.email` is stored as-entered (false post-PR) and recommended adding a `LOWER(email)` index that this migration creates. Comment now describes the post-0084 defence-in-depth posture accurately.

0.9.0.0

Safe-based Send + M-of-N team multisig. The Send page now broadcasts USDC out of the user's Safe (EVM) or Squads vault (Solana) instead of the embedded EOA. The form drives the existing M-of-N pipeline end-to-end: 1/1 users sign inline + the treasury relayer broadcasts; M-of-N parks at "X of N collected" with the remaining signers picking it up from the Actions inbox. Solana proposes inline and routes signing to Actions where the Squads-aware modal already lives. Vault-custody users no longer see the EOA wallet-side Send path (kept behind a flag for the upcoming lite-tier signup that skips multisig genesis to save L2 gas). Vault genesis got a binary "Just me / Team multisig" picker on the entity creation page. Team mode resolves member emails → users → Privy embedded-wallet addresses inside the entity-creation transaction, validates everyone has both EVM and Solana wallets ready, and bakes the resolved set into the Safe initializer + Squads multisig at CREATE2 / PDA derivation time. Genesis owner set is FROZEN on-chain after the commit; later adds, removes, and threshold changes go through the existing M-of-N config-tx proposals that land in Actions. The new vault members page at `/dashboard/vault/[id]/members` surfaces the on-chain owner sets per chain (Safe owners + EVM threshold, Squads members + Solana threshold), the off-chain `multisig_vault_members` rows (who Glide UI considers a manager), and pending invites. Inline buttons under each chain section open a propose modal — one for Add owner, one for Remove owner, one for Change threshold — that wraps the existing `proposeAddMember` / `proposeRemoveMember` / `proposeChangeThreshold` (EVM Safe) and `proposeSolanaAddMember` / `proposeSolanaRemoveMember` / `proposeSolanaChangeThreshold` (Squads) procedures. The modal accepts an email for Add (resolves via a new `multisig.resolveEmailForOwnerProposal` query with the same case-collision + self-rejection guards as the team-genesis path), a member dropdown for Remove, and a number input for Change threshold. EVM ops fan out across enabled chains (one proposal per chain, with per-chain partial-success reporting); Solana is single-network. Each proposal lands in the Actions inbox for current threshold sigs. The team picker also lands at `/select-type` for the Company branch — picking "Team multisig" routes the user through `/dashboard/entities/new?prefill=team` so the same email-resolver + threshold validator + provisioning path handles genesis without duplicating the contract across two procedures. `user.setUserType` gains an optional `skipAutoEntityCreate` flag that the picker uses to suppress the historical default-1/1-entity insert when the user has explicitly opted into team mode. `VaultSendCryptoForm` was split from a single 683-line component into four files (`index.tsx` orchestrator, `RecipientField`, `AmountInput`, `SignFlow`) with a shared `types.ts` that holds the validators. Mechanical refactor; zero functional change. The Solana recipient validator is now a standalone `validateRecipient(chain, value)` util, reusable by future "Send NFT" / "Send to beneficiary" surfaces. `multisig.getMyPrimarySendable` is the new query the Send form drives off — given the active entity, it picks the primary vault, picks the EVM + Solana wallet shells, lists chain deployments (sorted ethereum / base / arbitrum / polygon / others), and reports whether the caller is a signer. `multisig.getApprovalProgress` is the lightweight scoped read the form polls after a signature lands so it can render an accurate "X of N collected" line without dragging in `listPendingProposals`'s broader payload. Adversarial review pass before merge surfaced (and fixed) eight critical findings: The legacy `WalletSideSendCryptoForm` (v0.6.0.0–v0.7.0.0 wallet-side path) and the original `SendCryptoForm` (Bridge offramp) are both retained behind feature flags. `NEXT_PUBLIC_VAULT_BASED_SEND_ENABLED` (default off) drives the new Vault-based form; `NEXT_PUBLIC_WALLET_SIDE_SEND_ENABLED` stays around for the lite-tier rollout that bypasses multisig genesis. Production cutover for vault-custody users flips `NEXT_PUBLIC_VAULT_BASED_SEND_ENABLED` to `true`; legacy users continue on the existing path until the lite-tier ships. Coverage: vault-provision EVM + Solana M-of-N happy paths and threshold/dup gates pinned in `vault-provision-integration.test.ts` (4 new cases). Form state machine + members page deferred to E2E. `getApprovalProgress` and `getMyPrimarySendable` deferred to E2E (caller fixtures are heavy for the surface). 3973 vitest pass on full suite.

Added

  • `VaultSendCryptoForm` — Send USDC out of Safe / Squads with inline 1/1 sign + execute, M-of-N propose + park. Split into 4 files (`index.tsx`, `RecipientField`, `AmountInput`, `SignFlow`) plus shared `types.ts`.
  • `multisig.getMyPrimarySendable` — entity-scoped vault + wallet + chain query for the form.
  • `multisig.getApprovalProgress` — scoped X-of-N read for the form's pending state.
  • `multisig.resolveEmailForOwnerProposal` — email → (userId, EVM EOA, Solana pubkey, isAlreadyOwner) resolver with case-collision + self-rejection guards. Drives the inline Add Owner modal.
  • `/dashboard/vault/[id]/members` — vault members page with on-chain summary + inline Add / Remove / Change-threshold buttons under each chain section.
  • `OwnerChangeModal` — single modal handling all 3 owner-change actions × 2 chain families with EVM fan-out across enabled chains.
  • `/select-type` team picker — second-stage "Just me / Team multisig" picker on the Company branch. Team route redirects to `/entities/new?prefill=team`.
  • `/dashboard/entities/new?prefill=team` — query param pre-selects team mode for users arriving from the signup picker.
  • `user.setUserType` accepts optional `skipAutoEntityCreate` to suppress the default 1/1 entity insert when the user has explicitly opted into team mode.
  • Team mode in `/dashboard/entities/new` — emails + threshold picker; resolver in `entity.createEntity`.
  • `provisionEvmVaultForEntity` + `provisionSolanaVaultForEntity` — `additionalSigners` + `threshold` params for M-of-N genesis.
  • `multisig.inviteVaultMember` case-insensitive email lookup.
  • Soft client-side dedupe (60s same-fingerprint confirm) on the Send form.
  • `docs/runbooks/safe-send.md` — operator runbook (cutover flag, debugging stuck `'sign'` state, manual broadcast when relayer is down, forensic queries on `multisig_proposals`, what NOT to do for genesis misconfigurations).

Changed

  • Send page Crypto tab dispatches via `NEXT_PUBLIC_VAULT_BASED_SEND_ENABLED`. Default off; flip in Vercel prod env to roll out.
  • `getMyPrimarySendable` returns the minimum payload for non-signer entity members (vaultId + name + threshold + `isUserSigner: false`); chain data and addresses are gated server-side.
  • `getApprovalProgress` gates on vault membership (not entity role) and returns `null` for non-members.
  • Post-sign relayer error on a fully-signed proposal lands in `pending` with an Actions-inbox path instead of `error`.

Fixed

  • Eight adversarial-review findings (see narrative above).

0.8.0.0

Noah webhooks — Release 1 of the vendor-by-vendor webhook plan. Noah was registered in the dashboard but the route on `main` couldn't actually process Noah's payloads — the dispatch expected dotted event names (`customer.approved`) while Noah sends a coarse `EventType: "Customer" | "FiatDeposit" | "Transaction"` model with a `Status` discriminator. Every event was being persisted-and-acked as a silent no-op. This release ships a payload normalizer that translates the PascalCase shape into the synthetic dotted keys, plus framework helpers that compound across every future vendor release. Per-resource EventVersion deduplication via a new `noah_resource_versions` table (migration 0082) handles Noah's out-of-order retry semantics — older versions are dropped on the floor. The `tryClaimEventVersion` helper does an atomic conditional UPSERT (`ON CONFLICT DO UPDATE WHERE event_version < EXCLUDED.event_version`) so concurrent deliveries can race safely. Operator replay sets a `skipVersionGuard` flag — without it, replay would deterministically no-op and the admin UI would falsely report success. Three framework promotions land alongside Noah and apply to every future vendor release: Reconciliation backstop at `apps/web/src/inngest/functions/poll-noah-pending.ts` runs every 30 minutes and queries Noah's REST API for any `transactions` or `kyc_records` row stuck in `pending` past the expected window — defense in depth if Noah's 7 retries (~52h max) all 401 or 5xx. Plus a generic `alert-stuck-webhooks` cron that fires every 5 minutes and pages on any `webhook_events` row older than 5 minutes with `processed_at IS NULL` (per-vendor breakdown in the structured warn). Refunds inside `FiatDeposit` payloads write an audit row only — proper balance reversal needs a debit + journal-entry path that's out of scope for Release 1, tracked as a follow-up. The audit row carries `metadata.balanceReversalPending=true` so ops can identify rows needing manual reconciliation. Pre-merge prerequisites:

Added

  • Noah webhook payload normalizer (`apps/web/src/app/api/webhooks/noah/payload.ts`) translating Noah's PascalCase EventType+Status model into the synthetic dotted names the existing handlers read.
  • Per-resource EventVersion dedupe via `noah_resource_versions` table (migration 0082) and `tryClaimEventVersion` helper.
  • Optional IP allow-list helper at `apps/web/src/server/lib/webhook-ip-allowlist.ts` (IPv4 + CIDR, XFF first-entry).
  • `/admin/webhooks` observability + replay UI with vendor + status filters, payload inspection, one-click replay.
  • `adminWebhooksRouter` tRPC router (`list` / `detail` / `replay` / `counts` procedures, all `adminProcedure`-gated).
  • `poll-noah-pending` Inngest function (30-min cron, throttled 1/30m, concurrency 1).
  • `alert-stuck-webhooks` Inngest function (5-min cron, structured per-vendor warns).
  • `__replayNoahPayload` exported from the Noah route for the admin replay path.
  • Operator runbook at `docs/runbooks/noah-webhooks.md`.

Changed

  • `createWebhookHandler` signature now accepts an optional `options` object with `normalize` callback and `ipAllowListEnv` env-var name. Existing 5 callers (aeon, avenia, due, manteca, monerium) are unaffected because options is optional.
  • Noah webhook handler's `handleFiatDeposit` now gates the Inngest `deposit/received` emission on `event.type === 'fiat_deposit.settled'` — Pending and Failed no longer trigger spurious credits.

Fixed

  • Noah webhook dispatch now matches Noah's actual payload shape — every Noah event was being silently no-op'd on `main` because the switch keys (`customer.approved`, `transaction.settled`) never matched what Noah actually sends (`EventType: "Customer"` + `Status`).
  • Replay no-op bug: `__replayNoahPayload` now passes `skipVersionGuard: true`. Without this fix, replay would always silently no-op (version equals stored, not strictly greater) while the admin UI reported success.
  • Refund handling no longer flips `transactions.status='returned'` without a corresponding balance debit — this was misleading UX. Audit row only until proper reversal flow ships.

0.7.0.0

The v0.6.0.0 V6-R hardening sweep — 29 follow-ups closed across one mega-PR. Cross-chain SEND now ships unblocked: a new Inngest function (`crypto-send-discover-destination`) polls Particle UA REST every 5min to populate the destination-side tx hash, and `crypto-send-confirm`'s verifiers now accept bridge contract addresses (CCTP / Wormhole) as legitimate destination-side senders. The `NEXT_PUBLIC_WALLET_SIDE_CROSSCHAIN_ENABLED` flag default flipped from `false` → `true` (Wave 4); the Particle REST API key boot assertion makes the worker fail-loud if the env var is missing. `recordCryptoSend.input.amountCents` removed — the server now derives USD-cents from `(asset, amountRaw, decimals)` via a new `lib/price-feed.ts` module. Stablecoins shortcut to 1:1; ETH/SOL/MATIC use Coingecko (60s cache). Feed unavailable → audit row persists with `metadata.amountUsdMissing=true` and the UI shows "Pending USD valuation" instead of `$0.00`. The Convert rails widen from 2 buckets (`usdc_solana`/`usdc_evm`) to 9 per-(symbol, network) keys (`USDC_solana`, `USDC_evm`, `USDT_solana`, `USDT_evm`, `ETH_evm`, `MATIC_polygon`, `SOL_solana`, `USDC_polygon`, `USDT_polygon`). Migration 0078 renames existing rows in a transaction with two safety gates (RAISE on unrecognized rail values; RAISE on would-be collisions). DRY consolidation: `lib/explorer-url.ts` + `lib/format-address.ts` extracted from 9+ inline reimplementations. `WALLET_BROADCAST_DEFAULT_TIMEOUT_MS` const replaces 5 magic-number duplicates. `RAIL_CODES` is now exported as a runtime const from `lib/fee-model.ts`. Pre-merge prerequisites:

Added

  • **R28a — `apps/web/src/lib/wallet/bridge-contracts.ts`**: per-chain registry of acceptable destination-side bridge sender addresses (CCTP v1+v2 TokenMessenger + MessageTransmitter on ethereum; CCTP v2 on base/arbitrum/polygon; Wormhole TokenBridge on ethereum+solana). ZERO_ADDRESS accepted for CCTP mint events (F2). Pinned from public Circle + Wormhole docs.
  • **R28d — `apps/web/src/server/lib/particle-rest.ts`**: Particle UA REST client with strict zod schema (22 vitest cases). JSON-RPC 2.0 / Basic auth via `NEXT_PUBLIC_PARTICLE_*` env vars. Credentials validated at handler invocation time — missing env causes Inngest function failure+retry rather than crashing the serve route (F1).
  • **R28e — `apps/web/src/inngest/functions/crypto-send-discover-destination.ts`**: cron `*/5`, separate concurrency budget. Finds rows awaiting destination resolution, polls Particle, UPDATEs metadata via CAS-on-status. `Promise.allSettled` with concurrency=5 chunks. 429 → adaptive backoff via `Retry-After`. 401/schema_drift → log + metric (page on-call).
  • **R29 — Solana → EVM destination-chain selector** in `SendCryptoForm`. Lifts the v0.6.0.0 hard-block; user picks ethereum/base/arbitrum/polygon when format detection alone returns 'evm'.
  • **R30 — `apps/web/src/server/lib/price-feed.ts`**: server-side price feed with 60s cache. Stablecoin shortcut + Coingecko fallback. Returns null on unavailability; audit row carries `metadata.amountUsdMissing=true`.
  • **R12 — per-(symbol, network) Convert rails**. New `RAIL_CODES`: `USDC_solana`, `USDC_evm`, `USDT_solana`, `USDT_evm`, `ETH_evm`, `MATIC_polygon`, `SOL_solana`, `USDC_polygon`, `USDT_polygon` (G1 added the latter two to match the 0078 migration allowlist). Each gets its own admin-tunable spread row.
  • **`docs/designs/cross-chain-send/v0.7.0.0.md`**: full architectural record. Particle audit decision (polling fallback chosen; SSE re-shape reversible), bridge contract sources + rotation runbook, R30 downstream consumer audit, forward-fix-only commitments, pre-merge prerequisites checklist.

Changed

  • **`recordCryptoSend.input`**: `amountCents` removed; `decimals` added (required). Server derives `amountCents` from `(asset, amountRaw, decimals)` via `lib/price-feed.ts`. Verified zero external SDK consumers.
  • **`tx-confirmation.ts` verifiers**: `expectedSender` accepts `string | ReadonlyArray<string>`. Same-chain rows pass user wallet; cross-chain rows pass bridge address list. Verifier returns 'confirmed' if `tx.from` matches ANY in the list.
  • **`crypto-send-confirm.ts`**: cross-chain rows look up `expectedSender` from `bridge-contracts.ts` based on destination chain + asset kind. Backlog observability (R23): warns when candidates > 80% FIND_LIMIT or oldest pending row > 12h.
  • **Solana commitment**: `confirmSolanaTx` + `confirmSolanaSplTransfer` + `confirmSolanaNativeTransfer` switched from `'confirmed'` (~150 slots, fork-resistant) to `'finalized'` (~30s, supermajority lockout) for audit-confirmation reads. (R02)
  • **`preflightSendScreen` rate limit**: bucketed by userId via new `enforceAuthenticatedRateLimit`. Drops the IP+bot-UA bypass that an authenticated attacker on residential proxies could rotate around. (R01)
  • **Admin convert-spreads upsert**: wrapped in `ctx.db.transaction` + `SELECT ... FOR UPDATE` so concurrent admin edits serialize. (R04)
  • **`convertSpreadsUpsert` caps**: `spreadBps.max(10000) → max(500)`, `flatFeeCents.max(100_000) → max(10_000)`. Prevents typo-zero-out. (R05)
  • **`use-pending-send-resume`**: per-`txHash` localStorage keyspace + 32-entry cap (oldest-evicted by submittedAtUnixMs). Resume orchestrator iterates pending entries instead of clobbering. (R06)
  • **tx-detail polling**: `useRef`-backed `startedAt` anchor — visibility flips no longer reset the cadence to tier-1 (5s). (R07)
  • **SendCryptoForm amount parsing**: token-mode uses string→bigint via new `parseDecimalToBigInt` helper. Wei-level precision preserved at decimals=18. (R13)
  • **`getTransactionByHash`**: lower-cases EVM hashes on lookup ('0x' prefix detection); Solana base58 stays case-sensitive. Migration 0077 ships the matching functional `lower()` partial index. (R27)
  • **`beneficiary.create` + `findCryptoRecipientByAddress`**: normalizes chain alias before persist + lookup. New zod superRefine: `kind='crypto_recipient'` requires address+chain. (R03)
  • **`SendCryptoForm.tsx` split**: directory with `index.tsx` (orchestrator) + `AssetPicker.tsx` + `SuccessPanel.tsx` + `types.ts`. (Wave 0a)
  • **DRY extractions**: `lib/explorer-url.ts` (R14), `lib/format-address.ts` (R15), `WALLET_BROADCAST_DEFAULT_TIMEOUT_MS` const (R16), `RAIL_CODES` runtime export (R17). (Wave 0)
  • **R19**: `/admin/convert-spreads` delete confirmation now uses a styled modal (16px radius, dark palette) instead of `window.confirm()`. (Wave 5)
  • **R20**: inline aria-invalid + error paragraph on bps/cents inputs in `/admin/convert-spreads`. (Wave 5)
  • **R21**: `<h1>Transaction</h1>` (Playfair Display) added to `/dashboard/transactions/[txHash]`. (Wave 5)
  • **R22**: `AssetPicker` mobile bottom sheet on `<sm` (16px top corners, scroll-contained, safe-area-inset-bottom). (Wave 5)
  • **R24**: `useMemo` on `detectedRecipientChain` / `sourcePositionChain` / `isCrossChain` derivations in `SendCryptoForm`. (Wave 5)

Removed

  • **R26**: `executeRelayCrossChain` (test-only stub) + `tokensForChain` (unused export). Drops 2 dead functions and their tests.
  • **`amountCents` from `recordCryptoSend.input`**: see Changed above.

Migrations

  • **0077** (R10/R11/eng review 4.3): JSONB indexes on `transactions` re-scoped by `vendor='self_custody'`. New partial index on `(metadata->>'txHash')` + functional `LOWER(...)` indexes for the EVM case-insensitive lookup path (R27 hot path). CONCURRENTLY — apply via AUTOCOMMIT branch.
  • **0078** (R12): renames lowercase rail keys (`usdc_solana` → `USDC_solana`, `usdc_evm` → `USDC_evm`) in `convert_rail_spreads`. Transactional with two safety gates (unrecognized values, would-be collisions). Allowlist includes all 9 keys including `USDC_polygon` + `USDT_polygon` (G1 fix).

Followups

  • **v0.8.0.0**: flip `Quote.targetToSourceRate` to required + drop fallback (R25). Split `apps/web/src/server/routers/transfer.ts` (eng review eng-2). See `~/.gstack/projects/.../release-checklist.md`.
  • Particle docs audit before merge (eng review fork) — `particle-rest.ts` schema is best-effort.
  • Bridge contract addresses verification before merge.

0.6.0.0

The "wallet-side Send Crypto" ship plus admin-tunable Convert spreads. Replaces the server-mediated `transfer.sendUsdc` → Bridge offramp custodial path with an on-chain broadcast layer (Privy embedded EVM wallet + Particle UA Solana smart-account). Cross-chain via Particle UA. Audit-row content is now verified on-chain (recipient + amount + token + sender ownership) before flipping to `completed`. Admin can tune per-rail spread + flat fee from `/admin/convert-spreads` without a code deploy. Behind feature flag `NEXT_PUBLIC_WALLET_SIDE_SEND_ENABLED` (default `false`). Cross-chain SEND is additionally gated behind `NEXT_PUBLIC_WALLET_SIDE_CROSSCHAIN_ENABLED` (default `false`) until the destination-hash discovery worker (V6-R28) ships in a follow-up. Same-chain SPL/ERC-20 + native ETH/SOL/MATIC sends work today.

Added

  • **Wallet-side Send Crypto flow** at `/dashboard/send` (Crypto tab, behind feature flag). New `SendCryptoForm` component with state-machine UX: pick asset → paste recipient → enter amount → preview → sigil → success → save-as-recipient prompt. Tab-close recovery via `use-pending-send-resume`.
  • **`lib/wallet/*` broadcast layer** — pure-function helpers for Solana SPL (TransferChecked + ATA bootstrap with ATA-vs-owner pre-flight), Solana native (SystemProgram.transfer), EVM ERC-20 (encodeErc20Transfer + tokenAddress validation), EVM native (empty-data tx with value), and cross-chain (Particle UA `createTransferTransaction` → personal_sign → sendTransaction). 60s `withTimeout` on every broadcast guards Privy hang scenarios.
  • **`send-router.ts`** — 5-way dispatch by `(sourceChain == destChain) × isNative` plus a fallback ladder (Particle UA → Relay stub → same-chain). Cross-chain destination contract is resolved via `resolveDestinationToken({ symbol, destinationChain })` so USDC on Base maps to the right Ethereum/Arbitrum/Polygon contract address (not the source contract).
  • **Tx detail page** at `/dashboard/transactions/[txHash]`. Auto-poll cadence 5s/15s/60s/stop, Page Visibility-aware backoff, `aria-live` status badge. Renders source + destination explorer links for cross-chain rows.
  • **Admin Convert Spreads** at `/admin/convert-spreads`. Adds, edits, deletes per-rail entries. Validates against the canonical `RailCode` union. Every mutation writes an `admin_audit_event` row.
  • **`Quote.targetToSourceRate`** field on Convert quotes. Optional in v0.6.0.0 with one-release back-compat (`quote.targetToSourceRate ?? 1/effectiveRate`); will be required in v0.7.0.0. Fee-aware bidirectional back-compute on the Convert page now stays accurate when admin spreads are non-zero.
  • **Pre-broadcast sanctions screen** via `transfer.preflightSendScreen`. Rate-limited 30/min/IP. Defense-in-depth: `recordCryptoSend` re-screens with outage-tolerant posture (Chainalysis fail-closed errors persist the row with `metadata.sanctionsScreenStatus='outage_at_record'` so on-chain success isn't orphaned).
  • **`crypto-send-confirm` Inngest poller.** Cron every 2 min, concurrency 1. Content-verifying confirmation: ERC-20 `Transfer(from,to,amount)` log match (sender ownership too), SystemProgram.transfer pre/post lamport delta, SPL pre/post token-balance delta with sender + destination owner check. 24h aged-out → `failed` with `failureReason='no_receipt_after_24h'`. Cross-chain rows awaiting destination-hash discovery skip the hot probe (no queue starvation) and have a separate ageout sweep with `failureReason='no_destination_after_24h'`.
  • **Save-as-recipient inline prompt** on Send success. Persists to the unified `beneficiaries` table via `kind='crypto_recipient'` (new enum value). Dedupes on `(user_id, address, chain)` via partial unique index.
  • **`token-registry.ts`** — well-known token metadata (USDC across 5 chains, USDT, native ETH/SOL/MATIC) with `isNative` routing flag. `resolveDestinationToken({ symbol, destinationChain })` for cross-chain dispatch. Closes the "USDC on Base = USDC on Ethereum" assumption hole.
  • **`address.ts`** + **`chain-normalize.ts`** — `detectAddressChain` (Solana base58 vs EVM 0x-hex) + canonical chain alias mapping (`solana:mainnet → solana`, `eth-mainnet → ethereum`, etc).
  • **3 migrations:** `0073_convert_rail_spreads` (sparse table; missing rail = free), `0074_beneficiaries_crypto_recipient` (ALTER TYPE ADD VALUE) + `0074a_beneficiaries_crypto_recipient_columns` (column adds + partial unique index, separated to honor Postgres's "new enum value can't be used until commit" rule), `0075_transactions_self_custody_indexes` (partial indexes for poller hot path + tx detail).
  • **Cross-chain fallback hook** — `attemptCrossChainQuoteWithFallbacks` walks Particle UA → Relay (stub) → throw `CrossChainUnavailableError` so the form can degrade to "use a same-chain address" rather than silent failure.

Changed

  • **`transfer.sendUsdc` is now `@deprecated`.** Mobile + Telegram bot still call it during the migration. Removal trigger: BOTH Mobile Parity AND Recurring/Scheduled wallet-side send have shipped.
  • **Admin spread is applied as platform-fee bps in `computeFee`**, NOT as a rate haircut. Earlier draft applied bps in BOTH places (rate × fee), silently double-charging users (50 bps admin → ~100 bps observed on $100). All 4 buildQuote call sites (`quoteConvert`, `getPublicQuote`, `createQuoteSnapshot`, `publicCreateQuoteSnapshot`) now go through the shared `loadAdminSpread(db, intent)` helper so public/share quotes match executable-quote economics.
  • **Convert page back-compute** now prefers `quote.targetToSourceRate` with `1/effectiveRate` fallback for older clients. Stays accurate when admin spreads are non-zero.
  • **Sanctions screen chain family** is now derived from the resolved DESTINATION chain (not source). Solana → EVM previously screened the EVM recipient as `solana`, either false-blocking valid transfers or clearing the wrong namespace.

Fixed

  • **Cross-chain UUID-as-txHash fallback removed.** Particle UA's `transactionId` (internal UUID) is no longer silently stored as `metadata.txHash` when no on-chain hash is returned. Throws a retry-recoverable error instead. UUID is now persisted separately as `metadata.particleTransactionId` for the future destination-hash discovery worker.
  • **Solana ATA-vs-owner validation.** Recipient pre-flight `getAccountInfo` rejects pasted token-account (ATA) addresses with a clear "use the wallet owner address" error before broadcast. Same check Phantom and Solflare run.
  • **EVM ERC-20 broadcast guard.** Empty `tokenAddress` would have deployed a contract with the transfer calldata as bytecode. Pre-flight 0x-hex 40-char check rejects.
  • **Chain narrowing fix.** All EVM destinations (base/arbitrum/polygon/ethereum) are now passed verbatim to `recordCryptoSend.chain` — earlier code collapsed everything to `ethereum` and the poller queried Ethereum mainnet RPC for txes that landed on Base.
  • **Solana → EVM destination chain ambiguity** — hard-blocked until an explicit destination-EVM-chain selector ships (V6-R29). Earlier code resolved EVM-format recipients to `sourceChain`, so a Solana-source send to a `0x...` recipient would tell Particle to bridge Solana → Solana.
  • **Migration 0074 `ALTER TYPE ADD VALUE` + same-script `CREATE INDEX` race.** Split into 0074 (enum value, autocommit) + 0074a (columns + partial unique index, transactional) so Postgres can commit the new value before any DDL references it. `apply-pending-migrations.sh` AUTOCOMMIT branch now passes `-v ON_ERROR_STOP=on` so half-applied migrations fail loudly.

Security

  • **Audit-row content fabrication hole closed.** The poller's content verifiers (`confirmEvmErc20Transfer`, `confirmEvmNativeTransfer`, `confirmSolanaSplTransfer`, `confirmSolanaNativeTransfer`) now require sender ownership match — topic[1] of the Transfer event for ERC-20, `tx.from` for EVM native, source-owner balance decrease for SPL, lamport delta on the sender's account for Solana native. A malicious caller can no longer pass an unrelated tx hash where the recipient happens to match.
  • **Pre-broadcast sanctions screen rate-limit** prevents address-enumeration probing of the Chainalysis cache. (V6-R01 to switch from IP-based to per-userId limiter is filed as a P2 follow-up.)

Deferred (filed as P1/P2/P3 follow-ups in TODOS.md)

  • **V6-R28 (P1):** Cross-chain destination-hash discovery worker. Until this ships, cross-chain SEND is gated behind `NEXT_PUBLIC_WALLET_SIDE_CROSSCHAIN_ENABLED=false`.
  • **V6-R29 (P2):** Solana → EVM destination chain selector UI.
  • **V6-R30 (P2):** Server-authoritative `amountCents` (today client-trusted).
  • **V6-R31 (P2):** End-to-end integration test bundle covering all 6 broadcast paths + sender-ownership rejection + queue-starvation + sanctions outage + admin-spread coverage on all 4 quote endpoints.
  • 24+ smaller hardening items (V6-R01..R27) covering DRY extractions, Solana finalized-commitment for the poller, beneficiary chain alias normalization, `lastProbedAt` queue ordering, etc.

Reviews

  • **CEO Review:** CLEAR (PLAN, 2026-04-28, 13 proposals, 6 accepted, 7 deferred)
  • **Eng Review:** CLEAR (PLAN, 2026-04-28, 6 issues + 1 critical gap accepted)
  • **Codex Plan Review:** caught 8 critical defects the Claude /plan reviews missed (asset scope native paths, chain alias normalization, save-as-recipient schema, tx confirmation content verification, etc) — all fixed in-PR
  • **Design Review:** CLEAR (PLAN, score 6→9, 3 mockup sets approved)
  • **Codex /ship Adversarial:** caught 8 ADDITIONAL critical activation-blockers the Claude /review missed (cross-chain destinationTxHash never populated, native poller verifiers missing, queue starvation, Solana→EVM dest inference broken, sanctions chain wrong, migration 0074 partial-apply, audit-row fabrication via sender ownership, admin-spread only on 1 of 4 quote endpoints) — all fixed in-PR

Test posture

  • 241 test files / 3791 tests pass. Tsc + eslint clean.
  • 113 v0.6.0.0 specs added covering broadcast layer, send-router dispatch, admin spreads, fee-model, address detection, chain normalization.
  • Server-tier integration tests for the poller + tRPC procedures + Particle status worker filed as V6-R31 (P2).

0.5.0.0

Auth fixes plus a feature drop across the consumer/business dashboard. Privy's JWT claim shape change is now safe (every request was 401-ing in production until this fix); business signup auto-creates a default entity so users land on a working dashboard; tokenized-equity invest gates are now permissive (Glide does not gate access at the router level — compliance lives on-chain at the issuer); Send becomes a three-tab Crypto / Fiat / Email flow with a documented self-custody migration path; Add Money becomes a four-currency picker (USD / EUR / GBP / IDR); workers can now set their own tax residence on payroll contracts. Includes a local-dev preview-bypass so designers can review the dashboard without a Privy session, plus a Telegram dashboard tab and an entity-creation flow.

Added

  • **Invest dashboard tab** at `/dashboard/invest` — tokenized equities (Backed Finance xStocks: AAPLx, NVDAx, TSLAx, etc.) and pre-IPO placeholders via Solana SPL + Jupiter. Reuses the existing `consumerInvest` router. KYC no longer required at the router level.
  • **Money request flow** at `/dashboard/request` plus a "Request" tab on `/dashboard/add-money`. Three-tab Add Money: Crypto, Fiat, Request.
  • **Per-currency Add Money picker.** Pay-in currencies surfaced for USD (Noah ACH/Wire), EUR (Noah SEPA), GBP (Bridge FPS), IDR (IDRX). Lazy-loaded deposit details per currency.
  • **Telegram dashboard tab** at `/dashboard/telegram` plus the `TelegramPairingPanel` component. Production username is `@GlidePaybot`.
  • **Entity creation page** at `/dashboard/entities/new`, reachable from the new "+ Create company" sentinel in the sidebar dropdown.
  • **`Email` tab on the Send page** — restored the legacy peer-to-peer flow (`transfer.sendToEmail`) as a third top-level tab alongside Crypto and Fiat.
  • **`transfer.recordCryptoSend` mutation** — audit-only path for the upcoming wallet-side USDC send refactor (Privy `useSendTransaction` for EVM, `useSolanaWallets` for Solana, Particle UA for cross-chain). Re-screens for sanctions, inserts a `transactions` row with `vendor='self_custody'`, no balance hold.
  • **Worker tax-residence self-service.** `contract.updateTaxResidence` lets the worker (not the employer) set their own tax jurisdiction during onboarding. Onboard flow now has a tax-residence step before the payment-rail step.
  • **Bidirectional Convert input.** Type into either the From or To amount field; the other side updates from the live quote.
  • **Local-dev Privy preview-bypass** at `lib/preview-bypass.ts`. Hard-gated to `NODE_ENV=development` + `NEXT_PUBLIC_PREVIEW_BYPASS=1`. Lets designers review the dashboard UI without a real Privy session. Production builds dead-code-eliminate the helper to false.
  • **Mobile (P2):** business view of payroll workers + per-card limit editor (daily/monthly).
  • **`PARTICLE_SERVER_KEY` Vercel env** wired across production / preview / development.

Changed

  • **Auth: Privy `iat` → `issuedAt` claim shape.** `verifyAuthToken` exposes the JWT `iat` under `issuedAt` per the SDK's normalization. Pre-fix, every authenticated request 401-ed because the iat lookup returned `undefined`. Tests at `tests/trpc-iat-revocation.test.ts` updated to mock the correct shape.
  • **Business signup auto-creates a default entity.** `user.setUserType` with `type: 'business_admin'` now inserts a default entity (using a typed `companyName` if provided, else `<email-local>'s company`) so the dashboard renders Business mode immediately rather than an empty shell.
  • **Invest router compliance posture.** `prepareInvestment` / `confirmInvestment` / `prepareRedemption` no longer require KYC. `checkAssetEligibility` no-ops region/accredited gates. Compliance responsibility shifts to the on-chain token's own restrictions (Backed Finance transfer rules, SPL allow lists). `regionRestricted` / `requiresAccredited` metadata preserved on each asset for future reactivation.
  • **Send page rewrite.** Three-tab layout (Crypto / Fiat / Email). Crypto picker shows all five chains (Solana / Ethereum / Base / Arbitrum / Polygon) with non-Solana gated until the wallet-side broadcast lands (P0 TODO). Fiat is beneficiary-driven; rail dispatch chooses the right `transfer.send*` per beneficiary's `preferredRail`. Email is the legacy peer-to-peer flow.
  • **Dashboard nav.** Renamed Dashboard → Home, Yield → Earn, Beneficiaries → Recipients. Added Invest, Telegram, Agents. Personal vs Business is a context toggle inside the entity dropdown rather than a separate app — Personal users see only personal-relevant items in the sidebar.
  • **Convert page swap button.** Now also swaps amounts and `activeSide` so the user's intent ("I want X of this") survives the swap.
  • **KYC error UX.** `/dashboard/kyc` now surfaces vendor errors and missing-redirect-URL cases instead of silently doing nothing on a failed mutation.
  • **Telegram bot username** updated from `@GlideBot` to `@GlidePaybot` across all surfaces (in-app message + docs).
  • **`contract.updateTaxResidence`** validates the country code as a 2- or 3-letter ISO string at the trust boundary (server-side regex), not just the client picker.
  • **Sandbox email logs.** `lib/email.ts` `[email:sandbox]` now logs at `console.warn` so a misconfigured production deploy is obvious.
  • **Worker can create milestones.** `contract.createMilestone` permission expanded — assigned worker can now propose upcoming work; admin can still pre-load. Submission/approval still flows through `updateMilestoneStatus`.

Fixed

  • **Privy JWT claim mismatch (CRITICAL).** Root cause of all-requests-401 in production. Fail-closed iat validation tests preserved.
  • **`user.setUserType` company-name discard.** Operator-precedence bug `A || B ? C : D` was silently throwing away the user's typed company name on every business signup. Wrapped the ternary in parens.
  • **Hydration mismatch in `useActiveEntity`.** `useState(readLocalStorage)` rendered different values on server vs client. Now uses deterministic initial state and syncs in `useEffect`.
  • **Add Money currency-tab race.** Single-valued `loadingCurrency` state could clear while another currency was still loading. Now uses a `Set` of in-flight currencies.
  • **Inactive `kycRequired` prop** on `DepositMethodCard` was dead code in early revisions; documented as future-proofing.

Removed

  • **`payroll/contracts/[id]` and `/new` view tightening.** Worker-vs-admin permissions clarified; no functional removal.

Known follow-ups (P0/P1, see TODOS.md)

  • **[P0] USDC self-custody send via Privy / Particle.** Replaces the server-mediated `transfer.sendUsdc` → `bridge.offramp` path with wallet-side broadcast. Send page Crypto tab currently gates non-Solana chains until this lands.
  • **[P1] Convert page admin-configurable per-rail spread.** Stablecoin swaps stay free; invest and other swaps can be revenue-generating with a per-rail spread/fee table editable from `/admin/convert-spreads`.
  • **[P1] Delete `/dashboard/convert/legacy`** after one sprint of clean production traffic on the new Convert page.

Fixed

  • **Vercel runtime cannot load workspace `main: "./src/index.ts"` packages.** Node 20 ESM loader rejects raw `.ts` at runtime, and `apps/mcp` depends on four such workspace packages (`@glideco/schemas`, `@glideco/grant-wrapper`, `@glideco/policy-engine`, plus four `@repo/*` deps via the policy engine). Lazy-import + `.js` extension fixes (PR #281, #282) got `/healthz` to 200 but `/readyz` and the gateway path still 500'd on the first transitive workspace import. - Fix in PR #283: bundle `apps/mcp/index.ts` with esbuild (ESM, Node 20 target) inlining all workspace deps. `external: ['express', 'jose', 'zod']`. Output is a single 675 KB `index.js`. `vercel.json:buildCommand` invokes the bundler; `package.json:main` points at the bundled artifact so framework auto-detection picks it (not the `.ts` source). - The bundler is workspace-pure — no new toolchain in the runtime path. `apps/mcp/scripts/bundle-vercel.mjs` runs in <30ms locally; Vercel build executes during `installCommand → buildCommand`.
  • **Partner-manifest path bug.** Three manifests (`anthropic/connector-manifest.json`, `openai/plugin-manifest.json`, `google-vertex/agent-tool.json`) referenced `mcp.glide.co/{read,write,treasury}` but the server mounts `/mcp/{read,write,treasury}` (the `/mcp/` prefix is required). Fixed in PR #284. Submissions against the buggy paths would have 404'd at first probe.

Added

  • **`apps/mcp/scripts/bundle-vercel.mjs`** — esbuild bundler entry for the Vercel deploy.
  • **`apps/mcp/index.ts`** — lazy-boot Vercel handler: `/healthz` returns 200 even if `bootFromEnv()` throws; cached errors surface as JSON with stack traces (instead of opaque `FUNCTION_INVOCATION_FAILED`). Boots the gateway on first non-`/healthz` request.
  • **`docs/agents/submission-status.md`** — rewritten with infra-readiness table, smoke-test transcript (2026-04-27 18:55:41 UTC), per-registry checklist with operator/engineer/counsel attribution, and an operator runbook for the two pending Cloudflare CNAMEs.
  • **`live_deploy` block** on each of 4 partner manifests — verification timestamp + endpoint smoke-test results so registry reviewers see green endpoints on first inspection.
  • **TODOS.md** — `[DONE] Deploy apps/mcp Vercel project`; `[P1-OPS] Operator: wire DNS for mcp.glide.co + auth.glide.co`.

Added — Wave 2 finish (Lanes 2E + 2F)

  • **Telegram step-up commands** — `/send`, `/pay`, `/yield`, `/beneficiary-add`, `/confirm`. Threshold matrix: <$50 inline, ≥$50 mints `telegram_pending_actions` row + Telegram inline-keyboard URL pointing to `app.glide.co/telegram/confirm/[token]`. After Privy biometric approves, `/api/telegram/sigil-redeemed` CAS-transitions pending → approved + pings the bot. `/confirm` walks approved-not-yet-executed rows oldest-first.
  • Migration `0063_telegram_pending_actions.sql` (+ schema export + helpers in `apps/web/src/lib/telegram/step-up.ts`).
  • **Telegram LLM hybrid fallback** — freeform messages route to Claude Sonnet 4.6 via direct fetch (mirrors `inngest/generate-sar.ts` pattern). 7 customer-scoped tools (accounts_balance, transactions_list, beneficiary_list, cards_list, card_freeze, card_unfreeze, help). Tool-use loop capped at 4 turns. Per-user 10/hour rate limit via new `telegramAgentPerUser` Upstash key.

Added — Wave 4 finish (Lanes 4B + 4C)

  • **Mobile borrow screen** at `apps/mobile/src/app/(tabs)/borrow.tsx` — wires existing `borrow.ts` router for collateral + positions read; defers position open/close to web (Privy on-chain signing). Registered as hidden tab.
  • **Mobile card-controls modal** — daily/monthly limit rows in `cards.tsx` are now Pressable; modal posts via `consumerCard.updateCardControls`.
  • **Mobile beneficiaries type fix** — Beneficiary type aligned with the list-projection security fix (no accountNumber/iban; detail page uses gated `get` for full PII).
  • **Stripe Issuing connector** — `packages/connectors/stripe-issuing/` (5th `CardAdapter`). Production class wraps Stripe SDK directly; sandbox returns deterministic IDs per Wave 27 pattern. Manifest, credentials schema, icon, COMPLIANCE.md, 4 contract tests. Wired into `createCardAdapter()` in adapters/index.ts; missing STRIPE_SECRET_KEY falls back to Bridge.
  • **Stripe Radar wrapper** — `apps/web/src/lib/stripe/radar.ts` fail-open Radar pre-screen facade for Issuing authorizations.
  • Connector registry now lists 24 manifests (was 23).

Added — Wave 5 (Lanes 5B + 5C)

  • **Drop `cards.bridge_card_id`** — migration `0065_drop_bridge_card_id.sql`. Application code (`account.ts`, `consumer-card.ts`) no longer dual-writes or falls back to the legacy column. Closes the dual-column window opened by Wave 3 Lane 3A.
  • **OSS connector publishing prep** — `packages/connectors/{bridge,column,chainalysis}/package.json` flipped `private:false` with `publishConfig` (public + provenance). New `scripts/publish-glide-connector.mjs` rewrites the workspace `@repo/connectors-*` name to `@glideco/connector-*` for `npm publish`, then restores. Verified via dry-run.

Deferred (intentional, per scope policy)

  • **Lane 3D Stripe FC + Connect tRPC procedures** — billing.ts router additions for `createFinancialConnectionsSession` / `listLinkedBankAccounts` / `unlinkBankAccount` / `createConnectOnboardingLink` / `getConnectAccountStatus` are deferred. Infrastructure (Connect helpers in `lib/stripe/connect.ts`, generate-1099 Inngest, schema columns + linked_bank_accounts table) all shipped in `c092a8a0`. Re-enable by adding the procedures back to billing.ts in a follow-up PR.
  • **Lane 4C Stripe Identity tRPC procedure** — same pattern. The KYC fallback adapter shape can land standalone when the billing router is ready.
  • **Lane 5A Stripe P3 (Climate offset on payroll, Sigma SQL endpoint)** — defers under the same Stripe-tRPC policy. Treasury intentionally skipped per plan ("build only if a customer asked").
  • **Mobile scheduled-payments screen** — no consumer-side recurring-transfer router exists in this codebase; not in audit matrix. Future PR can add when the underlying feature ships.

Drive-by hardening (bundled with feature commits)

  • **Beneficiary router** — `escapeLikeMetachars()`, list-projection (no encrypted blobs), tightened `listForBusiness` tenant predicate (P0 — leak via leftJoin), workerType allowlist (P2), generic NOT_FOUND error.
  • **Tokenized-assets adapter** — default-deny eligibility for anonymous-country users on regionRestricted assets; alpha-3 → alpha-2 country normalization.
  • **consumer-invest** — amount regex tightened to 1–10 digits, real `is_accredited_investor` field threaded through.
  • **4Payments connector** — `NotImplementedError` duck-typed sentinel survives pnpm-hoisted module duplication (instanceof was fragile).

Added — Cross-surface consistency audit (Wave 0)

  • **`scripts/audit-consistency.mjs`** — verifier with embedded MATRIX over (feature × surface) cells. 6 surfaces (web, mobile, oss, cli, mcp, telegram). Modes: warn (default), `--strict` (CI), `--json`, `--markdown`. Verifiers for `screen` (real tRPC call detection vs stub fingerprints), `mcpTool`, `cliCommand`, `ossPackage`, `fileWithPattern`.
  • **`docs/CONSISTENCY_MATRIX.md`** — human-readable scope contracts per surface + status legend + auto-generated table.
  • **`packages/cli/src/commands/consistency.ts`** — `glide consistency` CLI wrapper.
  • **`apps/web/src/server/routers/_template.ts`** — JSDoc tag conventions (`@mcpRequired`, `@mcpBacked`, `@mobileRequired`).
  • **CI gate (Wave 4 Lane 4A)** — `pnpm validate` chains turbo lint + check-types + `--strict` audit. Stub fingerprints relaxed to code-level only (user-visible "Coming soon" copy no longer triggers a STUB).

Added — Beneficiaries unified module (Wave 1 Lane 1A + 1B)

  • Migration **`0058_beneficiary_kind_enum.sql`** — adds `kind` enum (individual / business / employee / contractor / vendor / agent_recipient) + composite indexes.
  • Migration **`0059_worker_type_vendor.sql`** — extends `worker_type` enum with `vendor` so the existing `contracts` table can model vendors.
  • New router `apps/web/src/server/routers/beneficiary.ts` — list/get/create/update/archive/transactionsFor/listForBusiness procedures. Legacy `transferRouter` beneficiary procedures stay for backward-compat with the send-money flow.
  • `transaction.list` gains a `beneficiaryId` filter (existing index reused).
  • Web pages **`/consumer/beneficiaries`** (list + add modal + kind filter chips) and **`/consumer/beneficiaries/[id]`** (detail + transactions scoped to the counterparty + archive).
  • Web page **`/dashboard/beneficiaries`** — unified business view of workers (contracts) + ad-hoc beneficiaries with kind filter + entity picker.
  • Beneficiaries entries added to the consumer SECONDARY_NAV (desktop sidebar) and business `dashboard-shell.tsx` nav.

Added — Mobile customer catch-up (Wave 1 Lanes 1D + 1E)

  • **`apps/mobile/src/app/(tabs)/payments.tsx`** — replaces the prior STUB with a real transaction history fed by `trpc.transaction.list`, with type filter chips + pull-to-refresh.
  • **`apps/mobile/src/app/kyc.tsx`** — new mobile KYC entry; opens the hosted KYC link via `Linking.openURL`.
  • **`apps/mobile/src/app/(tabs)/beneficiaries.tsx`** — mobile beneficiaries screen mirroring the web shape; registered as a hidden tab.

Added — MCP P1 tool backfill (Wave 1 Lane 1C)

  • 3 new MCP tools: **`beneficiary.list`**, **`beneficiary.update`**, **`kyc.start`** — registered in `apps/mcp/src/tool-catalog.ts`. Tool count: 22→25 (9 read / 9 write / 7 treasury).
  • New scopes added to `AgentScope` closed vocabulary: **`beneficiary:read`**, **`kyc:start`**.

Added — Invest tab on Solana via Jupiter (Wave 2 Lane 2H)

  • `apps/web/src/server/adapters/tokenized-assets.ts` extended with **`AssetCategory`** enum (12 categories: tbill, equity-tech/finance/energy/healthcare/consumer, etf-broad/sector, commodity-gold/treasury, pre-ipo, crypto-rwa).
  • Curated registry of **Backed Finance Solana SPL xStocks**: AAPLx, NVDAx, TSLAx, GOOGLx, METAx, COINx, MSTRx, SPYx, TLTx, GLDx — all region-restricted from US persons (Backed/FINMA wrapper).
  • Pre-IPO placeholders (STRIPEx, SPACEXx) marked `requiresAccredited` + region-restricted, shown with "Coming soon — join the waitlist" badge.
  • `consumer-invest` router gains **`listAssetsByCategory`** + **`getCategories`** procedures; `prepareInvestment` enforces server-side eligibility gate (xStocks blocked for US users; pre-IPO requires accredited).
  • `apps/web/src/app/(consumer)/earn/page.tsx` — Invest sub-tab promoted from "Coming in Phase 5" STUB to a live **`InvestPanel`** with category filter chips + asset grid + eligibility-aware blocked badges.

Added — Telegram bot core (Wave 2 Lanes 2A + 2B + 2C + 2D + 2G)

  • Migration **`0060_telegram_bot.sql`** — `users.telegram_chat_id`, `telegram_pairing_codes`, `telegram_bot_grants` tables.
  • **Pairing flow:** `user.generateTelegramPairingCode` / `getTelegramStatus` / `unlinkTelegram` tRPC procedures + settings page at **`/consumer/settings/telegram`**.
  • **Webhook handler:** `apps/web/src/app/api/telegram/route.ts` with HMAC verification on `X-Telegram-Bot-Api-Secret-Token`. Always returns 200 to prevent retry storms.
  • **Bot grant manager:** `apps/web/src/lib/telegram/bot-grant.ts` — HS256 JWTs signed with `TELEGRAM_BOT_TOKEN`. Resolves the consumer-vault risk: bot grants live in `telegram_bot_grants` separate from the agent platform's `agent_principals`, so consumer users without a multisig vault can pair without provisioning one. 24h TTL, auto-refresh at 75% TTL elapsed.
  • **Slash command dispatcher:** `apps/web/src/lib/telegram/dispatcher.ts` + 4 command files (balance, transactions, beneficiaries, cards [freeze/unfreeze/list], help). Alias-matched.
  • **Notification channel:** `notification-prefs.ts` extended with `'telegram'` channel; `notifications.ts` `notify()` extended with `'telegram'` and `'all'` channels. Best-effort inline send via `apps/web/src/lib/telegram/notifications.ts` — falls back gracefully when user hasn't paired or Telegram API errors.
  • Env vars: **`TELEGRAM_BOT_TOKEN`**, **`TELEGRAM_WEBHOOK_SECRET`**, **`TELEGRAM_BOT_USERNAME`**.

Added — 4Payments.io card connector (Wave 3 Lane 3B, stub-first)

  • New package **`packages/connectors/4payments/`** mirroring `gnosis-pay/`. Manifest declares slug `4payments`, capabilities `card`, regions EU/GB. Sandbox adapter uses `generateDeterministicId` (Wave 27 pattern).
  • Production adapter throws **`NotImplementedError`** until partner credentials arrive (per user direction in plan; 4Payments docs are a JS-rendered SPA that can't be inspected without auth). Factory wires the case in `createCardAdapter` switch with Bridge fallback.
  • Env vars: **`FOURPAYMENTS_API_KEY`**, **`FOURPAYMENTS_API_URL`**, **`FOURPAYMENTS_WEBHOOK_SECRET`**.
  • Operators flip back to Bridge globally with `DEFAULT_CARD_VENDOR=bridge`.

Changed — `cards.bridge_card_id` → `cards.vendor_card_id` zero-downtime rename (Wave 3 Lane 3A)

  • Migration **`0061_add_vendor_card_id.sql`** — adds new column, copies values, adds unique + lookup indexes. Legacy `bridge_card_id` column stays for one deploy cycle.
  • All 5 callsites in `apps/web/src/server/routers/account.ts` + `consumer-card.ts` updated to dual-write + read-with-fallback.
  • Wave 5 Lane 5B (deferred to follow-up) drops `bridge_card_id`.

Added — Stripe Billing + Subscriptions (Wave 3 Lane 3C)

  • Migration **`0062_stripe_customer_ids.sql`** — `users.stripe_customer_id`, `businesses.stripe_customer_id`, `user_subscriptions.stripe_subscription_id` + `stripe_price_id`. Uses existing `webhook_events` table for idempotency.
  • Stripe SDK pinned to **`stripe@^17.0.0`** with API version `2025-02-24.acacia`. Singleton client at `apps/web/src/lib/stripe/client.ts`.
  • New router **`apps/web/src/server/routers/billing.ts`** — `getSubscriptionStatus`, `createCheckoutSession`, `getBillingPortalUrl`, `cancelSubscription`. `ensureStripeCustomer` lazily provisions on first billed action.
  • Webhook handler **`apps/web/src/app/api/webhooks/stripe/route.ts`** — verifies signature, idempotency-checks against `webhook_events`, reconciles `customer.subscription.*` + `invoice.payment_succeeded/failed` events back to `user_subscriptions`. Maps Stripe status → Glide enum (active / canceled / past_due).
  • Env vars: **`STRIPE_SECRET_KEY`**, **`STRIPE_PUBLISHABLE_KEY`**, **`STRIPE_WEBHOOK_SECRET`**, **`STRIPE_PRO_PRICE_ID`**, **`STRIPE_BUSINESS_PRICE_ID`**, **`STRIPE_UNLIMITED_PRICE_ID`**.

Added — CLI gap fillers (Wave 4 Lane 4D)

  • **`glide deploy`** — detects Vercel / docker-compose target, prints the canonical deploy command (operator runs with their own creds).
  • **`glide secrets sync`** — detects `SECRETS_BACKEND`, prints the canonical secrets-pull command (env-file / doppler / aws-sm / vault).
  • **`glide policy lint <path>`** — validates an `AgentPolicyEnvelope` JSON file against `@glideco/schemas` + sanity checks (cap reasonableness, allowlist non-empty).
  • **`pnpm validate`** at workspace root — runs turbo lint + check-types + audit `--strict`. CI invokes this.

Added — Stripe Connect + 1099-NEC infra (Lane 3D, library-only)

  • Migration **`0064_stripe_connect_fc_extras.sql`** — adds `stripe_connect_account_id` to `users` + `businesses` plus FC linkage table.
  • **`apps/web/src/lib/stripe/connect.ts`** — Express Connect helpers (account creation, onboarding link, dashboard link).
  • **`apps/web/src/inngest/functions/generate-1099.ts`** — annual cron (Jan 31) + ad-hoc trigger that walks active businesses, computes per-contractor totals, and generates 1099-NEC drafts via Stripe Tax.
  • Library-only — UI consumers ship in a follow-up. Operators can wire `ensureUserConnectAccount` from custom flows now.

Added — Lane 4B mobile catch-up + Lane 4C stripe-issuing connector

  • **`apps/mobile/src/app/(tabs)/borrow.tsx`** — mobile borrow screen wires `borrow.ts` router (already complete on web).
  • **`apps/mobile/src/app/(tabs)/cards.tsx`** — adds card-controls modal (limits, MCC locks).
  • **`packages/connectors/stripe-issuing/`** — fifth `CardAdapter` implementation (after Bridge, Gnosis Pay, Wirex, 4Payments). Slots into `createCardAdapter()` factory; sandbox + production wiring. Includes COMPLIANCE.md vendor posture.
  • **`apps/web/src/lib/stripe/radar.ts`** — Stripe Radar fraud-rule wrapper for card transactions.

Fixed — Adversarial-review batch (10-agent Opus pass over the full Plan D diff)

  • **Beneficiaries** — `escapeLikeMetachars()` + `LIST_PROJECTION` (excludes encrypted blobs from list responses), `listForBusiness` joins `entity_members` and requires admin OR membership (authz bypass fix), `workerType` allowlist guard on the contracts→beneficiaries join.
  • **Invest compliance gate** — ALPHA3→ALPHA2 country mapping with default-deny on unknown countries when `regionRestricted` is set; placeholder pre-IPO assets (`contractAddress === 'TBD'`) blocked at `prepareInvestment`. consumer-invest hardening: numeric amount regex, 40–128 char txHash bound, per-chain wallet selection, idempotency via `metadata->>'txHash'`, currency=USD (was asset.symbol — fixes cross-currency contamination).
  • **Stripe** — `billing.ts` open-redirect protection on success/cancel URLs, idempotency on customer creation, existing-active-sub guard, generic error messages. Webhook: insert idempotency row FIRST inside transaction, trim payload to `{id,type,created}`, fail-closed `mapStripeStatus` for unknown values, tier inference from price_id only, `metadata.glide_user_id` cross-check, 200-ack on missing webhook secret.
  • **Telegram** — pairing code via `randomInt` (no Math.random bias), invalidate+insert in one transaction. bot-grant: `safeStringEqual` via `timingSafeEqual`, alg+typ header guard, strict exp check, iat sanity bounds. Webhook: timing-safe secret compare, reject non-private chats, drop oversize text before DB call, single transactional `UPDATE...RETURNING` for pairing redemption (TOCTOU fix), code-length+alphabet guard before DB call. Dispatcher: 32-char command-name cap.
  • **4Payments + card column** — `NotImplementedError` moved to top of `legacy.ts` (TDZ-safe), static `is()` duck-type for cross-module identity, boot-time guard when API key set, nonce-based sandbox IDs (collision fix), optional `idempotencyKey` for fixtures. Adapter factory: try/catch fallback to Bridge when 4Payments production stub throws. `account.freeze`/`unfreeze` + `consumer-card.provisionApplePay`/`provisionGooglePay`: refuse with `PRECONDITION_FAILED` rather than handing client `cardId: null` or sending Bridge a UUID. `schema.ts` partial-unique index on `cards_vendor_card_id_unique` (matches migration drift).
  • **MCP tenant scoping** — `beneficiary.update` and `kyc.start` add `fetchOwner` deps method + handler-level tenant check (defence-in-depth above the deps).
  • **Audit script** — `replaceAll` for tool-name dot→dash conversion (fixes `agent.grant.issue` lookup), terminal column width 8→10, enforce waiver `until:` dates (past-due waivers fall through as STUB), flip policy-lint cell from waived → live `cliCommand`.
  • **policy-lint** — read snake_case schema keys (`amount_cap_cents_per_tx`, `counterparty_allowlist`), path-traversal containment check (refuse paths that resolve outside `ctx.cwd`).

Excluded from this ship (deferred to follow-up)

  • **Telegram step-up commands (Lane 2E)** — sigil mint + confirm flow for `/send` above $50. Read-only commands work; write commands deferred.
  • **Telegram LLM hybrid fallback (Lane 2F)** — Claude-via-Anthropic-SDK natural-language dispatcher. Slash commands work; freeform messages reply "use /help".
  • **Stripe FC + Connect + Tax UI (Lane 3D UI)** — library helpers ship now (see above); the `/consumer/settings/banks` page + `payroll.generate1099` mutation surface wire up in a follow-up PR.
  • **Stripe Identity (Lane 4C)** — Issuing + Radar landed; Identity (KYC fallback) deferred.
  • **Mobile scheduled-payments (Lane 4B)** — borrow + card-controls landed; scheduled-payments screen deferred.
  • **Stripe P3 (Climate / Sigma / Treasury) (Lane 5A)** — nice-to-haves.
  • **Drop `bridge_card_id` column (Lane 5B)** — must wait for one full deploy cycle after Lane 3A.
  • **OSS connector publishing (bridge / column / chainalysis) (Lane 5C)** — flips `private: false` on three existing connectors. Ships separately to avoid coupling npm publishes to other lanes.

Added — 14 new packages

  • **`@glideco/mpp-adapter`** — Machine Payments Protocol charge intent encoder. RFC 7235 `WWW-Authenticate: Payment` over Tempo + Solana. CRLF-injection defense in quoted-string serialization (RFC 7230 §3.2.6).
  • **`@glideco/ap2-adapter`** — Google's Agent Payments Protocol VDC encoder. Maps Glide's 13-axis `agent_grants` to AP2 IntentMandate / CartMandate / PaymentMandate. Pinned to AP2 v0.1-alpha context URL `agent-payments.googleapis.com/2025/v1/credentials.jsonld`.
  • **`@glideco/acp-merchant`** — Agentic Commerce Protocol merchant adapter (OpenAI + Stripe, ACP v2026-04-17.1). Two REST endpoints (`/agentic_checkout`, `/delegate_payment`). Atomic claim-or-replay idempotency, header-preferred `Idempotency-Key`, header/body conflict rejection (409).
  • **`@glideco/ucp-profile`** — Universal Commerce Protocol capability profile builder (UCP v0.1-2026-04). Served at `/.well-known/ucp.json`.
  • **`@glideco/x402-facilitator`** — x402 v2 facilitator with Chainalysis sanctions-screening bake-in (verify + settle phases). Composite idempotency cache key (key + payTo + network + payload hash). Body-cap defense (4 MB).
  • **`@glideco/kya-vc`** — `AgentSanctionsPassCredential` W3C VC issuer. Wraps Chainalysis screening results as Ed25519/ECDSA-signed credentials. Cryptosuite-aware proof type (`Ed25519Signature2020` / `EcdsaSecp256r1Signature2019`) with multibase base58btc proofValue.
  • **`@glideco/kms-signer`** — KMS-backed signer abstraction (AWS KMS / GCP KMS / Vault Transit / env-key). ECDSA emits IEEE P-1363 raw r||s (not DER) for JOSE/W3C VC compatibility.
  • **`@glideco/agent-identity`** — `did:key` derivation from Apple App Attest P-256 keys + Android Key Attestation. SEC1-compressed multicodec encoding.
  • **`@glideco/smart-router`** — `glide.pay()` cheapest-first router with internal-vault short-circuit (free same-tenant transfers), latency p95 tiebreaker, max 6 failover attempts. App-level non-negative fee guard.
  • **`@glideco/spend-export`** — Receipt schema (costillery-compatible) + CSV / JSONL / QBO IIF / Xero JSON exporters. ISO 4217 uppercase + 4-digit MCC enforced. realizedGain bounded ±$100B per receipt.
  • **`@glideco/parley-tiers`** — Negotiated tier classification (turbo/fast/batch).
  • **`@glideco/crosschain-bridge`** — Squid + Across quote shape layer.
  • **`@glideco/a2a-resolver`** — Agent-to-agent counterparty resolver. Adds `counterparty_kind: agent_did` to beneficiary table. ERC-8004 Identity Registry + ENS resolution. **DNS-rebinding-safe `safeFetchDidWeb`** — undici dispatcher with `connect.lookup` pinned to pre-resolved IP, ipaddr.js-backed RFC 6890 + cloud-metadata-host rejection. Hostname max length cap.
  • **`@glideco/ows-react-native`** — Open Wallet Standard provider for React Native. Spec-aligned to Wallet Standard (CAIP-2 chains, `WalletAccount[]` with publicKey: Uint8Array, namespaced features, both register-wallet + app-ready event paths).

Added — 3 new public routes

  • `/.well-known/ucp.json` — UCP capability profile (force-dynamic; env-var changes propagate immediately).
  • `/api/acp/agentic_checkout` — ACP buyer-agent checkout endpoint. Stream-bounded JSON body cap (1 MB) defends against chunked-transfer-encoding bypass.
  • `/api/acp/delegate_payment` — ACP payment confirmation endpoint. Same body-cap defense.
  • `/.well-known/{agent,mcp,x402}.json` switched from `force-static` to `force-dynamic` so env mutations propagate without ISR delay.

Added — 7 new Postgres migrations

  • `0060_pay_attempts_saga_reaper.sql` — partial index for stuck-claim reclamation.
  • `0061_concurrent_index_rebuild.sql` — rebuilds 5 indexes CONCURRENTLY (no ACCESS EXCLUSIVE on hot tables); DROP INDEX CONCURRENTLY closes uniqueness window during swap.
  • `0062_pay_attempts_entity_id_safety.sql` — quarantine-then-delete (new `pay_attempts_quarantine` table with forensic preservation; `RAISE NOTICE` row count) instead of silent destructive DELETE.
  • `0063_pay_attempts_constraints.sql` — CHECK constraints (quoted_fee_cents ≥ 0, attempt_index 0..15, latency_p95 ≥ 0) + FK CASCADE → RESTRICT. All use `NOT VALID + VALIDATE CONSTRAINT` pattern (decouples new-write enforcement from existing-row validation).
  • `0064_mcp_audit_events_rail_check.sql` — closed-enum CHECK on rail vocabulary, NOT VALID + VALIDATE.
  • `0065_agent_pending_payments_settled_at.sql` — settled_at column + CONCURRENTLY index for TTS metrics, backfill from broadcasted_at.
  • `0066_pay_attempts_state_invariants.sql` — 4 status↔timestamp coupling CHECKs (status='claimed' → claimed_at NOT NULL, etc.). NOT VALID + VALIDATE.

Added — `@glideco/schemas` utilities

  • `uuidV7()` / `uuidV7Timestamp()` — RFC 9562 §5.7 sortable UUIDs for B-tree-friendly PKs on hot-write tables.
  • `canonicalJson()` — shared JCS canonicalizer used by kya-vc + mpp-adapter + x402-facilitator. Refuses NaN / Infinity / undefined / Date instances. Skips undefined-valued keys per JSON.stringify default.
  • `chainIdToCaip2()` / `caip2ToChainId()` / `caip2ChainIdSchema` — bidirectional CAIP-2 boundary translators (`eip155:1` ↔ `eth`, etc.).

Changed — schema strictness (potentially breaking)

  • ACP `acpMoneyPositiveSchema` mandatory for `unitPrice` / `subtotal` / `total` / `shipping` / `tax`. Discounts keep signed `acpMoneySchema` (spec allows negative discount amounts). Clients sending signed amounts on positive-only fields now see `400 invalid_request`.
  • ACP `acpStructuredIdSchema` enforces URL-safe ASCII on cart + line-item ids (`[a-zA-Z0-9._:\-]{1,128}`).
  • All ACP / AP2 / UCP / SPT / a2a `currency` fields enforce ISO 4217 uppercase via regex (was `length(3)` previously which accepted lowercase).
  • All MCC fields enforce `^\d{4}$` ISO 18245 across spend-export + agent-policy-envelope.
  • UCP `ucpProtocolSchema` dropped `paylink` (not in spec — strict UCP validators reject closed-enum violations).
  • Lightning `BOLT11_PREFIX_RE` adds BIP-325 signet (`lntbs` prefix); alternation order fixes silent fall-through to testnet.
  • ACP_SPEC_VERSION bumped from `2026-04-17` to `2026-04-17.1` so SDK consumers detect the strict-validation variant via the `X-Acp-Version` response header.
  • `ACP_IDEMPOTENCY_HEADER = 'Idempotency-Key'` constant. Routes prefer header over body; reject with `409 idempotency_conflict` when both present and disagree.
  • x402 facilitator routes drop the `__glide_status` extension field from 501 bodies (broke strict client validators); add `X-X402-Version` header to every response (symmetric with ACP).

Fixed — security + correctness (from 2 rounds of review)

  • **DNS-rebinding closed**: `safeFetchDidWeb` now actually pins undici dispatcher to the pre-resolved IP. Previous version constructed an `https.Agent` but never wired it; node:fetch performed its own DNS lookup, which a rebinding resolver could swap mid-flight.
  • **Body-cap DoS closed**: 4 public POST routes (ACP × 2, x402 × 2) replaced header-only Content-Length checks with stream-bounded JSON readers. Defends against chunked-transfer-encoding bypass and lying Content-Length headers.
  • **Saga reaper metric loss fixed**: drizzle's `db.execute(sql\`...RETURNING\`)`returns`{rows, rowCount}`not an Array; the previous`Array.isArray(result) ? result.length : 0`always reported`reclaimed: 0` even when the UPDATE moved rows. Operator dashboards thought the reaper was idle when it was working.
  • **Saga reaper unbounded text growth fixed**: `failed_reason` capped at 1024 chars via `RIGHT(...)` truncation. Previous unbounded concat opened a tenant-scoped disk-fill DoS for any worker that flapped mid-claim.
  • **Decimal overflow fixed**: `decimalStringToCents` caps integer length at 14 digits. Previous version accepted 17+ digit integers that overflowed `Number.MAX_SAFE_INTEGER` after `* 100`, producing imprecise floats that silently agreed across cart math.
  • **Drizzle schema drift fixed**: `pay_attempts` (FK + smallint + 4 partial WHERE indexes + DESC ordering), `agent_pending_payments` (settledAt column + index), `mcp_audit_events` (rail partial index). `drizzle-kit generate` no longer proposes destructive ALTERs.
  • **Migration column-name consistency**: 0057 fixed to use `occurred_at` (matching siblings from 0051) instead of `created_at`. Late-arriving Inngest retries no longer reorder rows in operator dashboards.
  • **FK swap lock-acquisition fixed**: 0063's FK CASCADE → RESTRICT swap now uses `NOT VALID + VALIDATE CONSTRAINT` (was synchronous validate, taking SHARE ROW EXCLUSIVE on both pay_attempts AND agent_pending_payments for the whole transaction).
  • **CHECK constraint deploy hazard fixed**: 0063, 0064, 0066 use `NOT VALID + VALIDATE` pattern with pre-flight `RAISE NOTICE` row counts. A single drifted row no longer blocks deploy; ops can investigate violations separately from new-write enforcement.
  • **canonical-json undefined fixed**: `canonicalJson` refuses top-level undefined (previously emitted literal `"undefined"` — invalid JSON) and skips undefined-valued keys in nested objects (matches JSON.stringify default + JCS RFC 8785 §3.2.3). Closes a signature-malleability surface.
  • **MPP CRLF injection closed**: `serializeWwwAuthenticatePayment` rejects CR/LF in any param (RFC 7230 §3.2.6 HTTP response-splitting defense).
  • **KYA proof correctness fixed**: `proofValue` is now actual base58btc-encoded raw signature bytes (was `u`-prefixed base64url, which strict W3C VC verifiers reject for `Ed25519Signature2020`).
  • **KMS ECDSA correctness fixed**: env-key signer adds `dsaEncoding: 'ieee-p1363'` to ECDSA sign calls. node:crypto defaults to ASN.1 DER for ECDSA, which produces "signature length mismatch" failures in every JOSE/JWS/digitalbazaar verifier.
  • **App-level fee guard**: smart-router rejects negative / non-finite / non-integer `quoted_fee_cents` at sort time (defense-in-depth beyond schema parse).
  • **OWS Wallet Standard alignment**: provider rewritten to canonical Wallet Standard surface. CAIP-2 chains, Uint8Array publicKey, namespaced features, both event paths. Chains derived from schema enum (DRY).
  • **Lightning msat overflow guards**: `assertMsatsInRange` capped at total Bitcoin supply; called on input AND output of `centsToMsats` / `msatsToCents` to defend against price-oracle bugs and sign-flipped bigints.
  • **Tempo placeholder validator**: `assertTempoUsdcConfigured()` throws if the connector is asked to use a placeholder USDC contract address before operator sets the canonical Circle xReserve deployment via env.
  • **DID URI hostname cap**: `isValidAgentDid` rejects DIDs longer than 512 chars (parser-DoS defense; covers RFC 1035 max + headroom).
  • **MCP server entity scoping fixed**: `cards.get_details` checks both `vault_id` AND `entity_id` against the verified tenant (was vault_id only — cross-tenant leak with shared vault).
  • **CLI atomic-write randomness fixed**: installer uses `crypto.randomUUID()` + `flag: 'wx'` (O_EXCL) instead of `pid-Date.now()` (race-vulnerable on shared FS).
  • **MCP preview env carve-out removed**: `NODE_ENV=production` always requires real Hydra-backed JWT verifier (no `VERCEL_ENV !== 'preview'` escape hatch).

Tests

  • **+19 property tests** — KYA proofValue base58btc round-trip (100 random + edge cases), CAIP-2 ↔ ChainId bijection (forward + reverse total + injective + closed-set), AP2 cart total invariant (50 random carts + uniform-currency).
  • **+17 SSRF tests** — RFC 6890 IPv4 ranges (loopback, RFC 1918, link-local, CGNAT, multicast, 0.0.0.0/8, RFC 5737 docs), IPv6 (::1, fc00::/7, fe80::/10, ff00::/8), explicit cloud-metadata-host blocklist (Alibaba, Azure WireServer, Oracle Cloud), unparseable input.
  • **+9 UUIDv7 tests** — version nibble, variant nibble, timestamp roundtrip, lexicographic order, 48-bit range, NaN/Infinity rejection.
  • **+7 CAIP-2 round-trip tests**.

Test status (local CI)

  • 304/304 MCP vitest cases
  • 3473/3475 web vitest cases (2 todo, 0 fails)
  • All 14 new packages: tests + typecheck pass
  • `pnpm turbo lint` 29/29 clean
  • `pnpm turbo check-types` 65/65 clean
  • `pnpm audit --audit-level=critical` 0 critical (40 high / 29 moderate pre-existing)
  • `scripts/scan-secrets.mjs` clean

Added — 10 packages published to npm under @glideco

  • **`@glideco/agent-events`** — closed-vocab event taxonomy for activity_log narratives (16 event types, discriminated Zod union).
  • **`@glideco/anomaly`** — heuristic risk detector + storm suppression + Sentry sink.
  • **`@glideco/compliance-export@0.2.0`** — JSON sync + PDF async pipeline + S3 retention adapters (STANDARD / GLACIER_IR / DEEP_ARCHIVE) for the M4 hot/warm/cold/regulatory tier transition.
  • **`@glideco/dsar`** — GDPR/CCPA redaction primitives. 8 v1 redactable fields, bitmap encoder, append-only-trigger session-var contract.
  • **`@glideco/explainer@0.2.0`** — LLM narrator skeleton with Wilson 95% UB eval harness + 10-case synthetic golden set. Ships feature-flagged off (operator brings their own dataset to flip the flag — Glide does not gate on this).
  • **`@glideco/recovery`** — pure encoders for social recovery: EVM Zodiac Delay v1.0.1 (deploy/queue/execute/cancel + on-chain queue reader) + Solana Squads v4 (config-tx-create + proposal-create + proposal-approve + cancel + execute). 112-case zodiac-delay test suite ships with the package.
  • **`@glideco/grant-wrapper@0.x`** — fresh-read tenant verification (money-safety F3 gate).
  • **`@glideco/policy-engine@0.x`** — pure `evaluate()` over 13 policy axes per agent grant.
  • **`@glideco/schemas@0.x`** — Zod schemas for AgentPolicyEnvelope, Grant, Receipt, AgentScope, ConnectorManifest, etc.
  • **`@glideco/secrets-scan@0.x`** — denylist-first scanner (JWT, AWS keys, Anthropic, Stripe, Ethereum/Solana private keys).

Added — code surface

  • **`packages/connectors/coinbase-x402/`** — x402 Payment Protocol adapter wrapping the Coinbase facilitator. `CoinbaseFacilitator` client, `handleX402Request` receiver flow, `X-PAYMENT` codec, F1 IRON RULE preserved (operator independently RPC-verifies before persisting).
  • **`apps/web/src/app/api/x402/[accountId]/route.ts`** — public x402 receive endpoint. Every Glide account is x402-addressable out of the box. DB-backed receive-address routing via `bank_accounts.x402_receive_address` + `x402_default_amount_cents` (migration 0056).
  • **`apps/mcp/api/index.ts`** + **`apps/mcp/vercel.json`** — Vercel-deployable serverless entry for the MCP gateway. Lazy-loaded `bootFromEnv()` returns the Express app per cold start.
  • **`apps/mcp/src/server.ts:buildJwksTokenVerifier`** — JWKS-backed JWT verifier using `jose.createRemoteJWKSet` + `jose.jwtVerify`. `bootFromEnv()` auto-picks JWKS verifier when `MCP_JWKS_URL` is set; falls back to dev-secret HMAC. **Production cutover is one env-flip, no code change required.**
  • **`apps/mcp/scripts/generate-api-reference.ts`** — regenerable Mintlify-flavored MDX page per MCP tool (22 pages + index). Auto-published to `glide.co/docs/agents/api/*`.
  • **`docker-compose.hydra.yml`** + **`scripts/hydra-bootstrap.sh`** — self-host fallback for the OAuth Authorization Server. Brings up Ory Hydra v2.2.0 + Postgres + reference consent UI; bootstraps two OAuth clients (confidential + public PKCE).
  • **`apps/web/e2e/`** — Playwright admin E2E suite scaffolds for the 5 M4 exit-criteria flows (connector toggle, agent-tier-change activity feed, DSAR redact, compliance PDF poll, share create/revoke). `page.route()` tRPC mock harness; runs without a database. Opt-in via `RUN_E2E=1`. Auth bypass via `NEXT_PUBLIC_E2E_BYPASS_AUTH=1` (development-gated).
  • **`.github/workflows/tthw-canary.yml`** — Mondays 14:00 UTC, measures `npx create-glide-app` → `pnpm glide demo` → `/health` 200 within 90 seconds.

Added — public schemas (promoted /draft/ → /v1/)

  • All eight agent-banking schemas at [`glide.co/schemas/agent-banking/v1/`](https://glide.co/schemas/agent-banking/v1/index.json) plus the new `_types.json` shared `$defs` file (28 types).
  • Promotion done **without** waiting for 3+ implementer verification. De-risked by an independent Opus adversarial review across 10 axes × 8 schemas (no individual cell <9, aggregate 93.25%).
  • Tightened types: geo allowlist `^[A-Z]{2}$`, MCC `^[0-9]{4}$`, velocity multiplier upper bound 1000, action regex `^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$`.
  • Added optional RFC 7519 `iss` claim and RFC 8707 `resource` array to grants.
  • `skillScope` subset on skill-manifest **EXCLUDES** admin scopes (`agent:budget:create`, `agent:budget:revoke`, `treasury:rotate-signer`).
  • 56 hand-rolled hostile-input tests + 5 backward-compat tests.
  • `/draft/*` retained as deprecated alias mirroring `/v1/*` exactly.

Added — legal artifacts (avg 98.93/100)

  • **`docs/TRUSTED_PARTNER_AGREEMENT.md`** (Form v1.1, 96.67/100) — TPA template for Verified-tier connector promotion. **Use-based consent — no countersign required.** Acceptance via §0: requesting promotion, maintaining Verified status ≥30 days, or using the Verified Mark publicly.
  • **`docs/TRUSTED_SKILL_AGREEMENT.md`** (Form v1.1, 100.00/100) — TSA template for Verified-tier skill promotion. Same use-based consent mechanic as TPA. Lighter than TPA per OSS plan §M5; heart of the agreement is prompt-injection review attestation.
  • **`packages/connectors/chainalysis/{DISCLAIMER,COMPLIANCE}.md`** (99.60/100) — full operator-facing legal notice + compliance brief. OFAC / EU / UK / UN / BSA / FinCEN / NYDFS / MiCA / FCA / AMLD / GDPR / CCPA / LGPD / SCCs / DPF cited.
  • **`packages/connectors/coinbase-x402/{DISCLAIMER,COMPLIANCE}.md`** (99.20/100) — F1 IRON RULE upgraded from one-line note to numbered 5-step RPC-verify procedure. FinCEN MSB / state MTL / MiCAR / PSD2-3 enumerated.
  • **`docs/legal/ory-vendor-posture-{DISCLAIMER,COMPLIANCE}.md`** (99.17/100) — Ory Network + self-hosted Hydra both first-class. GDPR ≤72h breach, CCPA, key-rotation cadence.
  • All artifacts reviewed by independent Opus Legal Partner agents against a 12-axis framework. Banner: "Status: Ready for counsel review · Not yet counsel-reviewed" (the Opus pass IS the OSS-grade counsel review per user decision).

Added — Mintlify docs at glide.co/docs

  • New `oss/legal/` section with index + 5 artifact pages.
  • `oss/standards/` section updated to announce `/v1/` promotion + adversarial-review provenance.
  • New `agents/api/` section with auto-generated MCP tool reference (22 pages).

Production wire-up

  • **Ory Network OAuth AS:** project `cac3e36d-…` (`determined-carver-nvxrhs24tf`). Two OAuth clients registered with `access_token_strategy: jwt`. End-to-end smoke verified (`client_credentials → RS256 JWT → JWKS verify` returns `verified: true`).
  • **Vercel project glide-mcp linked** with 9 production env vars (UPSTASH*\*, ANTHROPIC_API_KEY, DATABASE_URL, MCP_JWKS_URL, MCP_ISS_URL, MCP_AUDIENCE, ORY*\*). Deploy itself blocked on Vercel free-tier 100/day quota; cron retries scheduled.

Database migrations

  • `0056_bank_accounts_x402.sql` — adds `x402_receive_address` + `x402_default_amount_cents` columns + partial index on enabled rows.

Removed from the ship-blocker punch list

  • ~~Counsel reads 5 legal artifacts~~ — done. Opus Legal Partner pass at avg 98.93/100 IS the OSS counsel review.
  • ~~Explainer n≥500 labeled eval~~ — skipped by user decision; LLM-narrator stays off (eval harness ships as scaffolding for any operator who wants to flip the flag later).
  • ~~3+ implementer verification of /draft/ schemas~~ — promoted /v1/ via adversarial review.
  • ~~TPA + TSA wet-signed countersign by every Verified partner~~ — converted to use-based consent (Form v1.1).

[M5.5] - 2026-04-25 — Public standards + docs sites + glide partner submit

  • `@glideco/schemas/scripts/build-json-schemas.mjs` — Zod → JSON Schema export pipeline. Emits 8 documents under `dist/schemas/draft/*.json` for upload to `schemas.glide.dev/agent-banking/draft/`: `connector-manifest`, `agent-policy-envelope`, `agent-activity-event` (placeholder until M4), `trust-tier`, `scoped-grant-claims`, `skill-manifest`, `grant`, `receipt`.
  • `glide partner submit <path> --type=connector|skill` CLI command. Validates required-file checklist + prints partner-PR submission steps. 8 unit tests.
  • `scripts/generate-catalog-docs.mjs` — auto-generates `docs/CONNECTORS.md` (21 connectors) + `docs/SKILLS.md` (6 hero skills) from each package's manifest. `--check` mode flags drift in CI.
  • `docs/LICENSE_COMPATIBILITY.md` — accept/warn/block matrix (MIT / Apache / BSD / ISC pass; MPL / LGPL / AGPL warn; GPL / SSPL / unlicensed block).
  • `docs/hosted-vs-self-hosted.md` — explicit code-parity commitment + 3 documented exceptions.
  • `docs/THREAT_MODEL.md` v2 — extends M0 v1 with MCP gateway, partner-PR, public standards, and `demo.glide.dev` threats. Money-safety F-rules promoted to architectural commitments.
  • `schemas.glide.dev` / `docs.glide.dev` / `demo.glide.dev` hosting deploys.
  • License-compat-scan + supply-chain-scan + egress-host-lint CI gates (documented; workflows land in follow-up commits).

[M5] - 2026-04-25 — Agent Skills Library + contribution infra

  • `@glideco/skills-base` — `SkillManifest` Zod schema + `SkillContractTestSuite` with consent-under-disclosure guard.
  • 6 hero skill packages (each with manifest + 4-tier policy presets): `@glideco/skill-ap-agent-claude-quickbooks` (blocked V3), `@glideco/skill-ap-agent-chatgpt-xero` (blocked V3), `@glideco/skill-treasury-yield-agent` (blocked V3), `@glideco/skill-trip-budget-chatgpt` ✅, `@glideco/skill-market-research-cap` ✅, `@glideco/skill-payroll-co-signer` (blocked V2).
  • `CONTRIBUTING.md` — partner-PR flow + 8-gate CI matrix + 5d/10d review SLA.
  • `.github/ISSUE_TEMPLATE/{new-connector,new-skill}.yml`.
  • `CODEOWNERS` — `_base/` interface contracts + trust-field promotion gated.
  • `scripts/validate-manifests.mjs` + `.github/workflows/connector-skill-ci.yml` — 27 manifests pass on first run.

[M2.5] - 2026-04-25 — Headless OSS extraction + apps/mcp docs

  • Renamed 4 Headless packages from `@repo/*` → `@glideco/*`: `policy-engine`, `schemas`, `grant-wrapper`, `secrets-scan`. 49 import sites rewritten across `apps/mcp`, `apps/web`, and the four packages' mutual deps.
  • Each package gains `publishConfig.access:public`, `files` allowlist, `repository`, `homepage`, `keywords`, `description`.
  • `apps/mcp/README.md` + `apps/mcp/COMPLIANCE.md` + `apps/mcp/LICENSE`.
  • `docs/agents/SELF_HOSTING.md` — agent platform self-host guide.
  • `scripts/scan-secrets.mjs` — runs `@glideco/secrets-scan` over `apps/mcp/src` + Headless packages. Result: 0 findings.
  • False-blocker correction: Upstash free tier IS provisioned via Vercel-Upstash integration. Paid tier only required at the 10k commands/day cap.

[M3] - 2026-04-25 — Self-host quickstart

  • `@repo/secrets` — 4 backends (`env-file`, `doppler` live; `aws-sm`, `vault` stubs). 25 tests.
  • `@repo/cli` — `glide demo` / `doctor` / `migrate` / `docs:env`. 16 tests.
  • `create-glide-app` — npx scaffolder. 6 tests.
  • `docker-compose.demo.yml` + `scripts/generate-keys.sh`.
  • `docs/SELF_HOSTING.md` — 90s TTHW quickstart, 4 reference deploy targets.

[M2] - 2026-04-25 — Privy / Chainalysis / Alchemy decoupling + fail-closed audit

  • `@repo/connectors-chainalysis` (live + sandbox) + `DISCLAIMER.md`.
  • `@repo/connectors-chainalysis-mock` — deterministic-fixtures screener (`isMock: true`).
  • `@repo/connectors-sanctions-permissive` — no-op screener with red banner; production refusal unless explicit override.
  • `@repo/connectors-alchemy` — extracted from `composite-balance.ts` (681 LOC) + `alchemy-rpc.ts` (57 LOC). Subpath `/rpc-url` export.
  • `@repo/connectors-rpc-direct` — zero-vendor balance fallback. Raw `viem` (EVM) + JSON-RPC (Solana).
  • `@repo/connectors-privy` — wraps `PrivyClient` from `@privy-io/server-auth` as an `AuthProvider`-conforming class.
  • `apps/web/src/server/adapters/index.ts`: `SCREENING_PROVIDER` + `BALANCE_PROVIDER` env-driven resolution + production fail-closed audit.

[M1] - 2026-04-25 — Connector package extraction (13 vendors)

  • 13 vendor adapters extracted to `packages/connectors/<slug>/`: aeon, manteca, paytrie, due, monerium, noah, gnosis-pay, wirex, binance-pay, column, idrx, avenia, bridge.
  • `@repo/connectors-base/legacy` subpath — `BaseVendorAdapter` + v1.5 role-based interfaces.
  • `@repo/eslint-config/no-app-import` — directional ESLint rule blocking `packages/connectors/*` from importing `apps/*`.
  • 60 contract tests + 144 apps/web integration tests passing. Router diff vs main: empty except one trivial Aeon import-path rename.

[M0] - 2026-04-25 — Foundations (parallel-safe additive scope)

  • `packages/shared/src/env/schema.ts` — centralized Zod env schema keyed by runtime.
  • `packages/connectors/_base/` — capability interfaces + `ConnectorManifest` Zod + `ContractTestSuite` + 14 capability types.
  • `@glideco/secrets-scan` (initially `@repo/secrets-scan`) — denylist + entropy scanner. 19 tests.
  • `noah-mock` + `bridge-mock` proof-of-pattern mock connectors.
  • Mock-mode boot-time refusal: production + mock-mode = HARD REFUSAL unless `GLIDE_ALLOW_MOCKS_IN_PROD=true`.
  • `apps/web/src/app/health/route.ts` — minimal liveness probe.
  • `LICENSE` (MIT) + `SECURITY.md` + `CODE_OF_CONDUCT.md` + `.github/workflows/dco.yml` + `.github/pull_request_template.md` + `docs/THREAT_MODEL.md` v1.

0.2.0.0

**Glide Business mobile shell — v1 ships.** 47 slots delivered end-to-end: type-aware sign-up routing, a four-tab business shell (Treasury / Vaults / Approvals / More), native iOS + Android enclave signing for Solana, EIP-712 off-chain approval signing, rich + silent push, WalletConnect v2 pairing + session handling, batch transfer composer, allowlist propose-add, EVM recovery inbox, and full Solana recovery parity (backend + mobile UI). Feature ships ON by default in non-production and on the production EAS profile — multisig user vaults are live.

Added

  • **Business mobile shell** — new `(tabs-business)` route group with Treasury (cross-entity overview), Vaults (per-entity + flat), Vault detail (wallets, members, proposals, TreasuryHome dashboard, allowlist, WalletConnect), Approvals hero tab (list, detail, sign, reject, step-up biometric), Settings/More. Ship-dark `NEXT_PUBLIC_MULTISIG_USER_VAULTS_ENABLED` flag now defaults ON since web's multisig user vaults shipped.
  • **Native enclave signing** — `@glide/enclave-signer` Expo module. iOS uses a Secure Enclave P-256 KEK to ECIES-wrap a software Ed25519 seed (`GWB1` 296-byte wrap blob, HKDF info `"glide-ed25519-wrap-v1"`, 76-byte AAD). Android mirrors via StrongBox (TEE fallback), same wire format. Biometric per sign, zeroed seed via `sodium_memzero`, key invalidated on biometric re-enrollment. Design specs at `docs/designs/ios-secure-enclave-ecies.md` and `docs/designs/android-strongbox-ecies.md`.
  • **Mobile sign hooks** — `useRelayerSign` (EVM Privy → relayer, gasless), `useEnclaveSigner` (Solana via native enclave), `useBatchSign` (one biometric ceremony per batch, anomaly + step-up items excluded), `useOffchainSign` (EIP-712 Privy signTypedData for `add_allowlist_entry` proposals).
  • **Decision-aid backend** — 6 new tRPC procs: `multisig.getRiskAnnotations` (heuristic signals: hasAnomaly, isNewRecipient, amountDeviation median vs mean, policyDistance, simulationStatus, recipientHistoryCount), `multisig.getProposalSummary` (Claude-backed summary, feature-flagged, strict JSON schema, eval harness + 10 synthetic fixtures), `multisig.getPresence` + `multisig.heartbeat` (Redis ZSET, 30/min rate limit, 30s staleness, 300s key TTL, fail-open), `multisig.getUnreadBadgeCount` + silent push fan-out.
  • **Solana recovery parity** — new `initiateSolanaRecoveryAction` / `proposeCancelSolanaRecovery` / `executeSolanaRecoveryAction` procs. `solana-recovery-action.ts` state machine, `squads-recovery-encoder.ts` (add_member / remove_member / swap_owner / change_threshold via Squads configTransaction + proposalApprove + configTransactionExecute bundle). Server-side 72h cooldown enforcement (v1) with on-chain hardening deferred to v1.1. `GLIDE_RECOVERY_SOLANA_PRIVATE_KEY` KMS config (`solana-recovery-config.ts`) with lazy-load + `RECOVERY_SOLANA_PAUSED` kill-switch. `vault-create.ts` auto-attaches Seat R on solo 1/1 Solana vaults at genesis (idempotent, silently skips when recovery is disabled).
  • **Mobile cathedral expansions** — TreasuryHome dashboard (total value + 7d chart + network allocation + PnL), per-wallet NFT gallery, per-wallet approval-risks view (Revoke.cash-style with Unlimited/High/Bounded risk tiers), per-wallet swap (with mandatory $100k whale-tier block until G.5.3 ships token pricing), propose-batch-transfer (MultiSendCallOnly, with "Batched transfers count toward your daily cap" pre-submit copy), allowlist propose-add flow (with Chainalysis re-screen rejection handling on un-remove), WalletConnect v2 (QR scanner via `expo-camera`, session manager with PR #142 retry+sync, session-request handler, push fanout).
  • **EVM recovery UI** — `CountdownCard` (72h timer, 0px-radius progress bar, veto window), `InitiateRecoverySheet`, `RecoveryBanner` on vault detail, chain-aware for both EVM and Solana. Handles `ACTIVE_RECOVERY_EXISTS` from migration 0030's uniqueness gate gracefully (surfaces existing row instead of erroring).
  • **Device attestation backend** — `user.registerDeviceAttestation` mutation accepts iOS DCAppAttestService + Android Key Attestation cert chains. Shape-validates; cryptographic verification deferred to v1.1 hardening.
  • **Push payload enrichment** — visible pushes now carry amount / amountFormatted / recipient / chain / verdict / summary (rich) OR a generic stub (minimal), per user's `pushSubscriptions.privacyMode`. Silent badge-update push runs alongside for mobile icon-badge sync.
  • **Mobile infrastructure** — EAS build profiles (development / preview / production with multisig flag ON), `@sentry/react-native` (DSN via EAS env, `sendDefaultPii: false`, `tracesSampleRate: 0`), `jest-expo` harness + 248 tests, 16 Maestro E2E critical-path flows under `apps/mobile/.maestro/`.
  • **Store submission artifacts** — `apps/mobile/store/` directory: runbook, submit checklist, iOS/Android metadata stubs, screenshot specs, privacy policy + ToS drafts (flagged for legal review), post-launch runbook.
  • **Migrations** — `0035_push_subscriptions_privacy_mode.sql` (rich/minimal toggle), `0036_device_attestations.sql` (partial unique index excluding rejected rows), `0037_multisig_recovery_actions_solana_widen.sql` (widen tx_hash columns to 88 chars, add `solana_multisig_pda`).

Changed

  • **Typed `AppRouter` on mobile** — `apps/mobile/src/lib/trpc.ts` now imports `AppRouter` from `@repo/shared` (which re-exports from web's router). Dropped mobile's `type AppRouter = any` placeholder. This alone fixed ~27 pre-existing mobile type errors.
  • **`useFeatureFlags.multisigUserVaultsEnabled` defaults to `true`** at runtime now that web's multisig product is live. `app.config.js` mirrors web's `isMultisigUserVaultsEnabled` semantics: explicit `'true'`/`'1'` wins, explicit anything else is OFF, unset defaults ON for non-production. Emergency kill via explicit `"false"` still works.
  • **Confirm-recovery-actions Inngest cron** — dispatches Solana rows to `confirmSolanaTx` alongside EVM. Reads Squads `Proposal` account to detect on-chain cancellation and flips DB rows to `cancelled`.

Fixed

  • **Privy SDK type drift on mobile** — `usePrivy()` no longer exposes `isAuthenticated`. Replaced with `const { user, isReady } = usePrivy()` + `const isAuthenticated = isReady && !!user` in `login.tsx` and `privy-token-bridge.tsx`. `useRef<number>()` replaced with `useRef<number | undefined>(undefined)` for React 19 compatibility.

Added — Agent platform / MCP v1

  • **`apps/mcp` Express service** — HTTP gateway exposing 21 banking tools across `/mcp/{read,write,treasury}` (JSON-RPC 2.0). Read tools (6): `accounts.list`, `accounts.balance`, `transactions.list`, `agents.list`, `skills.list`, `audit.stream`. Write tools (11): `agent.budget.create`, `agent.budget.revoke`, `beneficiary.add`, `cards.freeze`, `cards.issue`, `payments.initiate`, `payments.simulate`, `payroll.run`, `transfer.schedule`, `x402.pay`, `x402.receive`. Treasury tools (4): `agent.grant.issue`, `kill_switch.all`, `vault.rotate_signer`, `yield.allocate`. Operational endpoints: `/healthz`, `/readyz`, `/mcp/manifest`. Boot-time uses dev-secret HMAC verifier (P1.1 Hydra OAuth deferred — see TODOS). 175 vitest cases across 15 test files covering tool behavior, error taxonomy, tenant routing, grant verification, rate-limiting, sigil elicitation.
  • **Three new packages.** `@repo/policy-engine` — pure `evaluate()` over 13 policy axes per agent grant (27 unit tests). `@repo/grant-wrapper` — fresh-read tenant verification against DB before tool execution (money-safety F3 gate, 16 unit tests). `@repo/schemas` — Zod schemas for `AgentPolicyEnvelope` / `Grant` / `Receipt` / `AgentScope` (35 unit tests).
  • **Agent tRPC router** at `apps/web/src/server/routers/agent.ts` — principal CRUD (list / fetch / revoke), grant management (list / revoke), forensics export, admin kill-switch. Protected by entity-member / admin authz gates.
  • **Six new migrations.** `0039_agent_principals.sql` (agent identity + vault linkage + skill installation), `0040_agent_grants_policies.sql` (`agent_grants` + `agent_policies` — what an agent is permitted to do), `0041_skill_installations_pending_payments.sql` (skill registry + `agent_pending_payments` for CAS-claim idempotency, money-safety F2 gate), `0042_activity_log_agent_cols.sql` (`activity_log` agent columns + append-only trigger, money-safety F3 gate), `0043_multisig_policy_mode_agent_values.sql` (`multisig_policy_mode` enum extended with `agent_budget` / `sweep_to_parent` / `frozen`), `0044_agent_pending_payments_saga_reaper_idx.sql` (partial index for the saga reaper).
  • **Eight new web routes.** `/skills` and `/skills/[slug]` (public catalog + install wizard), `/dashboard/agents` and `/dashboard/agents/[id]` (member read), `/dashboard/agents/[id]/audit` (admin forensics log), `/admin/agents-kill-switch` (mass emergency revoke), `/step-up/[sigil]` (URL-mode elicitation handler with 5-minute grace window).
  • **Money-safety regression suite.** F1 (server-side x402 tx verification before settle), F2 (CAS-claim semantics on `agent_pending_payments`), F3 (append-only `activity_log` SQL contract), F4 (per-agent rate limit), F5 (per-vault policy-version advisory lock).
  • **LLM eval harness with Wilson-95 confidence interval.** `eval-proposal-summaries.ts` adversarial fixture set (prompt injection, URL leak, homoglyph, ZWJ emoji, null-token probe, markdown fence, SYSTEM impersonation, role inversion) plus `mustNotContainInSummary` gate. Eval now actually _tries_ to break the post-parse guards.
  • **Tiered rate-limit gateway.** Per-agent + per-tenant + per-tool buckets via Upstash. Free-tier OK for staging; paid tier required before prod (TODO P1.1).
  • **Six hero skills** for distribution: AP, treasury, consumer, payroll, x402-pay, and x402-merchant. Source-of-truth catalog at `apps/web/src/lib/agent-platform/hero-skills.ts`.
  • **Partner pack drafts.** Manifest drafts for Anthropic Connector, ChatGPT Apps, Google Vertex, OpenClaw, Hermes at `docs/designs/agent-distribution-partner-packs/{anthropic,openai,google-vertex,openclaw,hermes}/`. Ready for partner registry submission once `apps/mcp` deploys.
  • **Six new env vars** for the agent platform (HMAC verifier dev secret, Hydra OAuth config slot, Privy IDP config, etc) added to `apps/web/.env.example` and documented in the deploy checklist.
  • **Dev docs** at `docs/agents/{README,oauth-flow,submission-status,tool-reference}.md` — quickstart, OAuth flow, partner submission status, full tool reference.
  • **Runbooks** at `docs/runbooks/agent-platform/{compromised-agent,kill-switch-incident,rate-limit-tuning}.md` — on-call playbooks for the agent platform.
  • **Deploy checklist** at `docs/runbooks/v0.2.0.0-deploy-checklist.md` — full v0.2.0.0 cutover procedure (multisig + agent platform).

0.1.1.1

Changed

  • Marketing pages (`/payroll`, `/pricing`, `/eligibility`, `/about`) now show marketing nav (Payroll / Pricing / Eligibility / Log In) instead of the legal-doc nav, so users can bounce between marketing pages without going back to home first. Legal pages (`/terms`, `/privacy`, `/monitoring`, `/data-retention`) keep the compliance nav.
  • Bumped `--color-hint` from `#7a7299` to `#948cb7` so hint microcopy clears WCAG AA contrast on the Mysteria Purple background (3.83:1 → 5.60:1). Affects hero microcopy, footer legal disclosure, card labels, form placeholders, and every other `text-hint` use across the app.
  • FX section H2 on the homepage ("The real rate, not your bank's.") now renders at the same Playfair Display weight 400 as every other H2, removing an inline `fontWeight: 500` outlier.

Fixed

  • Homepage FX comparison widget now shows "Unable to fetch live rates right now. Please try again." when the public quote API fails, matching the behavior on `/fx-calculator`. Previously the savings headline silently blanked, leaving users looking at "1000 USD → —" with no explanation.
  • "Full calculator →" and "See full comparison →" eyebrow links are now 44 px tall so they meet WCAG 2.5.5 and the DESIGN.md touch-target rule. Mobile users could miss-tap the 16–19 px originals.
  • `scripts/dev-start.mjs` uses `import.meta.url`-resolved paths instead of the hardcoded developer-specific paths baked in from an earlier workstation. `pnpm --filter web dev` now works from any clone location.

0.1.1.0

Security hardening batch closing ~28 findings surfaced by three independent adversarial reviews (Codex + Opus + Gemini) against PR #29 (Multi-sig V1) and the 2026-04-22 FX-allowance review.

Added

  • Migration 0033: widens `admin_audit_events.ip` to `varchar(128)` to accommodate zone-ID'd IPv6 (`fe80::1%en0`)
  • Migration 0034: new `multisig_wc_pending_pairings` table — maps WalletConnect pairing topic → vault/wallet/chain so `wcApproveSession` can refuse cross-entity session hijacks
  • Inngest cron `cleanupWcPendingPairings` — sweeps expired pending-pairing rows every 15 minutes
  • `server/lib/safe-text.ts` — `safeText(max)` zod validator rejecting `<`, `>`, and ASCII control chars; wired into `sendGift.message` + `updateMyProfile.bio` + `updateMyProfile.quote`
  • New rate limiters: `giftLookupPerUser` (30/min per user) and `multisigOffchainApprovePerUser` (5/min per user)
  • `server/lib/user-multisig/nonce.ts`: `fetchOnChainSafeNonce()` helper; `allocateNextNonce()` now reconciles DB nonce against live `Safe.nonce()` when `safeAddress`+`rpcUrl` are passed

Changed

  • Multi-sig `prepareExecute` + `executeReadyViaRelayer` now re-run the policy engine, re-screen recipients for sanctions, filter removed signers out of packed signatures, and enforce the pre-approval gate before CAS-claiming — closing TOCTOU windows between propose and execute
  • Multi-sig `executeReadyViaRelayer` mirrors `prepareExecute`'s `cancel_recovery` staleness check + chain-drift gate — relayer gas can no longer be burned on stale recoveries or drifted chains
  • Multi-sig CAS claims now allow stale-claim reclaim after a 5-minute grace when the prior claimer didn't broadcast (matches the `cancelReadyProposal` window) — closes the "browser crash bricks a signed proposal" failure mode
  • Multi-sig `initiateRecoveryAction` rate-limited, sanctions-screened on `newOwner`, rejects post-recovery threshold below vault's `approvalThreshold`, capped at 3 recoveries per vault per 24h, requires ticket-shaped `adminAuditRef` regex
  • Multi-sig `updateVault` refuses any weakening change to `timelockThresholdCents` (raising or disabling) or `timelockWindowSeconds` (shortening)
  • Multi-sig `proposeBatchTransfer` policy engine now evaluates the batch TOTAL in addition to each sub-transfer — closes the "10× sub-transfers under cap, total over cap" bypass
  • Multi-sig `proposeSwap` applies a stable-input heuristic: USDC/USDT/DAI input derives `amountCents` from raw units so whale-tier timelock + daily caps apply; non-stable inputs ≥1M token units are refused pending G.5.3 pricing
  • Multi-sig `submitSolanaSignature` fetches the on-chain Multisig account alongside the Proposal, filters approvals against the current on-chain member set, and rejects with CONFLICT if the Squads threshold drifted from the proposal's `requiredApprovalsOffchain`
  • Multi-sig `submitOffchainApproval` layers a per-user rate limit on top of the existing per-entity cap
  • Multi-sig `acquireNonceLock` switched from session-scoped `pg_advisory_lock` to `pg_advisory_xact_lock` — closes a connection-pool lock leak
  • Multi-sig `_proposeSolanaConfigChange` now serializes transactionIndex fetch + insert under an advisory lock, matching the EVM pattern
  • Multi-sig `composeSafeMultiSendCall` asserts target address at runtime — refactor-guard against accidentally wiring the plain MultiSend (DELEGATECALL-enabled) instead of MultiSendCallOnly
  • Push subscription persistence now transfers ownership across accounts on shared devices — a browser/device signed into a new account stops receiving the prior user's multisig pushes
  • Push fan-out gates web and Expo channels independently — Expo mobile pushes deliver even when VAPID is unconfigured (mobile-only / partial-staging envs)
  • Notification preferences now cover `multisig/pre_approval_requested` + `multisig/pre_approval_decided` (previously surfaced in GET but rejected by update/reset)
  • Gift cross-period accounting: `getAllowanceState` uses effective-date predicate per gift status (`active` by `acceptedAt`, `pending_accept` by `createdAt`) instead of createdAt for both
  • Gift-recipient handle lookup rate-limited per authenticated user (30/min)
  • `getAllowanceState` query batching: 5 independent queries now run in parallel via `Promise.all` — ~5× RTT savings on every dashboard mount
  • `evaluateAutoPullEligibility` drops its unreachable `debit_expired` branch — cron's upstream `expiresAt > now` filter is the authoritative guard
  • `fetchEvmDebitReceipt` performs inline USDC token-address comparison via `getUsdcAddress(chain)` — defense in depth on top of downstream `verifySignedDebit`
  • `CHANGELOG` (this file) and `TODOS.md` updated to reflect the full Batch I status (~28 fixes across 3 reviewers)

Fixed

  • Stored-XSS surface on `/founders/[handle]` bio/quote and gift messages (React already escapes, but server-side validator now hard-rejects `<`/`>`/control chars at the tRPC boundary)
  • Documented `fxAllowanceUsage.periodMonth` calendar-vs-billing-cycle semantics above `firstOfMonthUtc`

0.1.0.0

Added

  • Shareable payment tracking pages at `/track/[code]` with Wise-style vertical timeline, estimated arrival by rail, QR code for in-person sharing, and CSS confetti on completion
  • Recipient instant claim flow: recipients sign up from tracking page, link to sender's beneficiary via email matching, with sender confirmation required before future payment routing activates
  • Beneficiary links table (`beneficiary_links`) with full lifecycle management (pending, confirmed, rejected, revoked)
  • Admin tooling: lookup payments by tracking code, invalidate tracking links
  • Tracking link expiration: daily Inngest cron nullifies codes 90 days after completion (includes failed/returned transactions)
  • Email notification with tracking link sent to sender when payment moves to processing
  • Dashboard "Share" button on transaction rows copies tracking URL to clipboard
  • Privacy-safe Open Graph meta tags (generic content, `noindex`) for social sharing

Changed

  • Webhook handlers (Column, Noah, Bridge, Monerium, Paytrie) now insert `transactionEvents` on status changes with idempotent unique index, enabling richer payment timelines
  • Webhook status transitions are now guarded (`WHERE status IN (...)`) to prevent out-of-order regression
  • Rate limiting for public tracking endpoint uses dual strategy: per-IP via Upstash + per-code in-memory
  • `claim-received` email template added for beneficiary link notifications

Fixed

  • Self-claim prevention: senders cannot claim their own payment as recipient
  • Beneficiary matching restricted to email-only (removed insecure fallback to beneficiary ID)
  • Stale closure in TrackingClient polling error handler
  • Revoke claim is now idempotent (no-op on already-revoked links)
  • Expiration cron now covers failed/returned transactions (not just completed)