Skip to content

Add Turnkey USDB offramp instructions#426

Open
akanter wants to merge 2 commits intomainfrom
04-30-add_turnkey_usdb_offramp_instructions
Open

Add Turnkey USDB offramp instructions#426
akanter wants to merge 2 commits intomainfrom
04-30-add_turnkey_usdb_offramp_instructions

Conversation

@akanter
Copy link
Copy Markdown

@akanter akanter commented Apr 30, 2026

TL;DR

Adds a scripts/ directory with a step-by-step offramp guide and a Node.js signing helper for the USDB embedded wallet → USD bank flow, and updates Claude skill/project documentation to reference them.

What changed?

scripts/README.md — A complete walkthrough of the USDB embedded wallet offramp flow covering:

  • Customer onboarding (create, KYC approval, find internal accounts, bootstrap the embedded wallet via EMAIL_OTP credential registration, add a destination bank)
  • On-ramp (platform USD → customer USDB quote and execute)
  • Off-ramp (ephemeral keypair generation, OTP issuance, OTP verification + HPKE bundle decrypt, offramp quote creation, Turnkey stamp construction, and quote execution with Grid-Wallet-Signature)
  • A troubleshooting table covering common errors like to_network INTERNAL_FUNDED_FIAT does not support USDB, expired OTPs, and insufficient funds

scripts/embedded-wallet-sign.js — A Node.js CLI with three subcommands that handle the cryptographic operations not expressible in plain curl:

  • gen-keypair — generates an ephemeral P-256 keypair (pubHex/privHex) to supply as clientPublicKey to /auth/credentials/{id}/verify
  • decrypt-bundle — HPKE-opens the encryptedSessionSigningKey from the verify response using @turnkey/crypto
  • stamp — builds a Turnkey API stamp over a payloadToSign using @turnkey/api-key-stamper, producing the value for the Grid-Wallet-Signature header

scripts/package.json / package-lock.json — Package manifest with @turnkey/api-key-stamper and @turnkey/crypto as dependencies.

CLAUDE.md and .claude/skills/grid-api/SKILL.md — Updated to point Claude at scripts/README.md whenever tasks involve Turnkey signing, offramp, Grid-Wallet-Signature, HPKE bundle decryption, or the EMAIL_OTP credential flow. Also documents the bootstrapping gotcha: the Turnkey sub-org and Spark network wallet aren't provisioned until an EMAIL_OTP credential is registered, so this must happen before the first on-ramp quote.

How to test?

  1. cd scripts && npm install
  2. Set GRID_BASE_URL, GRID_CLIENT_ID, and GRID_CLIENT_SECRET
  3. Follow scripts/README.md end-to-end: create a customer, register an EMAIL_OTP credential against the USDB account, run an on-ramp quote, then execute the offramp using the three embedded-wallet-sign.js subcommands (gen-keypairdecrypt-bundlestamp)
  4. Confirm the transaction reaches COMPLETED and the destination bank receives USD

Why make this change?

The USDB embedded wallet offramp requires two cryptographic operations (HPKE decryption and Turnkey API stamp construction) that cannot be performed with curl alone. Without tooling and documentation for these steps, integrators have no clear path to complete the flow. The bootstrapping requirement (registering an EMAIL_OTP credential before the first quote) is also a non-obvious gotcha that causes confusing failures. This PR provides both the reference documentation and the minimal Node.js tooling needed to drive the full flow end-to-end.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

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

Project Deployment Actions Updated (UTC)
grid-flow-builder Ready Ready Preview, Comment May 1, 2026 0:37am

Request Review

@akanter akanter marked this pull request as ready for review April 30, 2026 21:29
Copy link
Copy Markdown
Author

akanter commented Apr 30, 2026

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

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 30, 2026

Greptile Summary

This PR adds a complete developer guide and helper script for the USDB embedded wallet → USD bank offramp flow, including a step-by-step scripts/README.md, a Node.js CLI (scripts/embedded-wallet-sign.js) wrapping Turnkey's HPKE decrypt and API-stamp primitives, and updates to CLAUDE.md and the Grid API skill to surface the new material. The code is clean and well-structured; the only minor concerns are a dead dB64 variable in privHexToCompressedPubHex and an implicit reliance on the @noble/curves transitive dependency without declaring it directly in package.json.

