diff --git a/.claude/skills/grid-tutorial/README.md b/.claude/skills/grid-tutorial/README.md new file mode 100644 index 00000000..53027d03 --- /dev/null +++ b/.claude/skills/grid-tutorial/README.md @@ -0,0 +1,71 @@ +# grid-tutorial + +A hands-on Claude Code skill that takes a developer from zero to a running Next.js demo on Grid in about 10 minutes — making real sandbox API calls along the way and explaining each step. + +The skill scaffolds a small Next.js + TypeScript app into your working directory, walks you through either the **Payouts** flow (cross-border bank/wallet payouts) or the **Global Accounts** flow (embedded-wallet withdrawals), and pauses to explain each API call so you understand *why* not just *what*. + +## Install + +This skill ships with the [grid-api](https://github.com/lightsparkdev/grid-api) docs repo and is installed with [`npx skills`](https://github.com/vercel-labs/skills). The simplest path: + +```bash +# Install globally for Claude Code so it's available in any project +npx skills add lightsparkdev/grid-api --skill grid-tutorial -g -a claude-code +``` + +If you'd rather have it scoped to a specific project, drop the `-g` flag and run from inside that project's root. + +To install both Grid skills (this tutorial + the curl-based [`grid-api`](https://github.com/lightsparkdev/grid-api/tree/main/.claude/skills/grid-api) skill for one-off API calls) at once: + +```bash +npx skills add lightsparkdev/grid-api --skill '*' -g -a claude-code +``` + +## Use + +Start Claude Code in any directory and say: + +> walk me through Grid + +Or any of these: + +- `I want to try Grid for the first time` +- `Build me a Grid demo that sends USD to a Mexican CLABE account` +- `Show me how Grid Global Accounts work end-to-end` + +The skill takes over from there: asks you which flow to walk through, scaffolds the demo into a directory you choose (default `./grid-demo`), helps you wire up your sandbox credentials, and runs each step interactively. + +You'll need a Grid sandbox API token before you start — get one at [app.lightspark.com/grid/dashboard](https://app.lightspark.com/grid/dashboard). The skill walks you through saving it to `~/.grid-credentials` (the same location the [`grid-api`](https://github.com/lightsparkdev/grid-api/tree/main/.claude/skills/grid-api) skill uses). + +## What you end up with + +A `./grid-demo/` directory containing: + +- A Next.js 15 + React 19 app +- `lib/grid.ts` — the only place credentials are read; uses `@lightsparkdev/grid` (or plain `fetch` with HTTP Basic Auth) +- `app/api/*` — one route per Grid endpoint the tutorial touches +- `app/page.tsx` — a stepper UI that drives the API routes from the browser +- A standalone `README.md` so the demo makes sense weeks later when you come back to it + +…plus at least one real `Transaction` in your sandbox account that reached `COMPLETED`, and a working mental model of the Grid happy path. + +## What's inside the skill + +``` +grid-tutorial/ +├── SKILL.md # Workflow + routing + pacing rules +├── references/ +│ ├── credentials.md # Sandbox keys + env layout +│ ├── payouts.md # 8-step Payouts walkthrough +│ ├── global-accounts.md # Embedded-wallet branch +│ ├── account-type-cheatsheet.md # CLABE / IBAN / UPI / ACH shapes +│ ├── webhooks-followup.md # Optional webhooks add-on +│ └── troubleshooting.md # Common failure modes +└── assets/ + └── nextjs-template/ # The scaffold copied into your cwd +``` + +## Related + +- **[grid-api](https://github.com/lightsparkdev/grid-api/tree/main/.claude/skills/grid-api)** — sibling skill for one-off Grid API calls (curl-based). Use this once you've finished the tutorial and want to issue ad-hoc requests. +- **[Building with AI](https://grid.lightspark.com/platform-overview/building-with-ai)** — how the broader Grid + AI tooling fits together (MCP server, llms.txt, etc.). diff --git a/.claude/skills/grid-tutorial/SKILL.md b/.claude/skills/grid-tutorial/SKILL.md new file mode 100644 index 00000000..82f74d97 --- /dev/null +++ b/.claude/skills/grid-tutorial/SKILL.md @@ -0,0 +1,213 @@ +--- +name: grid-tutorial +description: > + Use this skill whenever a developer wants a hands-on, interactive walkthrough of the Grid API — + going from zero to a running demo on their machine making real sandbox API calls. + Trigger on phrases like "walk me through Grid", "Grid tutorial", "Grid quickstart", + "build a Grid demo app", "show me how Grid works", "I want to try Grid", + "first Grid integration", "getting started with Grid", "send my first Grid payment", + "build a Grid payment app", "scaffold a Grid app", "Grid hello world", + "learn the Grid API", or any request where the user wants to learn Grid by doing + rather than reading docs. The skill scaffolds a small Next.js demo into the user's + working directory, walks them step-by-step through Payouts or Global Accounts flows, + and runs real API calls live so they end up with a working app they understand. +allowed-tools: + - Bash + - Read + - Write + - Edit + - Grep + - Glob + - WebFetch +--- + +# Grid Tutorial Skill + +You are a hands-on tutor for the Grid API. The user is a developer who wants to learn +Grid by doing, not by reading. Your job is to deliver a "zero to working demo" experience +in their working directory: a real Next.js app, real sandbox API calls, real responses, +and step-by-step explanation of *why* each call matters — not just *what* it does. + +## What this skill produces + +By the end of the tutorial the user has: + +1. A working `./grid-demo/` Next.js app on their machine, running on `localhost:3000`. +2. At least one real `Transaction` in their Grid sandbox that reached `COMPLETED`. +3. A mental model of the Grid happy path (customer → KYC → fund → external account → quote → execute, **or** Global Accounts equivalent). +4. A clear pointer to the existing static docs and the sibling `grid-api` skill for follow-up work. + +## Core pacing rules — read carefully + +The single biggest failure mode of a tutorial is dumping all 8 steps at once. **Don't.** +The user is here to *learn*, which means they need short, observable cycles: + +- **Run one step at a time.** Make the API call, show the request body, show the response, *then explain what just happened and why this step is necessary*. +- **Do not pre-script later steps in the same tool call.** Even chaining with `&&` or stuffing multiple curl/fetch calls into a single Bash heredoc defeats the pacing — the user can't pause and ask questions between steps if you've already queued them all. +- **Pause for questions** between steps. After each step, ask something like "any questions, or shall I move to the next step?" — but only ask once. Don't be obsequious. +- **Reveal code, don't hide it.** Whenever a step touches a file in the scaffold, name the file and the function. Use `lib/grid.ts:12` style references so the user can navigate. +- **Sandbox failures are a feature.** If a call returns an error (expired quote, bad CLABE, etc.), treat it as a teaching moment — explain what went wrong, then retry. +- **Don't write production caveats inline.** Defer "in production you'd use webhooks, signature verification, ramped retries…" to the wrap-up. Keep the happy path clean. + +If the user explicitly says "just show me everything" or "skip the explanations," +collapse the pacing — but default to step-by-step. + +## Phase 0 — Prerequisites + +Before scaffolding anything, verify the environment can run a Next.js app and reach Grid. + +```bash +node -v # must be v20.x or v22.x — Mintlify and most Grid samples don't support 25+ +npm -v +which curl +``` + +If Node is missing or wrong version, stop and tell the user how to install it (e.g., `brew install node@22` on macOS) before continuing. **Do not try to upgrade Node yourself** — that's a destructive operation on the user's machine. + +## Phase 1 — Routing + +Use `AskUserQuestion` to gather two routing decisions at once. Do this *before* asking for credentials, so you can tailor the credential explanation to the chosen flow. + +Ask: + +1. **Which Grid flow do you want to learn?** + - **Payouts** — Send a payment from a Grid-managed account to an external bank/wallet (CLABE in Mexico, IBAN in Europe, UPI in India, ACH in the US, etc.). Most common Grid use case. + - **Global Accounts** — Embedded wallet flow with passkey-based signing. More moving parts, but the most differentiated Grid feature. +2. **Where should I scaffold the demo app?** (default: `./grid-demo`) + +If the user picks Payouts, also collect (you can ask now or defer to Phase 4): +- Customer type: `INDIVIDUAL` or `BUSINESS` +- Destination corridor: `USD→MXN/CLABE`, `USD→EUR/SEPA`, `USD→INR/UPI`, `USD→USD/ACH` + +Then load the matching reference file: + +- Payouts → read `references/payouts.md` +- Global Accounts → read `references/global-accounts.md` + +For external account field requirements per corridor, you will also need `references/account-type-cheatsheet.md`. For the credential setup, read `references/credentials.md`. + +## Phase 2 — Credentials + +Read `references/credentials.md` and follow it. The short version: + +1. Check whether `GRID_CLIENT_ID`, `GRID_CLIENT_SECRET`, `GRID_BASE_URL` are already set + in the user's shell or in `~/.grid-credentials`. +2. If not, walk them through getting a sandbox API token from the Grid dashboard and writing it to `~/.grid-credentials` (matching the pattern the sibling `grid-api` skill uses). +3. Sanity-check by hitting `GET /config` from the command line: + + ```bash + curl -s -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" "$GRID_BASE_URL/config" | jq . + ``` + + A 200 with currency configuration means the creds work. A 401 means stop and fix. + +## Phase 3 — Scaffold the demo app + +The skill ships a complete Next.js template at `/assets/nextjs-template/`. +Resolve `` to the absolute directory of the SKILL.md you just read +(do this yourself when you compose the command — there's no `$0` available in a +Claude Code Bash invocation): + +```bash +# Replace with the actual absolute path, e.g., +# /Users//Dev/grid-api/.claude/skills/grid-tutorial +cp -R /assets/nextjs-template ./grid-demo # adjust target path if user chose a different one +cd ./grid-demo +cp .env.local.example .env.local +``` + +Then write the user's credentials into `.env.local`: + +``` +GRID_CLIENT_ID= +GRID_CLIENT_SECRET= +GRID_BASE_URL=https://api.lightspark.com/grid/2025-10-13 +``` + +Install dependencies and start the dev server: + +```bash +npm install +npm run dev # run in background; opens on http://localhost:3000 +``` + +Tell the user what they should now see in their browser: a single-page UI with a stepper +that mirrors the API calls you're about to make. Their browser is the visualization layer; +the chat with you is the explanation layer. + +> **Important — security note worth saying out loud:** the Grid API key lives only on the +> server (Next.js API routes, `lib/grid.ts`). It is never sent to the browser. Show the +> user `lib/grid.ts` and explicitly point this out. This is non-negotiable in production +> and worth establishing early. + +## Phase 4 — Walk the flow + +Branch on the routing answer from Phase 1. + +### If Payouts + +Follow `references/payouts.md` end-to-end. The 8 steps in order: + +1. `POST /customers` — create the customer (file: `app/api/customers/route.ts`) +2. `GET /customers/kyc-link` — generate a hosted KYC link, have the user open it +3. `GET /customers/internal-accounts` — show the auto-provisioned per-currency accounts +4. `POST /sandbox/internal-accounts/{id}/fund` — fund the source account with $1000 +5. `POST /customers/external-accounts` — register the destination (CLABE/IBAN/UPI/ACH) +6. `POST /quotes` — create a locked quote with FX + fees +7. `POST /quotes/{id}/execute` — kick off the transfer +8. `GET /transactions/{id}` — poll until `COMPLETED` (no webhooks in v1 of the demo) + +For per-corridor required fields and example payloads, read `references/account-type-cheatsheet.md`. For the corresponding curl commands and error patterns, the sibling `grid-api` skill has authoritative examples. + +### If Global Accounts + +Follow `references/global-accounts.md` end-to-end. Sandbox magic values from the +Global Accounts walkthrough — published at + (append `.md` +for an LLM-friendly version) — make passkey/OTP/wallet signatures trivial to demo +without real WebAuthn. + +## Phase 5 — Wrap-up and follow-ups + +Once a transaction is `COMPLETED`, recap the user's mental model in 4-6 lines: +"You just created a customer, funded it in sandbox, locked an FX rate, and executed a +transfer. Here's what's still abstracted away: webhooks, idempotency, beneficiary +verification edge cases, KYC review, production credential rotation." + +Then offer concrete next steps via `AskUserQuestion`: + +- **Add webhooks?** Load `references/webhooks-followup.md` and add `app/api/webhooks/route.ts`. +- **Try the other flow?** (Payouts ↔ Global Accounts) — re-enter Phase 4 with the other branch. +- **One-off API calls?** Point at the sibling `grid-api` skill — it's curl-based and built + for "send this payment" / "check that balance" requests. +- **Switch to production?** Briefly explain prod creds, the same base URL, and the + Mintlify docs at `https://grid.lightspark.com/`. + +## Troubleshooting + +When something breaks, check `references/troubleshooting.md` first — it has the common +failure modes (401, 409 quote expired, KYC pending, port 3000 in use, Node version). + +For deeper API questions not covered by the references, use `WebFetch` on: + +- `https://grid.lightspark.com/llms.txt` — concise LLM-optimized overview. +- `https://grid.lightspark.com/llms-full.txt` — full docs. +- `https://raw.githubusercontent.com/lightsparkdev/grid-api/refs/heads/main/openapi.yaml` — full schema. + +## Reference files index + +| File | When to read | +| --- | --- | +| `references/credentials.md` | Phase 2 — getting creds, env setup | +| `references/payouts.md` | Phase 4 — Payouts branch | +| `references/global-accounts.md` | Phase 4 — Global Accounts branch | +| `references/account-type-cheatsheet.md` | Phase 4 — corridor field requirements | +| `references/webhooks-followup.md` | Phase 5 — only if user asks about webhooks | +| `references/troubleshooting.md` | Anytime something fails | + +## Bundled scaffold + +The skill ships `assets/nextjs-template/`. Treat it as authoritative — when the user +asks "where does X happen in code?", point at the corresponding file inside the scaffold. +Keep your verbal explanation aligned with what's actually in the template; if you find +a discrepancy while teaching, fix the template, don't lie to the user. diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/.env.local.example b/.claude/skills/grid-tutorial/assets/nextjs-template/.env.local.example new file mode 100644 index 00000000..649b8c3a --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/.env.local.example @@ -0,0 +1,5 @@ +# Grid sandbox credentials. Copy this file to .env.local and fill in values. +# Never commit .env.local — it contains your secret. +GRID_CLIENT_ID= +GRID_CLIENT_SECRET= +GRID_BASE_URL=https://api.lightspark.com/grid/2025-10-13 diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/.gitignore b/.claude/skills/grid-tutorial/assets/nextjs-template/.gitignore new file mode 100644 index 00000000..91066b44 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/.gitignore @@ -0,0 +1,6 @@ +node_modules +.next +.env* +!.env.local.example +*.tsbuildinfo +.DS_Store diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/README.md b/.claude/skills/grid-tutorial/assets/nextjs-template/README.md new file mode 100644 index 00000000..966f9876 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/README.md @@ -0,0 +1,79 @@ +# Grid demo + +A minimal Next.js demo of the Grid API. Scaffolded by the `grid-tutorial` skill — but +fully self-contained, so you can come back to it weeks later without the skill loaded. + +## What it does + +A single page that walks an end-to-end Grid payout in 8 steps: + +1. Create a customer +2. Generate a hosted KYC link +3. List the customer's auto-provisioned internal accounts +4. Fund the source account via the sandbox faucet +5. Register an external destination account +6. Create a locked-rate quote +7. Execute the quote +8. Poll the resulting transaction to completion + +Each step calls a Next.js API route (under `app/api/`), which calls Grid via the +`@lightsparkdev/grid` SDK from `lib/grid.ts`. The Grid API key never leaves the server. + +## Setup + +Requires Node 20 or 22. + +```bash +cp .env.local.example .env.local +# edit .env.local — paste your sandbox GRID_CLIENT_ID and GRID_CLIENT_SECRET +npm install +npm run dev +``` + +Open `http://localhost:3000`. + +## Where things live + +``` +app/ + page.tsx One-page UI; one button per step. + layout.tsx App shell. + globals.css Minimal styles. + api/ + config/ GET /config — connectivity sanity check. + customers/ POST /customers (and /kyc-link sub-route). + internal-accounts/ GET /customers/internal-accounts. + sandbox-fund/ POST /sandbox/internal-accounts/{id}/fund. + external-accounts/ POST /customers/external-accounts. + quotes/ POST /quotes (and /execute sub-route). + transactions/ GET /transactions/{id}. +lib/ + grid.ts Constructs the Grid SDK client from env vars. +``` + +## Next steps + +- Add a webhook receiver: see `references/webhooks-followup.md` in the skill. +- Switch to production: replace the sandbox creds with prod creds. Same base URL. +- Try the other flow (Global Accounts): re-run the skill, pick "Global Accounts". +- Read the published docs: . + +## Share the skill that built this + +Teammates can install the same `grid-tutorial` skill in one command: + +```bash +npx skills add lightsparkdev/grid-api --skill grid-tutorial -g -a claude-code +``` + +Then in Claude Code: *"walk me through Grid"* or *"I want to try Grid for the first time"*. + +To install both Grid skills (this tutorial + the curl-based `grid-api` skill for one-off API calls): + +```bash +npx skills add lightsparkdev/grid-api --skill '*' -g -a claude-code +``` + +## License + +MIT — this is starter code; modify freely. diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/config/route.ts b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/config/route.ts new file mode 100644 index 00000000..ee592f5c --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/config/route.ts @@ -0,0 +1,9 @@ +// Tutorial step 0 (sanity check): GET /config +// Confirms credentials work and returns supported currencies / platform settings. +import { NextResponse } from "next/server"; +import { gridFetch } from "@/lib/grid"; + +export async function GET() { + const result = await gridFetch("/config"); + return NextResponse.json(result.data, { status: result.status }); +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/customers/kyc-link/route.ts b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/customers/kyc-link/route.ts new file mode 100644 index 00000000..b1d37979 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/customers/kyc-link/route.ts @@ -0,0 +1,14 @@ +// Tutorial step 2: GET /customers/kyc-link +// Returns a single-use hosted KYC URL. Customer opens it; sandbox auto-approves. +// Customer.status flips from PENDING to ACTIVE when verification completes. +import { NextRequest, NextResponse } from "next/server"; +import { gridFetch } from "@/lib/grid"; + +export async function GET(req: NextRequest) { + const platformCustomerId = req.nextUrl.searchParams.get("platformCustomerId") ?? undefined; + const redirectUri = req.nextUrl.searchParams.get("redirectUri") ?? undefined; + const result = await gridFetch("/customers/kyc-link", { + query: { platformCustomerId, redirectUri }, + }); + return NextResponse.json(result.data, { status: result.status }); +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/customers/route.ts b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/customers/route.ts new file mode 100644 index 00000000..8f07bf80 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/customers/route.ts @@ -0,0 +1,12 @@ +// Tutorial step 1: POST /customers +// Creates a customer (INDIVIDUAL or BUSINESS). Grid auto-provisions one internal +// account per supported currency. Save the returned customer.id — every subsequent +// step needs it. See references/payouts.md step 1 for the "why". +import { NextRequest, NextResponse } from "next/server"; +import { gridFetch } from "@/lib/grid"; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const result = await gridFetch("/customers", { method: "POST", body }); + return NextResponse.json(result.data, { status: result.status }); +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/external-accounts/route.ts b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/external-accounts/route.ts new file mode 100644 index 00000000..0bdff466 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/external-accounts/route.ts @@ -0,0 +1,11 @@ +// Tutorial step 5: POST /customers/external-accounts +// Registers a destination bank/wallet/UPI/CLABE account for the customer. +// Per-corridor field shapes live in references/account-type-cheatsheet.md. +import { NextRequest, NextResponse } from "next/server"; +import { gridFetch } from "@/lib/grid"; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const result = await gridFetch("/customers/external-accounts", { method: "POST", body }); + return NextResponse.json(result.data, { status: result.status }); +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/internal-accounts/route.ts b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/internal-accounts/route.ts new file mode 100644 index 00000000..a524b2d8 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/internal-accounts/route.ts @@ -0,0 +1,18 @@ +// Tutorial step 3: list internal accounts. +// Routes to the right Grid endpoint based on the query: +// - Global Accounts (Embedded Wallet): GET /internal-accounts?customerId=...&type=EMBEDDED_WALLET +// - Payouts and everything else: GET /customers/internal-accounts?customerId=... +// All client query params are forwarded so the caller can pass currency filters etc. +import { NextRequest, NextResponse } from "next/server"; +import { gridFetch } from "@/lib/grid"; + +export async function GET(req: NextRequest) { + const params = req.nextUrl.searchParams; + const query: Record = {}; + for (const [k, v] of params.entries()) query[k] = v; + + const path = query.type === "EMBEDDED_WALLET" ? "/internal-accounts" : "/customers/internal-accounts"; + + const result = await gridFetch(path, { query }); + return NextResponse.json(result.data, { status: result.status }); +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/quotes/execute/route.ts b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/quotes/execute/route.ts new file mode 100644 index 00000000..5222a569 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/quotes/execute/route.ts @@ -0,0 +1,26 @@ +// Tutorial step 7: POST /quotes/{quoteId}/execute +// Executes a previously-created quote. Returns a Transaction in PROCESSING; the +// transfer settles asynchronously (poll step 8 for completion). +// +// For Global Accounts withdrawals, pass `walletSignature` in the body — it is +// forwarded as the `Grid-Wallet-Signature` header. In sandbox the magic value +// "sandbox-valid-signature" is accepted (see references/global-accounts.md). +import { NextRequest, NextResponse } from "next/server"; +import { gridFetch } from "@/lib/grid"; + +export async function POST(req: NextRequest) { + const { quoteId, walletSignature, idempotencyKey } = await req.json(); + if (!quoteId) { + return NextResponse.json({ error: "quoteId required" }, { status: 400 }); + } + + const headers: Record = {}; + if (walletSignature) headers["Grid-Wallet-Signature"] = walletSignature; + if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey; + + const result = await gridFetch(`/quotes/${encodeURIComponent(quoteId)}/execute`, { + method: "POST", + headers: Object.keys(headers).length ? headers : undefined, + }); + return NextResponse.json(result.data, { status: result.status }); +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/quotes/route.ts b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/quotes/route.ts new file mode 100644 index 00000000..c2d973b6 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/quotes/route.ts @@ -0,0 +1,11 @@ +// Tutorial step 6: POST /quotes +// Creates a locked-rate quote. Always include `currency` in the destination +// object — it's the most common 400. Quotes expire in ~5 minutes. +import { NextRequest, NextResponse } from "next/server"; +import { gridFetch } from "@/lib/grid"; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const result = await gridFetch("/quotes", { method: "POST", body }); + return NextResponse.json(result.data, { status: result.status }); +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/sandbox-fund/route.ts b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/sandbox-fund/route.ts new file mode 100644 index 00000000..88e86d07 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/sandbox-fund/route.ts @@ -0,0 +1,17 @@ +// Tutorial step 4: POST /sandbox/internal-accounts/{accountId}/fund +// Sandbox-only faucet. Body: { amount } in smallest currency units (cents). +// In production this is replaced by a real ACH/wire/crypto deposit + INCOMING_PAYMENT webhook. +import { NextRequest, NextResponse } from "next/server"; +import { gridFetch } from "@/lib/grid"; + +export async function POST(req: NextRequest) { + const { accountId, amount } = await req.json(); + if (!accountId || typeof amount !== "number") { + return NextResponse.json({ error: "accountId and numeric amount required" }, { status: 400 }); + } + const result = await gridFetch(`/sandbox/internal-accounts/${encodeURIComponent(accountId)}/fund`, { + method: "POST", + body: { amount }, + }); + return NextResponse.json(result.data, { status: result.status }); +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/transactions/route.ts b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/transactions/route.ts new file mode 100644 index 00000000..67b3858c --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/api/transactions/route.ts @@ -0,0 +1,14 @@ +// Tutorial step 8: GET /transactions/{transactionId} +// Polled until status is COMPLETED or FAILED. In production you'd consume the +// OUTGOING_PAYMENT webhook instead of polling — see references/webhooks-followup.md. +import { NextRequest, NextResponse } from "next/server"; +import { gridFetch } from "@/lib/grid"; + +export async function GET(req: NextRequest) { + const id = req.nextUrl.searchParams.get("id"); + if (!id) { + return NextResponse.json({ error: "id required" }, { status: 400 }); + } + const result = await gridFetch(`/transactions/${encodeURIComponent(id)}`); + return NextResponse.json(result.data, { status: result.status }); +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/globals.css b/.claude/skills/grid-tutorial/assets/nextjs-template/app/globals.css new file mode 100644 index 00000000..5796ac31 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/globals.css @@ -0,0 +1,119 @@ +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + font-size: 14px; + color: #111; + background: #f7f7f8; +} + +main { + max-width: 880px; + margin: 0 auto; + padding: 32px 24px 64px; +} + +h1 { + font-size: 22px; + margin: 0 0 6px; +} + +h2 { + font-size: 15px; + margin: 0 0 12px; + color: #333; +} + +p.lede { + color: #555; + margin: 0 0 24px; +} + +.step { + background: #fff; + border: 1px solid #e3e3e6; + border-radius: 8px; + padding: 16px 20px; + margin-bottom: 14px; +} + +.step header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 10px; +} + +.step .title { + font-weight: 600; + font-size: 14px; +} + +.step .endpoint { + color: #6a6a72; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 12px; +} + +button { + background: #111; + color: #fff; + border: 0; + border-radius: 6px; + padding: 7px 14px; + font-size: 13px; + cursor: pointer; +} + +button:disabled { + background: #c4c4c8; + cursor: not-allowed; +} + +input, +select, +textarea { + border: 1px solid #d4d4d8; + border-radius: 6px; + padding: 6px 10px; + font-size: 13px; + font-family: inherit; + width: 100%; +} + +label { + display: block; + font-size: 12px; + font-weight: 600; + color: #444; + margin: 8px 0 4px; +} + +pre { + background: #0e0e10; + color: #e6e6e9; + padding: 12px 14px; + border-radius: 6px; + font-size: 12px; + overflow-x: auto; + margin: 10px 0 0; + max-height: 240px; +} + +.error { + color: #b00020; + margin-top: 10px; + font-size: 12px; +} + +.row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/layout.tsx b/.claude/skills/grid-tutorial/assets/nextjs-template/app/layout.tsx new file mode 100644 index 00000000..64b71955 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Grid demo", + description: "Hands-on Grid API tutorial", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/app/page.tsx b/.claude/skills/grid-tutorial/assets/nextjs-template/app/page.tsx new file mode 100644 index 00000000..7354c041 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/app/page.tsx @@ -0,0 +1,304 @@ +"use client"; + +import { useEffect, useState } from "react"; + +type StepResult = { status?: number; body?: unknown; error?: string }; + +const STORAGE_KEY = "grid-demo-state"; + +type DemoState = { + platformCustomerId: string; + customerId: string; + internalAccountId: string; + externalAccountId: string; + quoteId: string; + transactionId: string; +}; + +const emptyState: DemoState = { + platformCustomerId: "", + customerId: "", + internalAccountId: "", + externalAccountId: "", + quoteId: "", + transactionId: "", +}; + +function useDemoState() { + const [state, setState] = useState(emptyState); + + useEffect(() => { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) { + try { + setState({ ...emptyState, ...JSON.parse(raw) }); + } catch {} + } + }, []); + + const update = (patch: Partial) => { + setState((prev) => { + const next = { ...prev, ...patch }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); + return next; + }); + }; + + const reset = () => { + localStorage.removeItem(STORAGE_KEY); + setState(emptyState); + }; + + return { state, update, reset }; +} + +function Step({ + n, + title, + endpoint, + children, + result, +}: { + n: number; + title: string; + endpoint: string; + children: React.ReactNode; + result?: StepResult; +}) { + return ( +
+
+
+
Step {n} — {title}
+
{endpoint}
+
+
+ {children} + {result && ( + <> + {result.error &&
{result.error}
} + {result.body !== undefined && ( +
{JSON.stringify(result.body, null, 2)}
+ )} + + )} +
+ ); +} + +async function call(path: string, init?: RequestInit): Promise { + try { + const res = await fetch(path, { + ...init, + headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) }, + }); + const body = await res.json().catch(() => ({})); + return res.ok + ? { status: res.status, body } + : { status: res.status, body, error: `HTTP ${res.status}` }; + } catch (err: unknown) { + return { error: err instanceof Error ? err.message : String(err) }; + } +} + +export default function Home() { + const { state, update, reset } = useDemoState(); + const [results, setResults] = useState>({}); + + // Form state for sub-fields the UI needs to collect. + const [fullName, setFullName] = useState("Ada Lovelace"); + const [clabe, setClabe] = useState("002010000000000001"); + const [sendCents, setSendCents] = useState(50000); + + const setResult = (key: string, value: StepResult) => + setResults((prev) => ({ ...prev, [key]: value })); + + return ( +
+

Grid demo

+

+ Walk through the Payouts happy path. Each step calls a Next.js API route + in app/api/, which proxies to Grid via lib/grid.ts. +

+ + + + + + + + setFullName(e.target.value)} /> + + + + + + + + + + + + + + + + + + setClabe(e.target.value)} /> + + + + + + setSendCents(Number(e.target.value) || 0)} + /> + + + + + + + + + + + +
+ +
+
+ ); +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/lib/grid.ts b/.claude/skills/grid-tutorial/assets/nextjs-template/lib/grid.ts new file mode 100644 index 00000000..7834d411 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/lib/grid.ts @@ -0,0 +1,61 @@ +/** + * Grid client setup. + * + * This file is the only place credentials are read. API routes call `gridFetch()` + * to talk to Grid; the function adds HTTP Basic Auth and resolves the base URL. + * + * The official @lightsparkdev/grid SDK is also installed as a dependency. Once + * comfortable with the HTTP shape, you can swap routes over to the SDK + * (`import LightsparkGrid from '@lightsparkdev/grid'`) for typed call signatures. + * + * Note: this uses Node's `Buffer`. Routes default to the Node runtime, which is + * what we want — do not add `export const runtime = "edge"` to the API routes + * unless you also swap `Buffer.from(...).toString("base64")` for `btoa(...)`. + */ + +const required = (name: "GRID_CLIENT_ID" | "GRID_CLIENT_SECRET" | "GRID_BASE_URL") => { + const value = process.env[name]; + if (!value) { + throw new Error( + `Missing env var ${name}. Copy .env.local.example to .env.local and fill it in, then restart 'npm run dev'.`, + ); + } + return value; +}; + +export type GridFetchOptions = { + method?: "GET" | "POST" | "PATCH" | "DELETE"; + body?: unknown; + query?: Record; + headers?: Record; +}; + +export async function gridFetch( + path: string, + { method = "GET", body, query, headers }: GridFetchOptions = {}, +): Promise<{ status: number; data: T | { code?: string; message?: string } }> { + const baseUrl = required("GRID_BASE_URL"); + const auth = Buffer.from(`${required("GRID_CLIENT_ID")}:${required("GRID_CLIENT_SECRET")}`).toString("base64"); + + const url = new URL(`${baseUrl}${path}`); + if (query) { + for (const [k, v] of Object.entries(query)) { + if (v !== undefined) url.searchParams.set(k, String(v)); + } + } + + const res = await fetch(url, { + method, + headers: { + Authorization: `Basic ${auth}`, + "Content-Type": "application/json", + Accept: "application/json", + ...(headers ?? {}), + }, + body: body === undefined ? undefined : JSON.stringify(body), + cache: "no-store", + }); + + const data = (await res.json().catch(() => ({}))) as T | { code?: string; message?: string }; + return { status: res.status, data }; +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/next.config.mjs b/.claude/skills/grid-tutorial/assets/nextjs-template/next.config.mjs new file mode 100644 index 00000000..d5456a15 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +}; + +export default nextConfig; diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/package.json b/.claude/skills/grid-tutorial/assets/nextjs-template/package.json new file mode 100644 index 00000000..f234e1d2 --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/package.json @@ -0,0 +1,25 @@ +{ + "name": "grid-demo", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@lightsparkdev/grid": "^1.7.0", + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.6.0" + }, + "engines": { + "node": ">=20 <25" + } +} diff --git a/.claude/skills/grid-tutorial/assets/nextjs-template/tsconfig.json b/.claude/skills/grid-tutorial/assets/nextjs-template/tsconfig.json new file mode 100644 index 00000000..8c3a42db --- /dev/null +++ b/.claude/skills/grid-tutorial/assets/nextjs-template/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/.claude/skills/grid-tutorial/evals/evals.json b/.claude/skills/grid-tutorial/evals/evals.json new file mode 100644 index 00000000..a9fc9de2 --- /dev/null +++ b/.claude/skills/grid-tutorial/evals/evals.json @@ -0,0 +1,26 @@ +{ + "skill_name": "grid-tutorial", + "evals": [ + { + "id": 1, + "name": "first-time-grid", + "prompt": "I want to try Grid for the first time. Can you walk me through it from zero? I'm a TypeScript dev and learn best by building something I can run.", + "expected_output": "Skill triggers, explains what it'll do, asks routing questions (Payouts vs Global Accounts) via AskUserQuestion, walks through prereq checks, and sets up to scaffold a Next.js demo. Should NOT dump all 8 steps at once.", + "files": [] + }, + { + "id": 2, + "name": "usd-to-mxn-payout", + "prompt": "Build me a quick Grid demo that sends USD to a Mexican CLABE account. I want to see actual sandbox API calls happen.", + "expected_output": "Skill triggers, routes to Payouts branch (no need to ask the flow question — corridor is implied), confirms the user's destination as USD->MXN/CLABE, scaffolds the Next.js template, walks through customer creation -> KYC -> fund -> external account (CLABE) -> quote -> execute. References account-type-cheatsheet.md for CLABE format.", + "files": [] + }, + { + "id": 3, + "name": "global-accounts-walkthrough", + "prompt": "Show me how Grid Global Accounts work end-to-end. I'm comfortable with TypeScript and want to understand the embedded wallet flow.", + "expected_output": "Skill triggers, routes to Global Accounts branch via AskUserQuestion or directly (user phrasing makes the choice clear), explains sandbox magic values for passkey/OTP, walks through customer creation, account inspection, sandbox-mocked passkey registration, fund, external account, quote with session signing, execute. References references/global-accounts.md.", + "files": [] + } + ] +} diff --git a/.claude/skills/grid-tutorial/references/account-type-cheatsheet.md b/.claude/skills/grid-tutorial/references/account-type-cheatsheet.md new file mode 100644 index 00000000..6150bb2a --- /dev/null +++ b/.claude/skills/grid-tutorial/references/account-type-cheatsheet.md @@ -0,0 +1,142 @@ +# Per-corridor external account cheatsheet + +The minimum fields to register an external account for the four corridors the +tutorial covers. Use these exact shapes — the SDK will reject extra/missing fields. + +For all 27 supported countries with their full schemas, the sibling `grid-api` skill's +`references/account-types.md` is authoritative. This file is a focused subset. + +## USD → MXN (CLABE / SPEI) + +```json +{ + "customerId": "", + "currency": "MXN", + "accountInfo": { + "accountType": "MXN_ACCOUNT", + "paymentRails": ["SPEI"], + "clabeNumber": "002010000000000001", + "beneficiary": { + "beneficiaryType": "INDIVIDUAL", + "fullName": "Beneficiary Name", + "birthDate": "1990-01-15", + "nationality": "MX" + } + } +} +``` + +CLABE is exactly **18 digits**. The last digits act as a sandbox magic suffix: + +| Last 3 digits | Behavior | +| --- | --- | +| `001` (or other) | Success | +| `002` | Fails — insufficient funds | +| `003` | Fails — account closed | +| `004` | Fails — bank rejects | +| `005` | Delays ~30s then fails | +| `1xx` | Triggers beneficiary name verification scenarios | + +For a tutorial happy path, use a CLABE ending in `001`. + +## USD → EUR (SEPA Instant) + +```json +{ + "customerId": "", + "currency": "EUR", + "accountInfo": { + "accountType": "EUR_ACCOUNT", + "paymentRails": ["SEPA_INSTANT"], + "iban": "DE89370400440532013000", + "beneficiary": { + "beneficiaryType": "INDIVIDUAL", + "fullName": "Beneficiary Name", + "birthDate": "1990-01-15", + "nationality": "DE" + } + } +} +``` + +`bic` is optional for SEPA Instant — Grid derives it from the IBAN. + +## USD → INR (UPI) + +```json +{ + "customerId": "", + "currency": "INR", + "accountInfo": { + "accountType": "INR_ACCOUNT", + "paymentRails": ["UPI"], + "vpa": "beneficiary@bank", + "beneficiary": { + "beneficiaryType": "INDIVIDUAL", + "fullName": "Beneficiary Name", + "birthDate": "1990-01-15", + "nationality": "IN" + } + } +} +``` + +UPI VPA looks like an email. Standard sandbox values work; use `success@razorpay` +for a deterministic-success path. + +## USD → USD (ACH) + +```json +{ + "customerId": "", + "currency": "USD", + "accountInfo": { + "accountType": "USD_ACCOUNT", + "paymentRails": ["ACH"], + "routingNumber": "021000021", + "accountNumber": "000123456789", + "accountSubtype": "CHECKING", + "beneficiary": { + "beneficiaryType": "INDIVIDUAL", + "fullName": "Beneficiary Name", + "birthDate": "1990-01-15", + "nationality": "US" + } + } +} +``` + +ACH is "instant" in sandbox but in production settles next business day. If your +tutorial user wants to demo a faster real rail, swap to RTP / FedNow once those are +in their platform config. + +## Business beneficiary variant + +For a `BUSINESS` customer or a business beneficiary, replace the `beneficiary` block +with: + +```json +{ + "beneficiaryType": "BUSINESS", + "businessInfo": { + "legalName": "Acme Corp", + "entityType": "LLC", + "taxId": "12-3456789", + "registrationCountry": "US" + } +} +``` + +Country-specific tax-ID formats apply (RFC for Mexico, CNPJ for Brazil, etc.). The +`grid-api` skill's `references/account-types.md` has the full matrix. + +## Common pitfalls + +1. **Always include `currency`** in the top-level body. Even though `accountType` + implies it, omitting it is the most common 400. +2. **`paymentRails` is an array.** Pass `["SPEI"]` not `"SPEI"`. +3. **`beneficiary.fullName` is required** for INDIVIDUAL beneficiaries. `birthDate` + and `nationality` are optional but strongly recommended (some corridors require + them — Brazil / Nigeria). +4. **CLABE is 18 digits, not 16 or 20.** Pad-left with `0`s if the user's input is + shorter (and double-check before sending). diff --git a/.claude/skills/grid-tutorial/references/credentials.md b/.claude/skills/grid-tutorial/references/credentials.md new file mode 100644 index 00000000..8d427de8 --- /dev/null +++ b/.claude/skills/grid-tutorial/references/credentials.md @@ -0,0 +1,97 @@ +# Credentials & environment setup + +This is a reference file for **Phase 2** of the tutorial. It explains how to obtain +sandbox credentials and where to put them so the demo app can authenticate. + +## What Grid auth looks like + +Grid uses **HTTP Basic Auth**. There is no OAuth, no JWT, no refresh tokens. You get +two strings from the dashboard — a token ID and a client secret — and you send them +on every request as `Authorization: Basic base64(id:secret)`. + +The `@lightsparkdev/grid` SDK takes them as `username` and `password`: + +```ts +import LightsparkGrid from "@lightsparkdev/grid"; + +const client = new LightsparkGrid({ + username: process.env.GRID_CLIENT_ID, + password: process.env.GRID_CLIENT_SECRET, +}); +``` + +## Required environment variables + +The demo template reads three env vars from `.env.local`: + +| Var | Example | Notes | +| --- | --- | --- | +| `GRID_CLIENT_ID` | `ApiTokenId:0195...` | The token ID from the Grid dashboard. Public-ish. | +| `GRID_CLIENT_SECRET` | `2SKx...` | The secret. Treat like a password. Never in the browser. | +| `GRID_BASE_URL` | `https://api.lightspark.com/grid/2025-10-13` | Same URL for sandbox and prod — the credentials determine which. | + +These are the same names the sibling `grid-api` skill uses, so users with that skill +already configured can reuse `~/.grid-credentials`. + +## Step-by-step: getting sandbox credentials + +1. **Sign in** to the Grid dashboard at `https://app.lightspark.com/grid/dashboard` + (or the sandbox dashboard if the user has a separate sandbox account). +2. **Switch to Sandbox** — top-right environment switcher. +3. **API tokens** → **Create token**. Give it a name like "tutorial". +4. **Copy both values immediately** — the secret is shown once. If lost, create a new token. +5. **Paste into the demo's `.env.local`** (created from `.env.local.example` during scaffold). + +If the user already has `~/.grid-credentials` from the `grid-api` skill, read it: + +```bash +GRID_CLIENT_ID=$(jq -r '.clientId // .apiTokenId' ~/.grid-credentials) +GRID_CLIENT_SECRET=$(jq -r '.clientSecret // .apiClientSecret' ~/.grid-credentials) +GRID_BASE_URL=$(jq -r '.baseUrl // "https://api.lightspark.com/grid/2025-10-13"' ~/.grid-credentials) +``` + +Then echo them into `.env.local` (use `cat < .env.local` rather than `echo` to +avoid escaping issues, and don't print the secret to the terminal). + +## Sanity check + +Before scaffolding more, confirm the creds work end-to-end: + +```bash +curl -s -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" "$GRID_BASE_URL/config" | jq . +``` + +A successful response looks like: + +```json +{ + "supportedCurrencies": [...], + "platformCustomerId": "...", + "umaDomain": "..." +} +``` + +If you get a `401`, the credentials are wrong — re-check that you copied both halves +and that the secret didn't get truncated. If you get a connection error, check that +`GRID_BASE_URL` is reachable. + +## Security guardrails to teach the user + +These are non-negotiable and worth saying out loud while you scaffold: + +1. **The secret never goes in the browser.** It lives in `.env.local` (gitignored) and + is read only inside Next.js API routes. The user's React components must never + import it directly. +2. **Don't commit `.env.local`.** The template's `.gitignore` already excludes it, but + confirm. +3. **Sandbox credentials are not safe to share.** They can move test funds and create + test customers — small blast radius, but still. +4. **Rotation is cheap.** If they leak a secret, just create a new token in the dashboard + and revoke the old one (`DELETE /tokens/{tokenId}`). No downtime. + +## Pointer + +Authoritative auth spec: +(append `.md` to the URL for an LLM-friendly version, or read locally at +`mintlify/api-reference/authentication.mdx` if you're inside a clone of the +[grid-api](https://github.com/lightsparkdev/grid-api) repo). diff --git a/.claude/skills/grid-tutorial/references/global-accounts.md b/.claude/skills/grid-tutorial/references/global-accounts.md new file mode 100644 index 00000000..3be40461 --- /dev/null +++ b/.claude/skills/grid-tutorial/references/global-accounts.md @@ -0,0 +1,160 @@ +# Global Accounts walkthrough + +This is the **Global Accounts branch** for Phase 4. Global Accounts is Grid's +embedded-wallet flow: customer-controlled accounts authenticated by passkey, OTP, or +external wallet signature, with end-to-end-encrypted session data. + +For a tutorial, the goal is to get a working withdrawal flow end-to-end without +making the user implement WebAuthn, HPKE, or session signing themselves. We use the +**sandbox magic values** documented at + and + (append `.md` to either +URL for an LLM-friendly version) to short-circuit those complex steps. + +Same pacing rules as `payouts.md`: one step at a time, surface response, explain why, +ask before continuing. + +## Sandbox magic values you'll use + +| Step | Field | Sandbox value | What it bypasses | +| --- | --- | --- | --- | +| Email OTP | `otp` | `"000000"` | Sending a real email | +| Passkey signature | `signature` | `"sandbox-valid-passkey-signature"` | Real WebAuthn ceremony | +| OAuth/OIDC | `oidcToken` | `"sandbox-valid-oidc-token"` | Real OIDC provider round-trip | +| Wallet signature | `Grid-Wallet-Signature` header | `"sandbox-valid-signature"` | Real on-chain signature | + +In production these are replaced by actual cryptographic verifications. Make this clear +to the user — we are demoing the *flow*, not the security primitives. + +## Step 1 — Create the customer + +**File:** `app/api/customers/route.ts` +**Endpoint:** `POST /customers` + +Same as the Payouts branch (Phase 4 step 1). Save the customer ID. + +**Why this step exists.** Global Accounts attaches to a customer just like payout +accounts do. The customer is still the canonical identity. + +## Step 2 — Inspect the auto-provisioned Global Account + +**File:** `app/api/internal-accounts/route.ts` +**Endpoint:** `GET /internal-accounts?customerId=...&type=EMBEDDED_WALLET` + +When the request includes `type=EMBEDDED_WALLET`, the route forwards to the +Global-Accounts-specific `/internal-accounts` endpoint instead of `/customers/internal-accounts`. +Always include the `type` filter for Global Accounts — without it, the route returns +the Payouts-style internal accounts and the Global Account is missing from the result. + +For Global Accounts customers the internal account also exposes embedded-wallet +metadata: `accountStatus`, supported auth methods, the encryption public key for +session payloads, etc. + +**Why this step exists.** Unlike payouts, the customer (not the platform) is the +beneficial owner of these funds. The auth metadata is what your frontend uses to drive +the passkey/OTP/wallet UI. + +## Step 3 — Register a passkey (sandbox-shortcut) + +In production this is a full WebAuthn registration. In sandbox we POST a synthesized +attestation with the magic signature. The exact endpoint depends on the auth flow, +but for the tutorial use the registration endpoint documented at +. + +**Why this step exists.** Without a registered authenticator, the customer can't +authorize withdrawals. Real WebAuthn ties the keypair to the device's secure enclave; +we're skipping that ceremony. + +**Pacing tip.** This is a good moment to point at the existing snippet — the user can +read the production version once they finish the happy path. + +## Step 4 — Fund the Global Account + +**File:** `app/api/sandbox-fund/route.ts` +**Endpoint:** `POST /sandbox/internal-accounts/{accountId}/fund` + +Body: `{ "amount": 100000 }` (= $1,000). + +**Why this step exists.** Same as payouts step 4 — without funds the withdrawal in +step 7 fails. In production this would be a real deposit (ACH, wire, or crypto deposit +to the auto-provisioned funding instructions). + +## Step 5 — Add an external destination account + +**File:** `app/api/external-accounts/route.ts` +**Endpoint:** `POST /customers/external-accounts` + +Same pattern as payouts step 5. Pick any corridor — for Global Accounts demos USD→USD +ACH is the simplest because there's no FX to explain. + +**Why this step exists.** Global Accounts customers withdraw to their own external +bank or wallet. The external account record stores the rail-specific fields so future +withdrawals only need the account ID. + +## Step 6 — Create a withdrawal quote + +**File:** `app/api/quotes/route.ts` +**Endpoint:** `POST /quotes` + +Body shape is identical to payouts step 6. The customer's internal account is the +source; the external account from step 5 is the destination. + +**Why this step exists.** Even for same-currency withdrawals, the quote captures fees +and creates a stable reference for the executor — and is what the customer signs in +step 7. + +## Step 7 — Sign the session and execute + +In production: the frontend bundles the quote details, encrypts a session token with +the customer's authenticator, and submits the signed request. In sandbox: we send the +quote ID + magic wallet signature and Grid accepts it. + +**File:** `app/api/quotes/execute/route.ts` +**Endpoint:** `POST /quotes/{quoteId}/execute` + +The route's POST body accepts `walletSignature` (forwarded as the +`Grid-Wallet-Signature` header) and an optional `idempotencyKey`. For sandbox, send: + +```json +{ + "quoteId": "", + "walletSignature": "sandbox-valid-signature" +} +``` + +In production, replace `walletSignature` with the actual ECDSA signature over the +quote's `payloadToSign` (see the embedded-wallet walkthrough snippet for HPKE + +P-256 details). + +**Why this step exists.** The customer — not the platform — must authorize the move. +This is the critical Global Accounts security property: the platform can never spend +the customer's funds without an authenticated request from the customer's authenticator. + +## Step 8 — Poll the transaction + +**File:** `app/api/transactions/route.ts` +**Endpoint:** `GET /transactions/{transactionId}` + +Same as payouts step 8. + +## After step 8 — recap script + +Suggested recap: + +> You just walked through a Global Accounts withdrawal: created a customer-controlled +> account, registered a passkey (sandbox-mocked), funded it, added an external +> destination, priced a quote, signed it with the customer's authenticator, and saw +> the transfer settle. The single biggest difference from Payouts: the *customer* +> authorized the move, not your platform — that's the embedded-wallet model. + +Then offer the wrap-up follow-ups from `SKILL.md`. + +## Authoritative deeper docs + +- — full production walkthrough including HPKE encryption, real WebAuthn flow, OAuth/OIDC variants. +- — sandbox magic values reference. +- — Global Accounts docs section index. + +Append `.md` to any of these URLs for an LLM-friendly version. If you're inside a +clone of the [grid-api](https://github.com/lightsparkdev/grid-api) repo, the same +content is in `mintlify/global-accounts/` and `mintlify/snippets/global-accounts/`. diff --git a/.claude/skills/grid-tutorial/references/payouts.md b/.claude/skills/grid-tutorial/references/payouts.md new file mode 100644 index 00000000..31c5aac4 --- /dev/null +++ b/.claude/skills/grid-tutorial/references/payouts.md @@ -0,0 +1,247 @@ +# Payouts walkthrough + +This is the **Payouts branch** for Phase 4. Follow it sequentially. Each step: + +1. Says which scaffold file wraps the API call. +2. Shows the request the SDK builds. +3. Shows what to expect back. +4. Explains *why* the step exists — the part the user is here to learn. + +Do not run all 8 steps in a single tool call. The whole point of the tutorial is the +pause-and-explain rhythm. After each step, surface the response in the chat and ask if +the user wants to continue or dig in. + +## Sub-routing — pick a corridor + +Before step 1, ask the user (use `AskUserQuestion`): + +| Customer type | Destination corridor | Required external-account fields | +| --- | --- | --- | +| `INDIVIDUAL` or `BUSINESS` | `USD → MXN` (CLABE / SPEI) | 18-digit CLABE | +| | `USD → EUR` (SEPA Instant) | IBAN | +| | `USD → INR` (UPI) | UPI VPA (e.g., `name@bank`) | +| | `USD → USD` (ACH) | routing + account | + +For exact field shapes per corridor, read `references/account-type-cheatsheet.md`. For +deeper field-by-field details with all 27 supported countries, the sibling `grid-api` +skill's `references/account-types.md` is authoritative. + +## Step 1 — Create the customer + +**File:** `app/api/customers/route.ts` +**Endpoint:** `POST /customers` + +Request body: + +```json +{ + "platformCustomerId": "tutorial-", + "customerType": "INDIVIDUAL", + "region": "US", + "email": "ada@example.com", + "fullName": "Ada Lovelace", + "birthDate": "1815-12-10", + "nationality": "GB" +} +``` + +`region` and `email` are required for Global-Accounts-enabled platforms and are +recommended everywhere — including them keeps the same body working across both +flows. For `BUSINESS`, swap individual fields for `businessInfo` (see the cheatsheet). + +**Why this step exists.** The customer is the canonical identity in Grid. Every +account, every quote, every transaction hangs off a customer. When the SDK creates +one, Grid auto-provisions internal accounts in each currency the platform supports — +that's why we don't need a separate "create account" step before funding. + +**What to surface.** The response includes `id` (e.g., `Customer:0195...`) and a `status` +of `PENDING` (KYC not yet complete). Save the ID — every subsequent step needs it. + +## Step 2 — Generate a hosted KYC link + +**File:** `app/api/customers/kyc-link/route.ts` +**Endpoint:** `GET /customers/kyc-link?platformCustomerId=...&redirectUri=...` + +The response includes a single-use URL like `https://kyc.grid.lightspark.com/start/...`. + +**What the user does.** Open the URL in their browser, walk through the sandbox-flavored +KYC form (sandbox auto-approves with safe defaults). When they finish, the customer +status flips to `ACTIVE`. + +**Why this step exists.** Real money requires real identity verification. Grid offers +two KYC paths: hosted (a Lightspark-rendered form, what we're using) and API-based +(upload documents yourself via `POST /verifications` + `POST /documents`). For a +tutorial, hosted is *much* faster. + +**Pacing tip.** Wait for the user to confirm they finished the KYC flow before +moving on. You can poll `GET /customers/{id}` until `status === "ACTIVE"`, but in +sandbox the flip is usually within a few seconds. + +## Step 3 — Inspect the auto-provisioned internal account + +**File:** `app/api/internal-accounts/route.ts` +**Endpoint:** `GET /customers/internal-accounts?customerId=...` + +The response is a `data` array of accounts, one per currency. For our tutorial we want +the `USD` one. Save its `id` (e.g., `InternalAccount:0195...`). + +**Why this step exists.** This is the moment to clear up a confusion that trips up +nearly every new Grid integrator: *internal accounts hold balances; external accounts +are destinations*. They are different objects with different schemas. We're going to +fund this internal account, then send out to an external account in step 5. + +The response also includes `fundingPaymentInstructions` — bank wire details, ACH info, +crypto deposit addresses. In production, you'd display these to the customer so they +can fund the account from their own bank. In sandbox, we'll skip ahead with a faucet. + +## Step 4 — Fund the internal account (sandbox only) + +**File:** `app/api/sandbox-fund/route.ts` +**Endpoint:** `POST /sandbox/internal-accounts/{accountId}/fund` + +Request body: + +```json +{ "amount": 100000 } +``` + +That's `$1,000` in cents. Grid's amount fields are always in the smallest currency +unit (cents for USD, satoshis for BTC, etc.). + +**Why this step exists.** Without funds, the next step (`POST /quotes`) fails with +`INSUFFICIENT_BALANCE`. The faucet is sandbox-only — in production this is real money +arriving via ACH/wire/crypto deposit, with a webhook (`INCOMING_PAYMENT`) signalling +the deposit cleared. + +**What to surface.** Re-trigger Step 3 (the same "list internal accounts" button on +the page) to refresh the balance — there's no separate "show balance" UI. This makes +the cause-and-effect concrete: I called the faucet, the balance went up. + +## Step 5 — Register the external destination account + +**File:** `app/api/external-accounts/route.ts` +**Endpoint:** `POST /customers/external-accounts` + +Body for USD → MXN (CLABE): + +```json +{ + "customerId": "", + "currency": "MXN", + "accountInfo": { + "accountType": "MXN_ACCOUNT", + "paymentRails": ["SPEI"], + "clabeNumber": "002010000000000001", + "beneficiary": { + "beneficiaryType": "INDIVIDUAL", + "fullName": "Beneficiary Name", + "birthDate": "1990-01-15", + "nationality": "MX" + } + } +} +``` + +For other corridors, swap `accountType` / `paymentRails` / the rail-specific field +(`iban` for SEPA, `vpa` for UPI, `routingNumber` + `accountNumber` for ACH). See +`references/account-type-cheatsheet.md`. + +**Sandbox tip.** The CLABE number's last digit triggers a deterministic outcome: +`...001` succeeds, `...002` fails for insufficient funds, `...003` fails for closed +account. See for the +full table (append `.md` for an LLM-friendly version). Use a "good" suffix during +the tutorial; come back later to demo failures. + +**Why this step exists.** Grid keeps a registry of beneficiaries per customer. This is +where rail-specific fields (CLABE, IBAN, etc.) get validated. Storing them once makes +subsequent payments much simpler — quotes just reference the external account ID. + +## Step 6 — Create the quote + +**File:** `app/api/quotes/route.ts` +**Endpoint:** `POST /quotes` + +Body: + +```json +{ + "source": { + "sourceType": "ACCOUNT", + "accountId": "" + }, + "destination": { + "destinationType": "ACCOUNT", + "accountId": "", + "currency": "MXN" + }, + "lockedCurrencyAmount": 50000, + "lockedCurrencySide": "SENDING" +} +``` + +`50000` cents = $500.00 sending. Grid replies with the receiving amount in MXN, the FX +rate, and the fees. The quote is valid for ~5 minutes — the response includes +`expiresAt`. **Always include `currency` in the destination object even though the +external account already has one** — this is the most common 400 error in the +tutorial. + +**Why this step exists.** A quote is a *priced commitment*. It locks the FX rate and +fees so the user can confirm the trade before money moves. Without quotes, you'd be +guessing what the recipient receives. + +**What to surface.** Show the rate breakdown: +- `sentAmount` — the source-currency amount that leaves the internal account. +- `receivedAmount` — what lands in the destination, in destination currency units. +- `exchangeRate` — units of destination per unit of source. +- `fees.fixed` + any percentage fees. +- `expiresAt` — emphasize the 5-minute window before step 7. + +## Step 7 — Execute the quote + +**File:** `app/api/quotes/execute/route.ts` +**Endpoint:** `POST /quotes/{quoteId}/execute` + +No body required. + +**Why this step exists.** The quote was a commitment; execute kicks off the actual +transfer. Grid debits the internal account, talks to the destination rail, and returns +a `Transaction` object with `status: PROCESSING`. The transfer settles asynchronously. + +**What can go wrong here.** `QUOTE_EXPIRED` is the #1 error — if it's been more than 5 +minutes since step 6, repeat step 6. Other failures show up later via `Transaction.status`. + +## Step 8 — Poll the transaction to completion + +**File:** `app/api/transactions/route.ts` +**Endpoint:** `GET /transactions/{transactionId}` + +Poll every 2 seconds until `status` is `COMPLETED` or `FAILED`. In sandbox this is +usually within 5-10 seconds. + +**Why polling instead of webhooks.** In production you'd handle this with a webhook +(`OUTGOING_PAYMENT` event) — the demo skips that for v1 to avoid needing a tunnel. +If the user wants to add webhooks at the wrap-up, load `references/webhooks-followup.md`. + +**What to surface.** Final transaction object with `sentAmount`, `receivedAmount`, +the exchange rate that actually applied, and any per-step trace info. This is the +"you just sent money" moment — celebrate it, then recap what they built. + +## After step 8 — recap script + +Suggested 5-line wrap-up to deliver in chat: + +> You just (1) created a Grid customer, (2) walked through hosted KYC, (3) viewed the +> auto-provisioned internal account, (4) funded it via the sandbox faucet, (5) added a +> beneficiary external account, (6) priced a quote with a locked FX rate, (7) executed +> the transfer, and (8) saw it land. Eight API calls, ~3 minutes of real time. The same +> code works in production with a real-money credential. + +Then jump to the wrap-up follow-ups in `SKILL.md`. + +## Authoritative deeper docs + +- — narrative version of this flow. +- — sandbox magic suffix table. +- Sibling [`grid-api`](https://github.com/lightsparkdev/grid-api/tree/main/.claude/skills/grid-api) skill — for one-off API calls and per-corridor field reference. + +Append `.md` to either URL for an LLM-friendly version. diff --git a/.claude/skills/grid-tutorial/references/troubleshooting.md b/.claude/skills/grid-tutorial/references/troubleshooting.md new file mode 100644 index 00000000..bfe64ee9 --- /dev/null +++ b/.claude/skills/grid-tutorial/references/troubleshooting.md @@ -0,0 +1,127 @@ +# Troubleshooting + +Common failure modes during the tutorial and how to handle them. Read this any time +something breaks before you go fishing in the OpenAPI spec. + +## 401 Unauthorized + +**Symptom:** Any API call returns `401`. Often happens immediately after pasting creds. + +**Causes & fixes:** + +1. Secret was truncated when copied (the dashboard shows it once — easy to miss the trailing characters). +2. `.env.local` not picked up — Next.js only reads it at server startup. Restart `npm run dev`. +3. Used the wrong env vars (e.g., `GRID_API_TOKEN_ID` instead of `GRID_CLIENT_ID`). The template uses `GRID_CLIENT_ID` / `GRID_CLIENT_SECRET`. +4. Mixed sandbox creds with prod base URL or vice-versa. Same URL works for both, but the credential pair must match the environment they were created in. + +Quick verification: + +```bash +curl -s -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" "$GRID_BASE_URL/config" -o /dev/null -w "%{http_code}\n" +``` + +`200` → creds work. `401` → fix above. `000`/timeout → network issue. + +## 400 Bad Request on `POST /quotes` + +**Symptom:** Quote creation rejected. + +**Common causes:** + +1. **Missing `currency` in the destination object.** Even though the external account already declares its currency, the quote body needs it explicitly. Most common Payouts step 6 error. +2. `lockedCurrencyAmount` not in smallest units (cents/sats). `100` for USD = $1.00, not $100. +3. `lockedCurrencySide` typo'd — must be `"SENDING"` or `"RECEIVING"`, exact case. +4. Internal account doesn't have enough balance. Check via `GET /customers/internal-accounts` after step 4. + +## `QUOTE_EXPIRED` on execute + +**Symptom:** `POST /quotes/{id}/execute` returns 4xx with `code: "QUOTE_EXPIRED"`. + +**Fix:** Quotes live ~5 minutes. Re-run step 6 (create a new quote) and step 7 quickly. + +If the user was reading explanations slowly between steps 6 and 7, this is normal — +no need to apologize for it; just re-run. + +## Customer status stuck on `PENDING` + +**Symptom:** Tutorial step 2 (KYC) never flips the customer to `ACTIVE`. + +**Fixes:** + +1. Confirm the user actually opened the hosted KYC URL and walked through the form. The link is single-use; if dismissed, generate a new one with `GET /customers/kyc-link`. +2. Sandbox occasionally takes 10-30 seconds to flip status. Refresh `GET /customers/{id}` a few times. +3. If still pending, the platform config might require manual approval — check the dashboard's verifications panel. + +## `INSUFFICIENT_BALANCE` + +**Symptom:** Step 7 (execute) fails with insufficient balance even though step 4 funded the account. + +**Causes:** + +1. Quote source was the wrong internal account. Each customer has a per-currency account; make sure the source ID matches the *funded* one. +2. Step 4 hit a sandbox failure path because the CLABE/destination has a "fail" suffix. Double-check the response body of step 4. + +## `POST /sandbox/internal-accounts/.../fund` returns 404 + +**Symptom:** Sandbox funding endpoint not found. + +**Cause:** The credentials are pointing at production, not sandbox. Sandbox endpoints don't exist in prod. Verify with `GET /config` — the response shape and contents differ between envs (sandbox typically has a `sandbox: true` flag or includes test currencies). + +## `EADDRINUSE` on `npm run dev` + +**Symptom:** "Port 3000 is already in use." + +**Fixes:** + +1. Find the process: `lsof -ti:3000 | xargs kill -9` (kills it). +2. Or use a different port: `PORT=3001 npm run dev`. + +## Node version errors during `npm install` + +**Symptom:** `engine "node"` warnings or hard failures. + +**Cause:** Node 25+ isn't supported by the template (matches the repo's CLAUDE.md note for the same reason — Mintlify breaks too). Use Node 20 or 22: + +```bash +brew install node@22 +export PATH="/opt/homebrew/opt/node@22/bin:$PATH" +``` + +## Browser shows the old version after edits + +**Symptom:** Edited a route, but the browser still hits the old code. + +**Fix:** Next.js dev hot-reloads automatically — but only if `npm run dev` is still +running and the file is inside the project. Hard refresh the browser (`Cmd+Shift+R`). +If still stuck, restart `npm run dev`. + +## CORS / fetch errors from the browser + +**Symptom:** Browser console shows CORS errors when the page tries to call API routes. + +**Cause:** This shouldn't happen because the page and API routes share the same +origin. If it does, the user is probably hitting the API routes from a different port +or origin. Confirm they're at `http://localhost:3000` and not `http://0.0.0.0:3000` or similar. + +## When in doubt — read the response body + +Every Grid error includes a `code` and `message` in the JSON body. Don't just look at +the HTTP status — log the body: + +```ts +catch (err: unknown) { + if (err instanceof Error) console.error(err.message); + else console.error(JSON.stringify(err)); +} +``` + +The SDK's error classes also expose `.code`, `.message`, and the raw HTTP response — +useful for distinguishing transient (retry) from permanent (fix input) errors. + +## Escalation + +If none of the above helps: + +1. Re-fetch the OpenAPI spec — endpoint shapes can drift between SDK versions: `https://raw.githubusercontent.com/lightsparkdev/grid-api/refs/heads/main/openapi.yaml`. +2. Check the published docs for that endpoint at `https://grid.lightspark.com/api-reference/`. +3. Ask the user to share the full failing curl or SDK call (with the secret redacted), and reproduce against the sibling `grid-api` skill's curl examples to isolate whether the bug is in the demo template or the user's input. diff --git a/.claude/skills/grid-tutorial/references/webhooks-followup.md b/.claude/skills/grid-tutorial/references/webhooks-followup.md new file mode 100644 index 00000000..0984fdaf --- /dev/null +++ b/.claude/skills/grid-tutorial/references/webhooks-followup.md @@ -0,0 +1,108 @@ +# Webhooks follow-up + +Load this only when the user, after finishing the happy-path tutorial, asks something +like "how do webhooks work?" / "let's add webhooks" / "what about real-time updates?". + +The v1 tutorial polls `GET /transactions/{id}` to track payment status. Production +integrations don't poll — Grid pushes status changes via webhooks. This add-on +upgrades the demo. + +## What changes + +1. Add a webhook receiver route to the demo: `app/api/webhooks/route.ts`. +2. Configure Grid's platform to point at the receiver. +3. Locally, use a tunnel (`cloudflared`/`ngrok`) so Grid can reach `localhost:3000`. +4. Verify the `X-Grid-Signature` header on every incoming webhook. + +## Step 1 — Add the receiver + +Create `app/api/webhooks/route.ts`: + +```ts +import { NextRequest, NextResponse } from "next/server"; +import crypto from "node:crypto"; + +export async function POST(req: NextRequest) { + const rawBody = await req.text(); + const signature = req.headers.get("x-grid-signature") ?? ""; + const secret = process.env.GRID_WEBHOOK_SECRET ?? ""; + + const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); + if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { + return NextResponse.json({ error: "bad signature" }, { status: 401 }); + } + + const event = JSON.parse(rawBody); + console.log("[webhook]", event.type, event.id); + return NextResponse.json({ ok: true }); +} +``` + +Tell the user: `console.log` is fine for the tutorial. In production this is where you'd +update your database, fan out to subscribers, etc. *Always* respond `2xx` quickly — +Grid retries on `5xx` and timeouts. + +## Step 2 — Get a webhook signing secret + +The signing secret comes from the Grid dashboard's webhook configuration, not from +the API token. Add it to `.env.local`: + +``` +GRID_WEBHOOK_SECRET= +``` + +## Step 3 — Set up a tunnel + +Local dev servers aren't reachable from Grid's infra. Use a tunnel: + +```bash +brew install cloudflare/cloudflare/cloudflared # one-time +cloudflared tunnel --url http://localhost:3000 # prints a public URL +``` + +Or use ngrok if the user prefers. + +## Step 4 — Tell Grid where to send webhooks + +```bash +curl -s -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -X PATCH -H "Content-Type: application/json" \ + -d "{\"webhookEndpoint\": \"https:///api/webhooks\"}" \ + "$GRID_BASE_URL/config" | jq . +``` + +## Step 5 — Test it + +Trigger a synthetic webhook from sandbox: + +```bash +curl -s -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -X POST "$GRID_BASE_URL/sandbox/webhooks/test" | jq . +``` + +The receiver should log the event. Then re-run the payouts flow — the +`OUTGOING_PAYMENT` webhook should fire when the transfer completes, replacing the +need to poll. + +## Events worth handling + +| Event | When it fires | What to do | +| --- | --- | --- | +| `INCOMING_PAYMENT` | Funds arrive in an internal account | Update balance display | +| `OUTGOING_PAYMENT` | Outgoing transfer settles | Mark transaction as final | +| `CUSTOMER_STATUS_UPDATED` | KYC approved/rejected | Unblock or notify customer | +| `VERIFICATION_STATUS_UPDATED` | Document verification result | Prompt for re-upload if rejected | +| `INTERNAL_ACCOUNT_STATUS_UPDATED` | Balance/lock state changes | UI refresh | + +## Authoritative reference + +- — full webhook spec with all event types and signature verification details (append `.md` for an LLM-friendly version). + +## Production considerations to flag + +- **Idempotency**: Grid retries on failure. Use the event `id` to dedupe. +- **Ordering is not guaranteed**: don't assume `INCOMING_PAYMENT` arrives before + `INTERNAL_ACCOUNT_STATUS_UPDATED`. +- **Use a queue**: process webhooks asynchronously so a slow handler doesn't tip Grid + into retry-storm territory. +- **Rotate the signing secret** if it leaks; the dashboard supports this. diff --git a/.gitignore b/.gitignore index 39e0d77f..7c463092 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ scripts/export-icons.js # Superset .superset/ + +# Skill eval workspaces (local artifacts from skill-creator iteration runs) +.claude/skills/*-workspace/ diff --git a/README.md b/README.md index 94a76559..8bd25547 100644 --- a/README.md +++ b/README.md @@ -891,29 +891,55 @@ npm run lint # Or: make lint ``` -## Claude Code Skill +## Claude Code Skills -This repository includes a [Claude Code](https://claude.ai/code) skill for interacting with the Grid API. The skill enables Claude to execute API operations, answer documentation questions, and guide you through payment workflows. +This repository ships two [Claude Code](https://claude.ai/code) skills for working with the Grid API. Both install via [`npx skills`](https://github.com/vercel-labs/skills): -### Setup - -Before using the skill, build the CLI tool: ```bash -cd cli && npm install && npm run build && cd .. +# Install both skills globally for Claude Code +npx skills add lightsparkdev/grid-api --skill '*' -g -a claude-code ``` -Then configure credentials (see Configuration section below). +### grid-api — execute API calls + +Curl-based skill for sending payments, managing customers, creating quotes, and answering questions about Grid endpoints. Best once you know your way around the API. -### Usage +```bash +npx skills add lightsparkdev/grid-api --skill grid-api -g -a claude-code +``` -When using Claude Code in this repository, invoke the skill with `/grid-api` or ask questions like: +Invoke it with `/grid-api` or ask things like: - "Send a payment to Mexico" - "Create a quote from USDC to INR" - "What currencies does Grid support?" - "Help me set up a UPI account" -The skill uses the CLI at `cli/dist/index.js` to execute operations against the Grid API. +#### Setup + +Before using the skill, build the CLI tool: +```bash +cd cli && npm install && npm run build && cd .. +``` + +Then configure credentials (see Configuration section below). + +### grid-tutorial — learn by building + +Hands-on, interactive tutorial that scaffolds a small Next.js + TypeScript demo into your working directory and walks you through the Grid happy path with real sandbox API calls. About 10 minutes, end-to-end. Best if you've never used Grid. + +```bash +npx skills add lightsparkdev/grid-api --skill grid-tutorial -g -a claude-code +``` + +Then in Claude Code, say something like: + +- "Walk me through Grid" +- "I want to try Grid for the first time" +- "Build me a Grid demo that sends USD to a Mexican CLABE account" +- "Show me how Grid Global Accounts work end-to-end" + +See `.claude/skills/grid-tutorial/README.md` for details. ### Configuration diff --git a/mintlify/platform-overview/building-with-ai.mdx b/mintlify/platform-overview/building-with-ai.mdx index 9c78403a..75ba6e3b 100644 --- a/mintlify/platform-overview/building-with-ai.mdx +++ b/mintlify/platform-overview/building-with-ai.mdx @@ -105,6 +105,18 @@ It'll prompt you for your API Token ID and Client Secret, validate them, and sav Start in the sandbox environment to experiment safely. The Skill is great at generating fake account data to help you test different flows. +## Walk through your first Grid integration + +If you'd rather learn by building, the Grid Tutorial skill takes you from zero to a running Next.js demo making real sandbox API calls — about 10 minutes, end-to-end. Install it the same way as the API skill: + +```bash +npx skills add lightsparkdev/grid-api --skill grid-tutorial -g -a claude-code +``` + +Then start Claude Code in any directory and say something like *"walk me through Grid"* or *"build me a Grid demo that sends USD to a Mexican CLABE account."* The skill picks the right flow (Payouts or Global Accounts), scaffolds a Next.js + TypeScript app into a directory you choose, and walks you through each API call interactively — pausing to explain *why* each step matters. + +You can install both Grid skills at once with `--skill '*'`. The tutorial is the right starting point if you've never used Grid; the API skill is the right tool once you know your way around and want to issue ad-hoc requests. + ## MCP server Grid provides a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that gives AI agents access to Grid's documentation, so your agentic workflows have full context of the Grid API during implementation.