You are integrating BlindPay EVM payouts funded from a Fireblocks vault account into my application. Fireblocks holds the private key material (MPC, direct custody); every signature comes from Fireblocks' API, never from a local key or browser wallet.
Before writing code, read these sources and follow them over any prior knowledge:
- https://blindpay.com/docs/llms.txt (read the blockchain wallets page and the EVM payout guide)
- The OpenAPI spec: curl https://api.blindpay.com/doc
- https://developers.fireblocks.com/docs/quickstart.md
- https://developers.fireblocks.com/reference/create-transactions.md
- https://developers.fireblocks.com/reference/sign-typed-messages-for-ethereum-and-evm-networks.md
- https://developers.fireblocks.com/docs/interact-with-smart-contracts.md
- https://developers.fireblocks.com/reference/monitoring-transaction-status.md
Build the flow:
1. Set up Fireblocks API authentication: generate an RSA-4096 keypair and CSR, upload the CSR in the Fireblocks Console to create an API user, and store the resulting API key and private key server-side. Confirm the SDK (`new Fireblocks({apiKey, secretKey, basePath})`) signs every request as a JWT automatically, so the private key never leaves the server.
2. Register the sender wallet with BlindPay: call GET /v1/instances/{instance_id}/customers/{customer_id}/blockchain-wallets/sign-message to get the challenge string, then sign it via Fireblocks with a POST /v1/transactions call using operation TYPED_MESSAGE, source pointing at the vault account, and extraParameters.rawMessageData.messages[0] set to {content: hex(challenge), type: "EIP191"}; confirm the correct assetId for typed-message signing on this chain in the Fireblocks docs above rather than assuming one. Poll GET /v1/transactions/{txId} until status is COMPLETED, then assemble the signature from signedMessages[0].signature.{r,s,v} using the byte order and v-value convention documented on the Fireblocks typed-message signing page. POST the assembled signature to /v1/instances/{instance_id}/customers/{customer_id}/blockchain-wallets and persist the returned bw_ ID against the customer record.
3. Request a payout quote: POST /v1/instances/{instance_id}/quotes with bank_account_id, network, and token, and persist the quote_id plus the full contract object from the response (address, abi, functionName "approve", blindpayContractAddress, amount, network) alongside the pending payout record, reading every field verbatim from that response rather than a stored constant.
4. Send the approve call through Fireblocks: build the ABI-encoded calldata from the quote's contract.abi and functionName "approve" (using ethers.js, viem, or web3.js), then POST /v1/transactions with operation CONTRACT_CALL, assetId matching the chain, source set to the vault account, destination as a ONE_TIME_ADDRESS pointing at the token contract, amount "0", and extraParameters.contractCallData set to the encoded hex. Poll GET /v1/transactions/{txId} (or subscribe to Fireblocks' transaction webhook) through PENDING_SIGNATURE, BROADCASTING, and CONFIRMING until status is COMPLETED, and treat BLOCKED, FAILED, CANCELLED, or REJECTED as a hard stop that surfaces the Fireblocks policy or cosigner rejection reason to an operator.
5. Execute the payout before the quote expires: POST /v1/instances/{instance_id}/payouts/evm with quote_id and sender_wallet_address before expires_at (about 5 minutes out). Since Fireblocks' Transaction Authorization Policy and cosigner approval add latency on top of the approve confirmation, check the remaining time-to-expiry before calling this endpoint; if the quote has already expired, re-quote and skip a fresh approve call when the existing on-chain allowance already covers the new quote's amount.
6. Handle payout status from BlindPay's Svix-signed webhooks (svix-id, svix-timestamp, svix-signature verified with whsec_ against the raw body) for payout.new, payout.update, and payout.complete, and update the payout record's state on each event.
7. Run the full flow end to end on a development instance: use USDB on a testnet (for example base_sepolia), where KYC auto-approves, and verify all three outcomes with sentinel amounts: a normal amount completes, $666.00 (66600 minor units) forces a failed payout, and $777.00 (77700 minor units) forces a refunded payout.
Constraints:
- API keys and the Fireblocks RSA private key stay server-side; nothing about signing touches the client.
- Amounts are integer minor units everywhere, in both BlindPay and Fireblocks calls; never use floating point for money.
- Treat any Fireblocks TAP or cosigner-approval detail you can't confirm from the docs above as unverified and check the Fireblocks Console/docs before hardcoding an assumption about it.
Deliverables: Fireblocks API auth setup, the wallet registration flow (TYPED_MESSAGE sign plus bw_ persistence), the CONTRACT_CALL approve module with async status polling or webhook handling, quote-execute orchestration with the expiry race handled against Fireblocks' approval latency, BlindPay webhook handlers, and a passing end-to-end test on the development instance covering the normal, failed, and refunded sentinel amounts.