Product: auto.exchange — AI agent marketplace where agents hire and pay each other via USDC on Tempo (chain 4217).
The workaround: We have to manually serialize a type-0x76 tx, hash it, call secp256k1_sign on the raw hash via privy.wallets()._rpc() (an underscore-prefixed private API), parse the signature bytes manually (r/s/v extraction), re-serialize with the signature, and broadcast via eth_sendRawTransaction.
We verified with Privy's own documentation agent that eth_signTransaction exists but only supports standard Ethereum tx types (0, 1, 2, 4). Custom types like Tempo's 0x76 (with calls array and feeToken field) are rejected.
What we wanted: A chain-agnostic signRawTransaction that accepts arbitrary unsigned tx bytes and returns signed bytes — no tx type validation. This would work for Tempo, any L2 with custom tx types, and future chains. The existing secp256k1_sign on raw hashes works but requires ~70 lines of manual serialization, signature parsing, and broadcast logic.
Impact: ~70 lines of raw crypto plumbing that should be one line. Fragile — signature format assumptions, manual nonce management, hardcoded gas limits.
Privy creates embedded wallets (browser-only), smart wallets, and server wallets. Only server wallets work for our backend payment flow. But the frontend Privy SDK defaults to showing the embedded wallet. We had to:
- Disable embedded wallet creation (
createOnLogin: "off") - Never fall back to the Privy React wallet on the frontend
- Always fetch
server_wallet_addressfrom our API - Document everywhere: "use
server_wallet_address, not the Privy wallet"
What we wanted: One wallet type that works both client-side and server-side, or clear guidance on "for server-to-server payments, use X."
Verifying a Privy JWT gives us user_id. To get the wallet, we have to call privy.users()._get(userId) (another underscore API), then search linked_accounts for the embedded wallet. The server wallet isn't in the JWT or in the user object — we provision it separately and store it in our own DB.
What we wanted: JWT claims to include the server wallet address, or verifyAccessToken to return it.
We initially used @privy-io/server-auth (the documented package) before discovering it's deprecated in favor of @privy-io/node. The APIs are different. The node SDK uses privy.users()._get() instead of privy.getUser(), and privy.wallets() instead of separate wallet methods. This is documented nowhere obvious.
Tempo has: feeToken (pay gas in stablecoins), calls array (atomic multi-call), type-0x76/0x78 txs. None of these work through Privy's standard sendTransaction or eth_signTransaction. We ended up bypassing Privy's tx infrastructure entirely and using it purely as a key management service (secp256k1_sign).
What we wanted: Either support custom tx types in sendTransaction/eth_signTransaction, or provide a signRawBytes(walletId, unsignedTxBytes) method that's tx-type-agnostic. The latter is simpler and would work for any chain.
Summary: Privy works well as an auth + key management layer. The core ask is simple: a signRawBytes(walletId, bytes) method that signs arbitrary unsigned transaction bytes without validating tx type. This would eliminate 80% of our workarounds and work for any chain with custom tx types (Tempo, future L2s, etc.). The existing secp256k1_sign on raw hashes gets us there but with unnecessary complexity.