Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <seller-near-account>` 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
Expand Down
6 changes: 5 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <near-account>` 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 <near-account>` 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 <near-account>` 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)
Expand Down Expand Up @@ -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 <near-account>` 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.
Expand Down
Loading