Confidence Score: 4/5

Safe to merge; all findings are P2 style suggestions with no runtime impact.

Only P2 findings (dead variable, implicit transitive dependency). No logic errors, security issues, or breaking changes.

scripts/embedded-wallet-sign.js — dead variable and implicit @noble/curves dependency worth addressing before the script is widely distributed.

Important Files Changed

Filename Overview
scripts/embedded-wallet-sign.js New helper script for USDB offramp signing (gen-keypair, decrypt-bundle, stamp); a dead variable and an implicit transitive dependency on @noble/curves are minor issues.
scripts/README.md New step-by-step guide for the USDB embedded-wallet offramp flow; copy-pasteable curl and polling loops look correct.
scripts/package.json New package manifest; @noble/curves is used in embedded-wallet-sign.js but not declared as a direct dependency here.
scripts/package-lock.json Generated lockfile for the new scripts package; all versions are consistent with the declared dependencies.
CLAUDE.md Documents the new scripts/ section with the USDB offramp gotcha; straightforward documentation addition.
.claude/skills/grid-api/SKILL.md Adds USDB / Turnkey / embedded-wallet trigger keywords and a new prose section pointing at the new scripts.

Sequence Diagram

sequenceDiagram
    participant Dev as Developer
    participant Grid as Grid API
    participant Sign as embedded-wallet-sign.js

    Note over Dev,Sign: 1. Onboarding
    Dev->>Grid: POST /customers
    Grid-->>Dev: customerId
    Dev->>Grid: POST /auth/credentials (EMAIL_OTP)
    Grid-->>Dev: credId — bootstraps Turnkey sub-org

    Note over Dev,Sign: 2. On-ramp
    Dev->>Grid: POST /quotes (platform USD to USDB account)
    Dev->>Grid: POST /quotes/{id}/execute
    Grid-->>Dev: transactionId COMPLETED

    Note over Dev,Sign: 3. Off-ramp
    Dev->>Sign: gen-keypair
    Sign-->>Dev: pubHex + privHex
    Dev->>Grid: POST /auth/credentials/{id}/challenge
    Dev->>Grid: POST /auth/credentials/{id}/verify (clientPublicKey)
    Grid-->>Dev: encryptedSessionSigningKey
    Dev->>Sign: decrypt-bundle (encryptedBundle, privHex)
    Sign-->>Dev: sessionPrivHex
    Dev->>Grid: POST /quotes (USDB account to bank)
    Grid-->>Dev: quoteId + payloadToSign
    Dev->>Sign: stamp (sessionPrivHex, payloadToSign)
    Sign-->>Dev: Grid-Wallet-Signature value
    Dev->>Grid: POST /quotes/{id}/execute with Grid-Wallet-Signature
    Grid-->>Dev: transactionId COMPLETED
Loading

Comments Outside Diff (1)

  1. scripts/embedded-wallet-sign.js, line 377-385 (link)

    P2 Dead variable and implicit transitive dependency

    dB64 is computed but never read — the comment above it describes a workaround that was apparently abandoned in favour of calling p256.getPublicKey directly. Also, @noble/curves is used here but is not listed as a direct dependency in package.json; it's only available transitively through @turnkey/crypto. If a future version of that package pins or drops @noble/curves, this require will fail at runtime.

    Consider adding "@noble/curves": "^1.9.0" to scripts/package.json so the dependency is explicit.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: scripts/embedded-wallet-sign.js
    Line: 377-385
    
    Comment:
    **Dead variable and implicit transitive dependency**
    
    `dB64` is computed but never read — the comment above it describes a workaround that was apparently abandoned in favour of calling `p256.getPublicKey` directly. Also, `@noble/curves` is used here but is not listed as a direct dependency in `package.json`; it's only available transitively through `@turnkey/crypto`. If a future version of that package pins or drops `@noble/curves`, this require will fail at runtime.
    
    Consider adding `"@noble/curves": "^1.9.0"` to `scripts/package.json` so the dependency is explicit.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
scripts/embedded-wallet-sign.js:377-385
**Dead variable and implicit transitive dependency**

