Embedded Wallet Authentication
End-users authenticate to 1Claw directly (Email OTP, social providers) or via Sign in with 1Claw OAuth. On first login, treasury wallets can auto-provision for requested chains. All auth routes are rate-limited (5 burst, 1/sec per IP on public endpoints).
Email OTP (passwordless)
Best for apps that only need an email address — no password, no seed phrase.
Flow:
POST /v1/auth/email-otp/send— 6-digit code, 5-minute TTL, emailed via ResendPOST /v1/auth/email-otp/verify— returns JWT + creates user/org if new- Optional
auto_provision_chains— generates treasury wallets on first verify
- TypeScript
- curl
import { createClient } from "@1claw/sdk";
const client = createClient({ baseUrl: "https://api.1claw.xyz" });
await client.auth.sendEmailOtp({
email: "user@example.com",
platform_app_id: "YOUR_APP_UUID", // optional: tie login to platform app
});
const { data } = await client.auth.verifyEmailOtp({
email: "user@example.com",
code: "123456",
auto_provision_chains: ["ethereum", "bitcoin", "solana"],
});
client.http.setToken(data.token);
// data.user_id, data.org_id, data.is_new_user, data.wallet_address
# Send code
curl -X POST "https://api.1claw.xyz/v1/auth/email-otp/send" \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","platform_app_id":"APP_UUID"}'
# Verify code
curl -X POST "https://api.1claw.xyz/v1/auth/email-otp/verify" \
-H "Content-Type: application/json" \
-d '{
"email":"user@example.com",
"code":"123456",
"auto_provision_chains":["ethereum"]
}'
# Response: token, user_id, org_id, is_new_user, wallet_address?
If an email already belongs to another auth method or org, social/OTP flows return 409 — users must link explicitly via the consent flow (see Cross-org linking).
Social login
Server-verified OAuth for Google, Apple, and Discord.
| Provider | Client sends | Server validates |
|---|---|---|
id_token | Audience + issuer (ONECLAW_GOOGLE_CLIENT_ID) | |
| Apple | id_token | Audience + issuer (ONECLAW_APPLE_CLIENT_ID) |
| Discord | Authorization code + oauth_redirect_uri | Server-side token exchange (ONECLAW_DISCORD_CLIENT_ID + secret) |
const { data } = await client.auth.socialLogin({
provider: "google", // google | apple | discord
id_token: googleIdToken,
auto_provision_chains: ["ethereum"],
oauth_redirect_uri: "https://yourapp.com/auth/callback", // Discord only
});
New users receive an auto-provisioned Ethereum treasury wallet by default; pass auto_provision_chains for additional chains.
Environment variables (operator-configured on Vault):
ONECLAW_GOOGLE_CLIENT_IDONECLAW_APPLE_CLIENT_IDONECLAW_DISCORD_CLIENT_ID+ONECLAW_DISCORD_CLIENT_SECRET
Passkeys (WebAuthn)
Passkeys serve two roles for embedded wallets:
Login passkeys
Standard FIDO2 registration and assertion for passwordless dashboard login:
POST /v1/auth/passkeys/assert/begin+.../complete(public)POST /v1/auth/passkeys/register/begin+.../complete(authenticated)
See Two-factor auth & passkeys for human account passkey management.
Passkey transaction authorization
For high-value sends, require a WebAuthn assertion bound to the exact transaction instead of (or in addition to) password re-auth:
POST /v1/auth/passkeys/tx-assert/beginwith{ tx_digest }— SHA-256 hex ofchain|to|value_wei|dataPOST /v1/auth/passkeys/tx-assert/complete— returns 5-minutepasskey_token- Send with header
X-Passkey-Token: <token>onPOST /v1/treasury/wallets/{chain}/send
The server recomputes the digest from the send body and rejects mismatches.
// wallet-react handles this via sendWithPasskey()
const { sendWithPasskey } = useOneclawWallet();
await sendWithPasskey({
chain: "ethereum",
to: "0x...",
amount: "1.0",
});
The passkey prompt is cryptographically tied to one transaction. A captured token cannot authorize a different recipient or amount.
Sign in with 1Claw (OAuth)
1Claw acts as an OAuth2/OIDC authorization server. Third-party apps redirect users to the consent page; approved scopes yield access + ID tokens (RS256).
Authorization URL
Use PKCE (S256) in production:
import { generatePKCE, buildAuthorizeUrl } from "@1claw/sdk";
const { codeVerifier, codeChallenge } = await generatePKCE();
sessionStorage.setItem("pkce_verifier", codeVerifier);
const url = buildAuthorizeUrl("https://1claw.xyz", {
clientId: "my-wallet-app", // platform app slug
redirectUri: "https://yourapp.com/oauth/callback",
scopes: ["openid", "profile", "email"],
codeChallenge,
codeChallengeMethod: "S256",
state: crypto.randomUUID(),
});
window.location.href = url;
Dashboard consent page: /oauth/authorize.
Token exchange
const { data } = await client.auth.exchangeOAuthCode({
code: callbackCode,
client_id: "my-wallet-app",
redirect_uri: "https://yourapp.com/oauth/callback",
code_verifier: sessionStorage.getItem("pkce_verifier")!,
});
// data.access_token, data.id_token, data.refresh_token (if offline_access)
const userInfo = await client.auth.getUserInfo(data.access_token);
// userInfo.wallet_address when wallet scope granted
React shortcut
import { SignInWith1Claw, handleSignInCallback } from "@1claw/wallet-react";
<SignInWith1Claw
clientId="my-wallet-app"
redirectUri="https://yourapp.com/callback"
scopes={["openid", "profile", "email"]}
theme="dark"
/>
Supported scopes include openid, profile, email, and wallet-related scopes configured on your platform app. Revoke consent: DELETE /v1/oauth/consent/{app_slug}.
Full OAuth server details: Platform API — OAuth and OpenID discovery at GET /.well-known/openid-configuration.
Cross-org account linking
When a user with an existing 1Claw account (different org) signs in through your embedded flow, the API may return 409 with an authorize_url. The @1claw/wallet-react widget redirects to the grant/consent page automatically (onLinkRequired for custom UX).
After approval, the user's existing treasury wallets connect to your app — no duplicate wallets are created.
Platform-scoped login
Pass platform_app_id on Email OTP send/verify to associate the session with your platform app for connection tracking and spend policy resolution.
Security notes
- Auth endpoints use burst rate limiting; do not poll OTP verify in tight loops
- Failed password re-auth on export/send increments lockout counters (10 failures → 15-minute lock)
- JWTs for users include org membership; treasury endpoints require
principal_type: "user"
Related
- Getting started — platform app setup
- Send, swap, receive — passkey tx auth on sends
- React integration — widget auth UX
- Security overview — threat model and verification endpoints