Turnkey integration
Pockey runs on Turnkey, non-custodially. This file says how it works, what was verified against the real Turnkey API, and what is left to check by hand. The reasoning behind the model is in docs/decisions.md, and what it does and does not guarantee is in SECURITY.md.
How it works
Three credentials, three kinds of power:
| Credential | Who holds it | What it can do |
|---|---|---|
Parent API key (TURNKEY_API_*) |
Pockey API (Wrangler secret) | Create sub-organizations, run login ceremonies (OTP, OAuth). Nothing inside a user sub-organization. |
| Session key (P-256, non-extractable) | The embed iframe, IndexedDB | Everything the user can do: sign, export, add a passkey, create session policies. Expires with the Pockey session (30 days). |
Signer key (TURNKEY_SIGNER_*, defaults to the parent key) |
Pockey API | Only what a user policy allows (session signers), and deleting those policies. Sole owner of the per-app Solana fee payer sub-organizations. |
Every request touching a user key is stamped in the iframe and relayed by the API, which checks it matches the route (packages/key-provider/src/turnkey/provider.ts, relay) and cannot change it.
Code map:
packages/key-provider/src/index.ts: theKeyProviderport.packages/key-provider/src/turnkey/: wire format, stamps, HTTP client, provider, policy mapping, HPKE port, the in-memory emulator (emulator/), the browser side (client.ts,key-client.ts,client-crypto.ts).apps/embed/src/lib/keys.ts: IndexedDB key store, production builds pin Turnkey's enclave key.packages/core/src/keys.ts: the provider-agnosticKeyClientthe SDK uses.
Verified live, against a real organization
packages/key-provider/src/turnkey/live.test.ts (run with TURNKEY_LIVE=1) and the two spikes behind the decision:
- the parent key cannot sign, export or add a user in a user sub-organization;
- sub-organization and wallet (EVM and Solana accounts) created in one call;
- external wallet sign-up and login (EIP-191 and ed25519 stamps), passkey registration and login addressed to the parent (the provider finds the sub-organization);
- session policies: signer user, revoke permission,
wallet.idpluswallet_account.addressbinding,eth.tx.*andsolana.tx.*conditions, native expiry (time), revocation by the signer, no policy creation by the signer; - EVM and Solana transaction signing, Solana raw messages, export decrypted against Turnkey's production enclave key;
- about one second of propagation after creating a wallet, user or policy (retried for 5 seconds).
Email OTP, checked on 2026-10-04 with a real inbox (otp-live.test.ts, two steps, one code per run):
INIT_OTP_V3sends the code,VERIFY_OTP_V2accepts the bundle encrypted in the iframe to Turnkey's enclave, so our API never sees the code;- sign-up with the
signupV2client signature, then the session key alone signs for the new user; - logging back into the same account with a fresh session key (
OTP_LOGIN_V2, approved by that key's own signature), which then reads the wallet and creates a session policy the Pockey signer uses alone; - cost: one signature per signed payload. Creating a sub-organization, sending and verifying a code and logging in are not billed as signatures.
Left to check by hand
- Google sign-in with real OAuth keys: Turnkey must accept our ID token (nonce = SHA-256 of the session public key) at sub-organization creation and in
OAUTH_LOGIN. - Apple sign-in: same, plus the Apple client secret (a signed JWT) in
APPLE_CLIENT_SECRET. - The OTP email template: app name and logo come from the app appearance. Sending from our own domain needs a verified domain at Turnkey.
Questions for Turnkey
- Price per active wallet and per signature. Section 11 assumes 0.02 to 0.03 USD per monthly active wallet; above 0.04 USD the price moves from 0.06 to 0.08 USD.
- Limits: sub-organizations per parent, activities per second, free tier.
- Raw payloads under a policy: can a policy only allow payloads Turnkey hashes itself? Today a session that signs messages (
allowMessages) can sign any 32-byte digest while it lives, which is why it is capped at 24 hours. - ERC-4337: can a policy parse user operations? A sponsored EVM send under a session is a raw digest for Turnkey.
Local development
KEY_PROVIDER=mock(default): the emulator. OTP codes are printed in the API log like before. State is lost when wrangler restarts: wipe the local D1 then (rm -rf apps/api/.wrangler/stateand re-run the migrations and seed).KEY_PROVIDER=turnkeywithTURNKEY_ORG_ID,TURNKEY_API_PUBLIC_KEY,TURNKEY_API_PRIVATE_KEYinapps/api/.dev.vars: real sub-organizations, real OTP emails. Local users created with the emulator do not exist at Turnkey: wipe the local D1 when switching.