`dB64` is computed but never read — the comment above it describes a workaround that was apparently abandoned in favour of calling `p256.getPublicKey` directly. Also, `@noble/curves` is used here but is not listed as a direct dependency in `package.json`; it's only available transitively through `@turnkey/crypto`. If a future version of that package pins or drops `@noble/curves`, this require will fail at runtime.

Consider adding `"@noble/curves": "^1.9.0"` to `scripts/package.json` so the dependency is explicit.

```suggestion
function privHexToCompressedPubHex(privHex) {
  // Re-derive the public point in compressed SEC1 form (the format the
  // Turnkey stamp expects).
  const { p256 } = require("@noble/curves/p256");
  const compressed = p256.getPublicKey(privHex, true); // Uint8Array
  return Buffer.from(compressed).toString("hex");
}
```

Reviews (1): Last reviewed commit: "Add Turnkey USDB offramp instructions" | Re-trigger Greptile

@akanter akanter requested a review from shreyav April 30, 2026 22:02
Copy link
Copy Markdown
Contributor

I don't think we want to mention Turnkey publicly?

Comment thread scripts/README.md
are HPKE bundle decrypt and Turnkey API stamp construction, which live in
[`embedded-wallet-sign.js`](./embedded-wallet-sign.js) (using
`@turnkey/crypto` and `@turnkey/api-key-stamper`).

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.

do you want to point to one of the sdks?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Applied. Added a callout near the top pointing production integrators at the official Grid SDKs (https://grid.lightspark.com), and noting this README is intended for hands-on tinkering / debugging.

Review response generated using /pr-review-respond Claude command.

Comment thread scripts/README.md

Customer creation returns `kybStatus: "UNVERIFIED"`. Internal accounts
auto-provision **after** approval. Poll:

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.

currently individuals are auto kyc'd

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Applied. Reworked section 1.2 to call out that INDIVIDUAL customers are auto-KYC'd (no wait), while only BUSINESS customers go through the manual KYB poll.

Review response generated using /pr-review-respond Claude command.

Comment thread scripts/README.md
Comment thread scripts/README.md
```

Read the OTP code from the email and assign to `$OTP`.

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.

could probably add something here about in sandbox otp is 000000

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Applied. Added a sandbox tip noting that no email is sent in sandbox mode and the fixed OTP is 000000.

Review response generated using /pr-review-respond Claude command.

Comment thread scripts/README.md Outdated
| select(.accountType=="EMBEDDED_WALLET").payloadToSign')
```

The quote response also includes a `SPARK_WALLET` invoice — that's the
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.

is this true?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Applied. Yes — the quote response includes both: paymentInstructions is a 2-element array with one SPARK_WALLET entry (address + invoice fields) and one EMBEDDED_WALLET entry (payloadToSign). Reworked the section to show the actual response shape and explicitly call out that the two are alternatives — pick one.

Review response generated using /pr-review-respond Claude command.

Comment thread scripts/README.md
### 3.5 Stamp the payload

```bash
STAMP=$($SIGN stamp "$SESSION_PRIV_HEX" "$PAYLOAD")
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.

do you wanna have sandbox or prod instructions here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Applied. Added a sandbox tip noting you can skip the keypair / decrypt / stamp dance and use Grid-Wallet-Signature: sandbox-valid-signature instead; any other value gets rejected the same way a bad real stamp would.

Review response generated using /pr-review-respond Claude command.

@akanter
Copy link
Copy Markdown
Author

akanter commented May 1, 2026

Not applicable. Per offline confirmation, mentioning Turnkey is fine, so I've left the references in. Happy to genericize if that ever changes — @turnkey/api-key-stamper and @turnkey/crypto are public npm packages so the dependency is already visible there.

Review response generated using /pr-review-respond Claude command.

- Add SDK pointer for production integrators (https://grid.lightspark.com)
- Clarify INDIVIDUAL auto-KYC vs BUSINESS manual KYB in section 1.2
- Note sandbox OTP `000000` in step 3.2
- Show paymentInstructions response shape; call out SPARK_WALLET vs EMBEDDED_WALLET as alternatives
- Note sandbox `Grid-Wallet-Signature: sandbox-valid-signature` shortcut in step 3.6

Co-Authored-By: Claude <noreply@anthropic.com>
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.

3 participants