Skip to content

feat(grid-wallet-prod): live Grid API — real passkey auth, funding, and signed cash-out (Phase 2) - #740

Merged
pengying merged 18 commits into
07-21-feat_grid-wallet-prod_fork_grid-wallet-demo_as_the_production_demo_appfrom
07-22-feat_grid-wallet-prod_add_vitest_gridunits_usdb_-_cents_
Jul 25, 2026
Merged

feat(grid-wallet-prod): live Grid API — real passkey auth, funding, and signed cash-out (Phase 2)#740
pengying merged 18 commits into
07-21-feat_grid-wallet-prod_fork_grid-wallet-demo_as_the_production_demo_appfrom
07-22-feat_grid-wallet-prod_add_vitest_gridunits_usdb_-_cents_

Conversation

@pengying

@pengying pengying commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Wires components/grid-wallet-prod from fully-scripted to the real Grid API (public sandbox), per the Phase 2 spec (docs/superpowers/specs/2026-07-22-grid-wallet-prod-live-api-design.md). The API panel now renders the requests/responses that actually went over the wire.

The full money loop is live-verified end to end: passkey sign-in → Add $20 (real platform on-ramp, COMPLETED) → Cash out $5 (quote → browser-signed Grid-Wallet-Signature execute → polled to COMPLETED in ~62s), with the displayed balance tracking the real account throughout.

