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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ For the **USDB embedded wallet → USD bank offramp** flow, see [`scripts/README

Both are wrapped by [`scripts/embedded-wallet-sign.js`](./scripts/embedded-wallet-sign.js) (uses `@turnkey/crypto` + `@turnkey/api-key-stamper`). One-time setup: `cd scripts && npm install`.

**Important gotcha**: the USDB embedded wallet's Turnkey sub-org and Spark network wallet aren't fully bootstrapped when a customer is created. **Register an `EMAIL_OTP` auth credential against the USDB account before the first quote**, otherwise on-ramp quotes fail with `to_network INTERNAL_FUNDED_FIAT does not support USDB`. This is documented in `scripts/README.md` step 1.4.
**Important gotcha**: the USDB embedded wallet's Turnkey sub-org and Spark network wallet aren't fully bootstrapped when a customer is created. **Verify the auto-created `EMAIL_OTP` auth credential on the USDB account before the first quote**, otherwise on-ramp quotes fail with `to_network INTERNAL_FUNDED_FIAT does not support USDB`. This is documented in `scripts/README.md` step 1.4.

Read `scripts/README.md` whenever the task involves Turnkey signing, offramp, or `Grid-Wallet-Signature`.

Expand Down
11 changes: 4 additions & 7 deletions mintlify/snippets/global-accounts/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ sequenceDiagram
participant E as Email

C->>IB: POST /my-backend/otp/register { email }
IB->>G: POST /auth/credentials { type: EMAIL_OTP, email, accountId }
IB->>G: POST /auth/credentials { type: EMAIL_OTP, accountId }
G->>E: deliver OTP email
G-->>IB: 201 AuthMethod
IB-->>C: { credentialId }
Expand All @@ -448,8 +448,7 @@ curl -X POST "$GRID_BASE_URL/auth/credentials" \
-H "Content-Type: application/json" \
-d '{
"type": "EMAIL_OTP",
"accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002",
"email": "jane@example.com"
"accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002"
}'
```

Expand Down Expand Up @@ -577,8 +576,7 @@ Requires an active session on an *existing* credential on the same account. The
-H "Content-Type: application/json" \
-d '{
"type": "EMAIL_OTP",
"accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002",
"email": "jane@example.com"
"accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002"
}'
```

Expand Down Expand Up @@ -607,8 +605,7 @@ Requires an active session on an *existing* credential on the same account. The
-H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
-d '{
"type": "EMAIL_OTP",
"accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002",
"email": "jane@example.com"
"accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002"
}'
```

Expand Down
2 changes: 1 addition & 1 deletion mintlify/snippets/global-accounts/walkthrough.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Registration only binds the passkey to the account — it doesn't issue a sessio
}'
```

Grid verifies the attestation and replies `201` with the new `AuthMethod:...` id plus the first-authentication `challenge`, `requestId`, and `expiresAt`. Persist the auth method id against the customer — you'll pass it to `/challenge` and `/verify` whenever the customer needs to sign.
Grid verifies the attestation and replies `201` with the new `AuthMethod:...` id. Persist the auth method id against the customer — you'll pass it to `/challenge` and `/verify` whenever the customer needs to sign.
</Step>
</Steps>

Expand Down
23 changes: 13 additions & 10 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,27 @@ g "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID" \

Capture the **USDB account id** into `$USDB_ACCT`.

### 1.4 Bootstrap the embedded wallet (register an EMAIL_OTP credential)
### 1.4 Bootstrap the embedded wallet (verify the EMAIL_OTP credential)

> **Required before the first quote.** The USDB embedded wallet's Turnkey
> sub-org and Spark network wallet aren't fully provisioned at customer
> creation time. Registering an auth credential triggers that bootstrap.
> Skipping causes the first on-ramp quote to fail with
> creation time. Verifying the auto-created auth credential triggers that
> bootstrap. Skipping causes the first on-ramp quote to fail with
> `to_network INTERNAL_FUNDED_FIAT does not support USDB`.

An `EMAIL_OTP` credential is automatically created when the embedded wallet
is provisioned. Find it and issue a challenge to send the OTP email:

```bash
CRED=$(g -X POST -H 'Content-Type: application/json' \
-d '{"type": "EMAIL_OTP", "accountId": "'$USDB_ACCT'"}' \
"$GRID_BASE_URL/auth/credentials")
CRED_ID=$(echo "$CRED" | jq -r .id)
CRED_ID=$(g "$GRID_BASE_URL/auth/credentials?accountId=$USDB_ACCT" \
| jq -r '.data[] | select(.type=="EMAIL_OTP") | .id')

g -X POST "$GRID_BASE_URL/auth/credentials/$CRED_ID/challenge"
Comment on lines +92 to +95
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 If no EMAIL_OTP credential is found (e.g., $USDB_ACCT is unset or the auto-provisioned credential hasn't appeared yet), $CRED_ID will be empty and the challenge request will hit /auth/credentials//challenge, producing a confusing 404 or routing error. A guard prevents silent failures in a dev walkthrough script.

Suggested change
CRED_ID=$(g "$GRID_BASE_URL/auth/credentials?accountId=$USDB_ACCT" \
| jq -r '.data[] | select(.type=="EMAIL_OTP") | .id')
g -X POST "$GRID_BASE_URL/auth/credentials/$CRED_ID/challenge"
CRED_ID=$(g "$GRID_BASE_URL/auth/credentials?accountId=$USDB_ACCT" \
| jq -r '.data[] | select(.type=="EMAIL_OTP") | .id | first // empty')
[ -z "$CRED_ID" ] && echo "ERROR: no EMAIL_OTP credential found for $USDB_ACCT" && exit 1
g -X POST "$GRID_BASE_URL/auth/credentials/$CRED_ID/challenge"
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/README.md
Line: 92-95

Comment:
If no `EMAIL_OTP` credential is found (e.g., `$USDB_ACCT` is unset or the auto-provisioned credential hasn't appeared yet), `$CRED_ID` will be empty and the challenge request will hit `/auth/credentials//challenge`, producing a confusing 404 or routing error. A guard prevents silent failures in a dev walkthrough script.

```suggestion
CRED_ID=$(g "$GRID_BASE_URL/auth/credentials?accountId=$USDB_ACCT" \
  | jq -r '.data[] | select(.type=="EMAIL_OTP") | .id | first // empty')

[ -z "$CRED_ID" ] && echo "ERROR: no EMAIL_OTP credential found for $USDB_ACCT" && exit 1

g -X POST "$GRID_BASE_URL/auth/credentials/$CRED_ID/challenge"
```

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

Fix in Claude Code

```

This also sends an OTP email to the address on the customer record. Keep
the code handy; you'll use it for the offramp signing step (3.3) within
the OTP TTL (~5 min). If it expires, re-issue via `/challenge` (3.2).
This sends an OTP email to the address on the customer record. Keep the code
handy; you'll use it for the offramp signing step (3.3) within the OTP TTL
(~5 min). If it expires, re-issue via `/challenge` (3.2).

### 1.5 Add a destination bank (USD external account)

Expand Down
Loading