diff --git a/DEPLOY.md b/DEPLOY.md index e503a57..97b8df1 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -168,6 +168,53 @@ Without it, previews price but no deposit address is issued. Git-connected, so a merge alone does not ship it. Verify with the same command locally against the stored value; the hosted pay page then issues real addresses. +### Real payment runbook (S8.4, 👤) + +Prepared 2026-11-11. Everything below except the credential and the two sends was checked +without moving funds. + +**What was measured (live `dry: true` quotes, no credential, nothing sent):** a **$2** invoice +paid out as USDC on NEAR is quoted on every route tried. The client pays 2.020443 USDC from +Base (~35 s) or Arbitrum (~25 s), 2.022398 USDT from Ethereum (~45 s), ~0.00099 ETH from Base +(~37 s), or ~0.56 wNEAR (~22 s). That is about 1% on top, and no route refused $2 as too small. +`/api/health` on the hosted URL answered `{"status":"ok","demo":false,"database":"ok"}`. + +**The worker is not needed for a supervised payment.** The pay page polls +`GET /api/pay/:publicId/status` every 5 s (`STATUS_POLL_MS`), and that route runs the same +`syncAttempt` write the worker does (`lib/server/sync.ts`). So the Vercel URL can take the +payment as long as the pay page stays open until "Paid". What the worker adds is unattended +updates and the 60 s quote expiry, and a 2-minute payment needs neither. + +**The storage payer is not needed either, if the payout is picked well.** USDC on NEAR is only +delivered to an account registered with the USDC contract. A NEAR account that already holds +USDC is registered, so it needs no payer funding (S0.10/S2.10 stay parked). The settings page +runs the read-only storage check when the payout is saved and says which case applies. Another +way to skip the question is a USDC-on-Base or USDC-on-Arbitrum payout address the seller controls. + +**Steps (about 15 minutes once the credential exists):** + +1. **Credential (S0.8).** Follow "Obtaining the 1Click credential" above. Proof: + `pnpm oneclick:check --real ` prints a verified signature and + `depositAddress: yes`. Then `vercel env add ONECLICK_JWT production` and + `vercel deploy --prod --yes --scope accensa` from `settle/`. +2. **Seller.** Open https://settle-wine-alpha.vercel.app, sign in with the seller's NEAR wallet + (this is also the first live wallet login, still unproven). Under Settings, set the payout: + USDC on NEAR to an account that already holds USDC, or USDC on Base/Arbitrum. +3. **Invoice.** Create one line, **$2.00** (or $5 for more margin), and copy the pay link. +4. **Client.** Use a different wallet from the seller's, holding about $2.10 of USDC on Base or + Arbitrum plus a little ETH for gas. Open the pay link, pick that token, enter that wallet as + the refund address, press the free preview, then "Get payment address". +5. **Send.** Send the exact amount shown to the address shown, from the refund wallet. Paste the + transaction hash into the page if it offers the field, and **leave the tab open** until it + says Paid. +6. **Record.** Copy the deposit tx link (Basescan/Arbiscan), the delivery tx link (NEAR explorer + or the receipt), and the receipt URL into S8.4 and issue #72. Screen-record steps 4–6 for + the video's real-payment slot (`recordings/real/real-payment.mp4`, S10.2). + +**If something goes wrong:** money sent short is refunded in full to the refund address at the +deadline, unless it is topped up first. Money sent after the address expires, or sent in excess, +also goes back to the refund address. No path leaves funds with Settle. + ## 6. Host options, read on 2026-11-08 S8.1 has to buy the shape proven in §3: **two long-lived processes and one Postgres, all reachable diff --git a/ROADMAP.md b/ROADMAP.md index a0f9c73..9a82eb7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -181,7 +181,7 @@ Format: `- [task id] what blocks it · who can unblock · since date` - [x] **S8.1** [#69](https://github.com/MFrancis-dev/settle/issues/69) ~~👤 Pick hosting (web, worker, Postgres) and create accounts~~ → **decided: Vercel, on the human's instruction, 2026-11-08** · the Vercel and Render CLIs were already authenticated to her accounts, so no account was created. The web tier is a Vercel serverless Next.js deploy; the database is a dedicated Neon Postgres (`settle-hackathon`, region `aws-us-east-1`, free tier — Neon stores are provisioned through the marketplace/dashboard, not `vercel storage create`, so it was made with the authenticated `neon` CLI). The honest cost of the serverless choice: it satisfies "reachable web + Postgres" but **cannot run the background worker**, which needs a long-lived process — so this host shows the app but is not, on its own, the full two-process topology `DEPLOY.md` §1–§2 requires for live payments. That limit is stated in the README, not hidden · [#69](https://github.com/MFrancis-dev/settle/issues/69) - [x] **S8.2** [#70](https://github.com/MFrancis-dev/settle/issues/70) ~~👤 Set production env vars~~ → **set on the Vercel project, 2026-11-08** · `SESSION_SECRET` (a fresh `openssl rand -hex 32`, generated here and stored as a Vercel secret — it is required on a serverless host because `randomProcessSecret()` differs per function and would break every session), `DATABASE_URL` (the Neon pooled `-pooler` string, which the app's own `postgres`+Drizzle path was tested against before deploy), `PUBLIC_APP_URL=https://settle-wine-alpha.vercel.app`, and `NEAR_RPC_URL=https://rpc.mainnet.near.org`. `DEMO_MODE` is unset (correct — `demoModeConflict` would refuse it behind `https://`); `ONECLICK_JWT` is deliberately absent, so payment quotes stay fail-closed until S0.8 · [#70](https://github.com/MFrancis-dev/settle/issues/70) - [x] **S8.3** [#71](https://github.com/MFrancis-dev/settle/issues/71) ~~Deploy; hosted demo mode reachable~~ → **Deployed real mode; the hosted app is reachable and `/api/health` is green** · 2026-11-08: `vercel deploy --prod` built the app and it is live on https://settle-wine-alpha.vercel.app with deployment protection turned off so a judge reaches it (SSO was on by default — a login wall would fail the "reachable" bar). Landing 200, protected routes 307 to the sign-in redirect, `/api/tokens` 200, `/api/invoices` 401 without a session, `GET /api/health` → `{"status":"ok","demo":false,"database":"ok"}` **from the running deployment**. Running it exposed and fixed a real serverless defect that no local test caught: Drizzle's migrator reads `./drizzle/meta/_journal.json` from the filesystem at request time and is never `import`ed, so output-file tracing omitted it and every DB route 503'd with `Can't find meta/_journal.json` — fixed with `outputFileTracingIncludes: { "/*": ["./drizzle/**/*"] }` in `next.config.ts`. The demo-mode story remains `pnpm demo` (SD-8), and live payments still need the worker + JWT (S8.4). **Re-deployed 2026-11-09** (`vercel deploy --prod --yes --scope accensa`, deployment `settle-hq4v2wxpg-accensa.vercel.app`, aliased to the same URL) because the running build predated PR #133: the staleness probe `GET /api/auth/challenge?intent=session` answered **404** before and **200** after, with a real nonce, the server's own recipient and a 5-minute expiry. Re-probed from outside: `/api/health` 200 `{"status":"ok","demo":false,"database":"ok"}`, `/` 200, `/demo` 200, `/settings` 307 to sign-in, no HTTP ≥ 400 on any asset the page loads, and no horizontal overflow at 390 px. A real-browser visit found one defect the curl probes could not — S7.13/#140, the landing page's hydration mismatch, fixed in PR #141 and carried to the same URL by a second deploy from `5fb11cc` (`settle-e25w2k2lm-accensa.vercel.app`): the served HTML now offers sign-in instead of denying it, and Chromium at 1440 px and 390 px reports the button enabled with zero page errors, zero console errors, zero failed requests and no horizontal overflow · [#71](https://github.com/MFrancis-dev/settle/issues/71) -- [ ] **S8.4** [#72](https://github.com/MFrancis-dev/settle/issues/72) 👤 Approve and do one small real payment end-to-end; record tx links · **approved 2026-11-10, blocked on the vendor credential.** The refusal was measured against the live API that day, and it is narrower than first recorded (S8.7): a **dry** quote still answers 200 without a credential, so a preview is drivable — only the **non-dry** quote that carries a `depositAddress` is gated. A real payment needs, in this order: `ONECLICK_JWT` from partners.near-intents.org (S0.8, 👤; the steps are in `DEPLOY.md` "Obtaining the 1Click credential", and `pnpm oneclick:check --real ` proves it without moving funds), a funded signer for the client's side, a host that runs both the web app and the worker (Vercel runs neither the 5 s poll nor the 60 s expiry), and a payout address the seller controls. +- [ ] **S8.4** [#72](https://github.com/MFrancis-dev/settle/issues/72) 👤 Approve and do one small real payment end-to-end; record tx links · **approved 2026-11-10, blocked on the vendor credential.** The refusal was measured against the live API that day, and it is narrower than first recorded (S8.7): a **dry** quote still answers 200 without a credential, so a preview is drivable — only the **non-dry** quote that carries a `depositAddress` is gated. A real payment needs, in this order: `ONECLICK_JWT` from partners.near-intents.org (S0.8, 👤; the steps are in `DEPLOY.md` "Obtaining the 1Click credential", and `pnpm oneclick:check --real ` proves it without moving funds), a funded signer for the client's side, a host that runs both the web app and the worker (Vercel runs neither the 5 s poll nor the 60 s expiry), and a payout address the seller controls. **2026-11-11: prepared; the list is shorter than this.** The step-by-step is in `DEPLOY.md` "Real payment runbook". (a) **The worker is not needed for a supervised payment.** The pay page polls the status route every 5 s, and that route runs the worker's own `syncAttempt` write, so the Vercel URL can complete a payment while the tab stays open. (b) **The storage payer is not needed** if the payout is USDC on NEAR to an account that already holds USDC, or USDC on Base/Arbitrum. (c) Live `dry` quotes for a **$2** invoice into USDC on NEAR were served from USDC on Base, USDC on Arbitrum, USDT on Ethereum, ETH on Base and wNEAR, at about 1% over, settling in 22–45 s. What remains: the credential (S0.8), a client wallet with about $2.10 plus gas, and the two sends. - [ ] **S8.5** [#73](https://github.com/MFrancis-dev/settle/issues/73) Fix anything the real run exposed · 2026-11-09: the **hosted** run exposed one — the landing page's hydration mismatch, fixed as S7.13 ([#140](https://github.com/MFrancis-dev/settle/issues/140)). Stays open because S8.4's real payment has not run, and that is the run this task is really waiting on. - [x] **S8.6** [#118](https://github.com/MFrancis-dev/settle/issues/118) Make the app deployable by whoever picks the host: `GET /api/health` (200 `ok` / 503 `degraded`, no-store, unauthenticated, not rate limited, 5 tests, probed against a real running demo server), `tsx` moved to runtime dependencies so `pnpm worker` can start on a production install, and `DEPLOY.md` — including the two **measured** constraints above. No vendor manifest committed and nothing deployed: an untested `render.yaml` would read as a claim that it works · 2026-11-08 - [x] **S8.7** [#159](https://github.com/MFrancis-dev/settle/issues/159) Make a missing 1Click credential legible, and prove one in one command · 2026-11-10, on the human's "address the ONECLICK_JWT blocker": re-measured the live API and the earlier finding was too broad. A **dry** `POST /v0/quote` answers 200 with no credential; the **non-dry** quote that carries `depositAddress` answers `400 {"message":"Internal server error"}` without one and `401 Invalid token` with a bogus bearer; `/v0/tokens` answers 200 either way. `400 Internal server error` for a missing deployment secret would have been shown to the client as their own mistake, so `lib/server/pay.ts` now checks `deps.oneClick.hasJwt` inside the real-quote branch (after the reuse/conflict checks, dry previews untouched, **demo mode exempt** — its transport mints the address itself and never forwards a real quote) and throws `500 server_misconfigured` with a sentence that says the deployment is at fault. Two route tests pin it — 500 + `server_misconfigured`, **zero calls to 1Click**, and the dry preview still 200; and a demo deployment with no JWT still gets an address. `pnpm oneclick:check` (`scripts/check-oneclick.ts`) does the rest: with no token it exits 1 saying previews still work but no address can be issued; with a token it runs a real `EXACT_OUTPUT` quote for 10.5 USDC→USDC and prints status, in/out base units, fee, signature verdict and whether a deposit address came back; `--real ` validates the address (`validateAddress`, exit 2 if bad) and proves the address-issuing path without moving funds; a bogus token prints `failure: unauthorized (401) Invalid token`. It never prints the JWT. Corrected the stale "every unauthenticated quote" claim in `ARCHITECTURE.md` §3.2, README, `DEPLOY.md`, `LAUNCH.md` §1 and the S8.2 note; `DEPLOY.md` gained "Obtaining the 1Click credential" (portal → `.env.local` → check → `vercel env add`). The account itself is still human-only: the partner portal signs in through WorkOS (email/SSO) · [#159](https://github.com/MFrancis-dev/settle/issues/159) @@ -227,6 +227,10 @@ Format: `- [task id] what blocks it · who can unblock · since date` One line per contribution, newest first: `YYYY-MM-DD · who/agent · what changed · tasks` +- 2026-11-11 · agent · Re-deployed the hosted app on the human's "deploy it": `vercel deploy --prod --yes --scope accensa` → `settle-gefhtzmub-accensa`, built from the runbook branch (docs only, so the app is `main` at `0bcb949` unchanged). Probed from outside: `/`, `/demo`, `/api/tokens` and `/api/auth/challenge?intent=session` 200, `/api/health` `{"status":"ok","demo":false,"database":"ok"}`. No env change: `ONECLICK_JWT` is still absent, so a real quote still answers `server_misconfigured` · S8.3, S8.4 + +- 2026-11-11 · agent · Prepared the real payment on the human's "prep the real payment". Nothing was sent and no credential was used. Live unauthenticated `dry` quotes priced a **$2** invoice into USDC on NEAR from five origins (USDC Base/Arbitrum, USDT Ethereum, ETH Base, wNEAR) at about 1% over. Reading the code removed two prerequisites the S8.4 note listed. First, the pay page's 5 s poll runs the worker's own `syncAttempt`, so the Vercel URL can complete a supervised payment without the worker. Second, a payout account that already holds USDC (or an EVM payout) needs no storage payer. Added `DEPLOY.md` "Real payment runbook" (credential → seller sign-in and payout → $2 invoice → client pays → record links and screen capture for S10.2). Hosted `/api/health` green. Still 👤: the credential (S0.8) and the sends · S8.4 + - 2026-11-10 · agent · Re-deployed the hosted app so the live build carries S8.7 (and S7.14) rather than trailing `main` before judging. `vercel deploy --prod --yes --scope accensa` → `settle-72rnar3ir-accensa`, created 08:21 UTC from `main` at `0bcb949`. Probed from outside: landing 200, `/api/health` `{"status":"ok","demo":false,"database":"ok"}`, `/api/auth/challenge?intent=session` 200, `/api/tokens` 200. No env change: `ONECLICK_JWT` is still absent, so real quotes answer the new `server_misconfigured` sentence until the credential exists. · S8.7, S8.3 - 2026-11-10 · agent · Turned the dead end of a missing vendor credential into a named state and a one-command proof, on the human's "address the ONECLICK_JWT blocker". Re-measured the live API and the earlier "every quote is refused" record was too broad: a **dry** quote answers 200 without a credential, while the **non-dry** quote that returns `depositAddress` answers `400 {"message":"Internal server error"}` without one and `401 Invalid token` with a bogus bearer (`/v0/tokens` answers either way). **S8.7** ([#159](https://github.com/MFrancis-dev/settle/issues/159)): `lib/server/pay.ts` checks `deps.oneClick.hasJwt` in the real-quote branch (demo mode exempt, since its transport mints the address itself) and throws `500 server_misconfigured`, so a missing deployment secret no longer surfaces to the client as their own mistake — two route tests pin 500 + zero upstream calls + a dry preview still 200, and a JWT-less demo still getting an address. `pnpm oneclick:check` (`scripts/check-oneclick.ts`) exits 1 with "previews still work" when no token exists, and with one runs a real `EXACT_OUTPUT` 10.5 USDC→USDC quote printing status, base units, fee, signature verdict and whether an address came back; `--real ` validates the address and proves the address-issuing path without moving funds; a bogus token prints `failure: unauthorized (401)`. JWT never printed. Corrected the stale claim in `ARCHITECTURE.md` §3.2, README, `DEPLOY.md`, `LAUNCH.md` §1 and the S8.2 note, and added `DEPLOY.md` "Obtaining the 1Click credential". The portal sign-in (WorkOS email/SSO) is still 👤, then S8.4 needs a funded signer, a two-process host and a seller payout address.