What's in here

  • Server proxy (/api/grid/[...path]): allow-listed paths only, Basic-auth injected server-side, {request,response} envelopes with redacted auth — the panel logs real traffic.
  • Browser crypto (gridCrypto.ts): TEK keygen, HPKE encrypt/decrypt, Turnkey stamps via @turnkey/* — session keys never leave the client; cross-checked against scripts/embedded-wallet-sign.js.
  • Real auth (gridSession.ts): first-run EMAIL_OTP bootstrap (magic 000000 in sandbox) → registers a real Grid PASSKEY credential via WebAuthn → returning sign-ins are pure passkey (challenge → .get() → verify).
  • Webhook receiver (/api/webhooks): X-Grid-Signature P-256 verification against GRID_WEBHOOK_PUBKEY, event ring buffer + poll route.
  • Live money: balance/activity reads, Add money via the real platform on-ramp (the customer-wallet sandbox fund endpoint mints unspendable book balance — discovered live), outbound quote/stamp/execute/poll with completion checkmarks gated on COMPLETED.
  • 38 vitest tests across the pure modules; every task browser-verified against the live sandbox.

Env contract

GRID_CLIENT_ID/SECRET, GRID_API_BASE_URL, GRID_CUSTOMER_ID, GRID_WEBHOOK_PUBKEY in .env.local (git-ignored). Prod cutover = swap those values; first tap re-runs passkey registration against the production customer.

Tracked pre-prod follow-ups (not in this PR)

  • Gate GET /api/webhooks/events — route removed; deliveries are pushed over SSE instead of polled.
  • Proxy customer-scoping before production keys (today it's a single-customer sandbox gateway).
  • Wire real activity rows onto the phone — done below.
  • Phone-side failure states for execute errors (panel-only today).
  • Report upstream: POST /quotes returns undocumented 200 for platform-sourced quotes.

Update — funding flows, webhook-driven arrivals, sandbox gating

A second pass over the same component. Everything below was verified against the live sandbox in a browser; the API panel now shows only real traffic.

Auth follows the documented bootstrap order

  • Sign-in leads with the credential a Global Account is actually born with (EMAIL_OTP), with the entry step prefilled from the credential's own nickname — no invented address.
  • A passkey is added later, from the wallet, as its own signed action: one device ceremony authorized by the live session (it used to register and immediately authenticate, so a single tap asked for approval twice).
  • The CTA advertises the credential signIn will really use. Passkeys are device-bound, so a passkey listed on the account but registered elsewhere no longer sends this browser into an assertion it can't satisfy.
  • A lapsed session (15 min) can now re-authenticate in place: the OTP sheet has a surface on the wallet screen, and the WebAuthn call runs inside the tap's user activation instead of behind the re-auth round-trip.

Funding uses the endpoints Grid actually supports

  • Add money → country → deposit instructions: the customer's own account/routing/rails/reference, read from GET /customers/internal-accounts. (The EUR block is a clearly-marked placeholder IBAN in data/placeholderDeposit.ts until the customer has an EUR account — the read already returns one section per fiat account, so a real one appears with no code change.)
  • …or "add an account" to pull from. A pull quote sources the ExternalAccount — the previous body pointed at the platform's internal account. Grid rejects /execute for such a quote ("funds must be pushed … use POST /sandbox/send"), so the quote is created and left pending, and POST /sandbox/send stands in for the push. Amounts here are USD cents, not USDB micro-units.
  • Add from crypto: Base / Solana / Ethereum, network → amount → the real Grid-provisioned deposit address for that chain.
  • Saved banks are seeded from GET /customers/external-accounts (filtered to USD/EUR, ACTIVE); selecting one quotes against that account instead of re-creating it — which also fixes the stale-recipient gap noted in feat(grid-wallet-prod): live Grid API — real passkey auth, funding, and signed cash-out (Phase 2) #740's original follow-ups.

Webhooks

  • X-Grid-Signature parsing was rejecting every real delivery. Grid sends {"v":"1","s":"…"} (or a bare base64 header); we required {"version":1,"signature":"…"}, so verification never ran and everything 401'd. The earlier tests signed using our own assumed shape, so they passed — they now assert the documented formats.
  • Verified deliveries stream to the panel over SSE (/api/webhooks/stream); nothing polls. The bus is pinned to globalThis because Next gives each route handler its own module instance — the publisher and subscriber were otherwise in different registries.
  • An arrival, not a tap, is what says money landed: INCOMING_PAYMENT.COMPLETED drives the toast and Activity row from its payload and re-reads the balance from the API. Confirming an add no longer claims anything.

Panel, wallet, packaging

  • The panel shows only real requests: every synthesized call and response body is gone (including a fabricated POST https://your-app.com/webhooks/grid and ~330 lines of stubbed quote/transaction/session responses). Flows with no client call behind them log nothing.
  • Activity renders the real GET /transactions history (EXPIRED quotes filtered out); the balance hero shows available with the account total beneath, since sandbox book balance runs ahead of spendable.
  • Bank picker trimmed to the US + euro area (ACH + SEPA); APY 3%.
  • Dockerfile + .dockerignore on output: 'standalone'. NEXT_PUBLIC_GRID_SANDBOX is a build arg (NEXT_PUBLIC_* is inlined at build time) and gates the sandbox-only "simulate funding" affordance — a production image must be built without it.
  • scripts/ensure-business-customer.mjs: idempotent BUSINESS customer provisioning, writes GRID_BUSINESS_CUSTOMER_ID to .env.local.

60 vitest tests (was 38).

Still open

  • A received webhook only drives the add path; INCOMING_PAYMENT.PENDING and OUTGOING_PAYMENT.* are logged but don't touch the wallet.
  • The sandbox funding stand-in for the instructions screen still runs the platform on-ramp (no quote exists there to /sandbox/send).
  • USDB-native funding over Spark works (REALTIME_FUNDING + cryptoNetwork: SPARK, rate 1.0, verified) but isn't offered in the UI — both current paths convert.
  • docker build unverified: the daemon wasn't running here. The standalone server itself was run and exercised (page, webhook, SSE, proxy → Grid all responding).

🤖 Generated with Claude Code

pengying and others added 17 commits July 22, 2026 19:03
Rewires the passkey tap to gridSession.signIn (Task 5) instead of the
cosmetic ceremony, routing first-run EMAIL_OTP bootstrapping through the
existing AuthSheet OTP step and the WebAuthn create/get ceremonies through
the tap's own activation chain. Every real {request,response} envelope
is logged into the API panel via the new envelopeToApiCall (gridEntry.ts),
which replaces the synthesized stub whenever a real resBody is present.
Guards the sign-in call with an in-flight ref so a second tap landing
before `running` re-renders can't fire a second WebAuthn ceremony.

Also fixes next.config.mjs's webpack resolve.modules order: the absolute
project-root entry was shadowing @turnkey/crypto's own nested @noble/*
pin (a different, incompatible major version with different subpaths),
which broke `npm run build` once gridSession/gridCrypto were first
reachable from client code. The relative 'node_modules' entry now comes
first so the standard nested resolution wins, with the absolute path
only as a fallback.

Sandbox demo OTP autofill changed from 123456 to the magic code 000000
required by the live HPKE-encrypted verify.

Co-Authored-By: Claude Fable 5 <[email protected]>
…ent add, partial failure)

Co-Authored-By: Claude Fable 5 <[email protected]>
…und mints book balance only)

Co-Authored-By: Claude Fable 5 <[email protected]>
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
grid-wallet-demo Building Building Preview, Comment Jul 25, 2026 1:22am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Jul 25, 2026 1:22am

Request Review

pengying commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@pengying pengying changed the title feat(grid-wallet-prod): add vitest + gridUnits (USDB<->cents) feat(grid-wallet-prod): live Grid API — real passkey auth, funding, and signed cash-out (Phase 2) Jul 24, 2026
@pengying
pengying marked this pull request as ready for review July 24, 2026 18:07
@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the fully-scripted grid-wallet-prod demo with real Grid API calls end-to-end: passkey auth (EMAIL_OTP bootstrap → WebAuthn register → passkey sign-in), sandbox on-ramp funding (platform account fund → quote → execute → poll), and signed cash-out (quote → HPKE/TEK stamp → execute → poll). A same-origin Next.js proxy injects Basic auth server-side and echoes redacted envelopes to the API panel.

  • Server proxy (/api/grid/[...path]): allow-list enforced, Basic auth injected, {customerId} substituted; the unauthenticated GET /api/webhooks/events endpoint is the main pre-prod blocker.
  • Browser crypto (gridCrypto.ts): TEK keygen, HPKE encrypt/decrypt, Turnkey ApiKeyStamper — session keys never leave the client.
  • Money flow (gridFunding.ts, gridTransfer.ts, useWalletDemoLogic.ts): optimistic balance bridged with per-add settle callbacks; real balance refreshed after each terminal outcome.

Confidence Score: 3/5

Safe to merge for continued sandbox development; the unauthenticated /api/webhooks/events endpoint must be gated before any production key rotation.

The core money loop is well-structured and verified end-to-end. The webhook events reader ships completely open — any caller who knows the URL can read all real Grid event payloads without credentials. The PR description flags this as a known pre-prod follow-up, but the route is live in this diff and would expose live transaction data the moment real keys are swapped in. Two secondary concerns (CUSTOMER_ID empty-string fallback, caches not cleared on sign-out) are manageable in a single-customer sandbox but need cleanup before wider use.

src/app/api/webhooks/events/route.ts needs an auth gate before production keys are used. src/app/api/grid/[...path]/route.ts benefits from a startup guard on required env vars. src/lib/gridTransfer.ts and src/lib/gridFunding.ts should expose cache-clear functions called alongside clearSession().

Security Review

  • Unauthenticated event endpoint (/api/webhooks/events/route.ts): GET /api/webhooks/events has no auth gate; any caller can read real transaction event payloads (IDs, types, timestamps, raw data) from the server ring buffer.
  • No other injection or credential-leakage issues found: the proxy correctly redacts Authorization before echoing to the panel; Basic-auth credentials are server-side only; {customerId} substitution uses safe split/join (no regex injection surface); HPKE and session keys are generated and kept in the browser bundle, never sent to the server.

Important Files Changed

Filename Overview
components/grid-wallet-prod/src/app/api/webhooks/events/route.ts New GET endpoint returning all buffered webhook events with no authentication.
components/grid-wallet-prod/src/app/api/grid/[...path]/route.ts New server-side proxy with allow-list, Basic auth injection, and {customerId} substitution. Empty CUSTOMER_ID fallback produces malformed paths.
components/grid-wallet-prod/src/lib/gridSession.ts Real Grid auth: EMAIL_OTP bootstrap to passkey register to passkey sign-in. clearSession exported and called on reset/logout.
components/grid-wallet-prod/src/lib/gridTransfer.ts Quote creation, stamp/execute, and polling. Module-level externalAccountCache is never cleared on sign-out.
components/grid-wallet-prod/src/lib/gridCrypto.ts Browser crypto: TEK keygen, HPKE encryption, Turnkey API key stamping. lowS:false correctly accepts non-canonical signatures from Node crypto.
components/grid-wallet-prod/src/lib/webhookEvents.ts In-memory 50-event ring buffer. Works in a single Node.js process; will not share events across serverless instances.
components/grid-wallet-prod/src/hooks/useWalletDemoLogic.ts Major rework wiring real Grid API calls. refreshBalance omitted from onTransferExecute dep array due to TDZ; functionally correct because both share [logEnvelope] as dep.
components/grid-wallet-prod/src/app/api/grid/allowlist.ts Allow-list with anchored regexes and method enforcement. Redacts Authorization before echoing to panel. Correct and well-tested.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant Proxy as /api/grid proxy
    participant Grid as Grid API
    participant Webhook as /api/webhooks

    Note over Browser,Grid: First-run sign-in EMAIL_OTP to passkey registration
    Browser->>Proxy: GET /auth/credentials
    Proxy->>Grid: GET /auth/credentials Basic auth injected
    Grid-->>Browser: emailOtpId + passkeyId
    Browser->>Proxy: POST challenge
    Grid-->>Browser: otpEncryptionTargetBundle
    Browser->>Browser: HPKE-encrypt OTP plus TEK pubkey
    Browser->>Proxy: POST verify 202 leg
    Grid-->>Browser: payloadToSign + requestId
    Browser->>Browser: stamp TEK priv over payloadToSign
    Browser->>Proxy: POST verify plus Grid-Wallet-Signature
    Grid-->>Browser: session
    Browser->>Browser: navigator.credentials.create WebAuthn
    Browser->>Proxy: POST /auth/credentials passkey 202 leg
    Grid-->>Browser: payloadToSign
    Browser->>Browser: stamp session priv
    Browser->>Proxy: POST /auth/credentials plus Grid-Wallet-Signature
    Grid-->>Browser: passkeyAuthMethodId
    Note over Browser,Grid: Add money on-ramp
    Browser->>Proxy: GET /platform/internal-accounts
    Browser->>Proxy: POST /sandbox fund platform account
    Browser->>Proxy: POST /quotes platform to wallet
    Browser->>Proxy: POST execute no signature
    loop poll until COMPLETED
        Browser->>Proxy: GET /transactions/id
    end
    Note over Browser,Grid: Cash out
    Browser->>Proxy: POST /customers/external-accounts
    Browser->>Proxy: POST /quotes wallet to external
    Grid-->>Browser: payloadToSign
    Browser->>Browser: stamp session priv
    Browser->>Proxy: POST execute plus Grid-Wallet-Signature
    loop poll until COMPLETED
        Browser->>Proxy: GET /transactions/id
    end
    Note over Grid,Webhook: Webhooks
    Grid->>Webhook: POST /api/webhooks plus X-Grid-Signature
    Webhook->>Webhook: verifyGridSignature P-256
    Webhook->>Webhook: pushEvent ring buffer
    Browser->>Webhook: GET /api/webhooks/events unauthenticated
Loading
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
components/grid-wallet-prod/src/app/api/webhooks/events/route.ts:7-9
**Unauthenticated event-listing endpoint**

`GET /api/webhooks/events` returns the full ring buffer of received webhook events with no auth check. Every event payload contains real Grid API data — transaction IDs, event types, timestamps, and the raw `data` field. Any unauthenticated caller who discovers this URL can poll it indefinitely and read the complete event history for all transactions processed by this server instance. The PR description calls this out as a pre-prod follow-up, but the endpoint is live in this diff. A minimal fix before this goes near production keys is a `Bearer` or session-cookie check, or at minimum returning an empty array when no valid session header is present.

### Issue 2 of 4
components/grid-wallet-prod/src/app/api/grid/[...path]/route.ts:12
**Empty `CUSTOMER_ID` produces malformed Grid paths**

`GRID_CUSTOMER_ID ?? ''` defaults to an empty string when the env var is not set. `substituteCustomerId` then replaces every `{customerId}` token with `''`, producing paths like `/customers//internal-accounts`. The Grid API will return an error for these, but the failure will surface as a confusing upstream 404/400 with no hint that proxy misconfiguration is the root cause.

### Issue 3 of 4
components/grid-wallet-prod/src/lib/gridTransfer.ts:25
**`externalAccountCache` not cleared on sign-out**

`externalAccountCache` is a module-level `Map` that lives for the lifetime of the browser tab. `clearSession()` (called on reset and sign-out) resets session and account state, but this cache — and `platformUsdAccountId` in `gridFunding.ts` — are never invalidated. For the current single-customer sandbox this is benign, but if a second user signs in on the same tab, `ensureExternalAccount` will return a stale ExternalAccount ID from the previous session without hitting the network.

### Issue 4 of 4
components/grid-wallet-prod/src/lib/webhookEvents.ts:10-11
**In-process ring buffer won't survive multi-instance deployments**

`events` is module-level state in a Node.js route handler. With a single long-lived server process this works, but any deployment running multiple Next.js server instances (horizontal scaling, lambda rotation) will give each instance its own ring buffer — webhooks received by instance A are invisible to a browser polling instance B. Acceptable for the sandbox demo, worth flagging before this is reused under production load.

Reviews (1): Last reviewed commit: "fix(grid-wallet-prod): panel shows only ..." | Re-trigger Greptile

Comment on lines +7 to +9
export async function GET() {
return NextResponse.json({ events: listEvents() });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Unauthenticated event-listing endpoint

GET /api/webhooks/events returns the full ring buffer of received webhook events with no auth check. Every event payload contains real Grid API data — transaction IDs, event types, timestamps, and the raw data field. Any unauthenticated caller who discovers this URL can poll it indefinitely and read the complete event history for all transactions processed by this server instance. The PR description calls this out as a pre-prod follow-up, but the endpoint is live in this diff. A minimal fix before this goes near production keys is a Bearer or session-cookie check, or at minimum returning an empty array when no valid session header is present.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-prod/src/app/api/webhooks/events/route.ts
Line: 7-9

Comment:
**Unauthenticated event-listing endpoint**

`GET /api/webhooks/events` returns the full ring buffer of received webhook events with no auth check. Every event payload contains real Grid API data — transaction IDs, event types, timestamps, and the raw `data` field. Any unauthenticated caller who discovers this URL can poll it indefinitely and read the complete event history for all transactions processed by this server instance. The PR description calls this out as a pre-prod follow-up, but the endpoint is live in this diff. A minimal fix before this goes near production keys is a `Bearer` or session-cookie check, or at minimum returning an empty array when no valid session header is present.

How can I resolve this? If you propose a fix, please make it concise.

export const dynamic = 'force-dynamic';

const BASE = process.env.GRID_API_BASE_URL ?? 'https://api.lightspark.com/grid/2025-10-13';
const CUSTOMER_ID = process.env.GRID_CUSTOMER_ID ?? '';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Empty CUSTOMER_ID produces malformed Grid paths

GRID_CUSTOMER_ID ?? '' defaults to an empty string when the env var is not set. substituteCustomerId then replaces every {customerId} token with '', producing paths like /customers//internal-accounts. The Grid API will return an error for these, but the failure will surface as a confusing upstream 404/400 with no hint that proxy misconfiguration is the root cause.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-prod/src/app/api/grid/[...path]/route.ts
Line: 12

Comment:
**Empty `CUSTOMER_ID` produces malformed Grid paths**

`GRID_CUSTOMER_ID ?? ''` defaults to an empty string when the env var is not set. `substituteCustomerId` then replaces every `{customerId}` token with `''`, producing paths like `/customers//internal-accounts`. The Grid API will return an error for these, but the failure will surface as a confusing upstream 404/400 with no hint that proxy misconfiguration is the root cause.

How can I resolve this? If you propose a fix, please make it concise.

export function destSignature(input: ExternalAccountInput): string {
return input.kind === 'crypto'
? `crypto:${input.network}:${input.currency}:${input.address}`
: `bank:${input.currency}:${input.bankName}:${JSON.stringify(input.fields)}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 externalAccountCache not cleared on sign-out

externalAccountCache is a module-level Map that lives for the lifetime of the browser tab. clearSession() (called on reset and sign-out) resets session and account state, but this cache — and platformUsdAccountId in gridFunding.ts — are never invalidated. For the current single-customer sandbox this is benign, but if a second user signs in on the same tab, ensureExternalAccount will return a stale ExternalAccount ID from the previous session without hitting the network.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-prod/src/lib/gridTransfer.ts
Line: 25

Comment:
**`externalAccountCache` not cleared on sign-out**

`externalAccountCache` is a module-level `Map` that lives for the lifetime of the browser tab. `clearSession()` (called on reset and sign-out) resets session and account state, but this cache — and `platformUsdAccountId` in `gridFunding.ts` — are never invalidated. For the current single-customer sandbox this is benign, but if a second user signs in on the same tab, `ensureExternalAccount` will return a stale ExternalAccount ID from the previous session without hitting the network.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +10 to +11
let events: WebhookEvent[] = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 In-process ring buffer won't survive multi-instance deployments

events is module-level state in a Node.js route handler. With a single long-lived server process this works, but any deployment running multiple Next.js server instances (horizontal scaling, lambda rotation) will give each instance its own ring buffer — webhooks received by instance A are invisible to a browser polling instance B. Acceptable for the sandbox demo, worth flagging before this is reused under production load.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-prod/src/lib/webhookEvents.ts
Line: 10-11

Comment:
**In-process ring buffer won't survive multi-instance deployments**

`events` is module-level state in a Node.js route handler. With a single long-lived server process this works, but any deployment running multiple Next.js server instances (horizontal scaling, lambda rotation) will give each instance its own ring buffer — webhooks received by instance A are invisible to a browser polling instance B. Acceptable for the sandbox demo, worth flagging before this is reused under production load.

How can I resolve this? If you propose a fix, please make it concise.

…webhook-driven arrivals

Sign-in now starts on the credential a Global Account is actually born with, and
the funding flows use the endpoints Grid actually supports:

- EMAIL_OTP first (entry step prefilled from the credential's own nickname);
  a passkey is added later from the wallet as its own signed action — one device
  ceremony, authorized by the live session, no immediate re-authentication
- the sign-in CTA advertises the credential signIn will really use: passkeys are
  device-bound, so a server-listed passkey from another device no longer sends
  this browser into an assertion it can't satisfy
- Activity shows the account's real GET /transactions history (EXPIRED quotes
  filtered out); saved banks are seeded from GET /customers/external-accounts,
  and picking one quotes against that ExternalAccount instead of re-creating it
- Add money: a country's real deposit instructions (account/routing/rails/
  reference from the customer's own fiat account; placeholder EUR IBAN until an
  EUR account exists) OR "add an account" to pull from. A pull quote sources the
  ExternalAccount — Grid rejects /execute for it ("funds must be pushed"), so the
  quote is created and left pending; POST /sandbox/send stands in for the push
- Add from crypto: Base/Solana/Ethereum, network -> amount -> the real
  Grid-provisioned deposit address for that chain
- webhooks: X-Grid-Signature parsing accepts Grid's real {"v","s"} shape and a
  bare base64 header (the old {version,signature} assumption 401'd every real
  delivery); verified deliveries stream to the panel over SSE, and an arrival —
  not a tap — moves the balance, inserts the row and raises the toast
- bank picker limited to the US + euro area; balance hero shows available with
  the account total beneath; APY 3%
- API panel shows only real traffic: every synthesized call and response body is
  gone, including the fictional your-app.com webhook entry
- Dockerfile + .dockerignore (standalone output), and NEXT_PUBLIC_GRID_SANDBOX
  gating the sandbox-only "simulate funding" affordance
- scripts/ensure-business-customer.mjs: idempotent BUSINESS customer provisioning

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Pjqydq34z8DBK1kBrioXK2
@pengying
pengying merged commit 3179b16 into 07-21-feat_grid-wallet-prod_fork_grid-wallet-demo_as_the_production_demo_app Jul 25, 2026
7 of 8 checks passed
@pengying
pengying deleted the 07-22-feat_grid-wallet-prod_add_vitest_gridunits_usdb_-_cents_ branch July 25, 2026 01:31
mariano-macri added a commit that referenced this pull request Jul 27, 2026
The Dockerfile added in #740 has never produced an image. `npm ci` dies in the
deps stage: the seven @central-icons-react packages each run a postinstall
license-check.js that hard-fails unless CENTRAL_LICENSE_KEY is in the
environment, and that variable appears nowhere in this repo.

  npm error path /app/node_modules/@central-icons-react/round-filled-radius-2-stroke-2
  npm error command sh -c node ./license-check.js
  npm error Error: Central Icons license key is not set.

vercel.json for this same component already sets
`installCommand: npm install --ignore-scripts`, which is why the Vercel
deployments work and the container build never did. This aligns the Dockerfile
with the install behavior the component is actually developed and shipped
against. No dependency here needs a postinstall to produce a working build.

Verified linux/arm64 end to end: image builds (332MB), /app/server.js is the
entrypoint, NEXT_PUBLIC_GRID_SANDBOX inlines into the client bundle, the server
boots and serves / with 200 carrying no Grid credentials, and an unsigned
POST /api/webhooks is rejected 401 by the signature check.

Co-Authored-By: Claude Opus 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants