You are integrating BlindPay EVM payouts funded from Utila MPC vault wallets into my application. Utila holds the private key shares; my backend calls Utila's REST API to sign the BlindPay registration challenge and the ERC-20 approve, then calls BlindPay to execute the payout.
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://docs.utila.io/reference/api-overview
- https://docs.utila.io/reference/authentication
- https://docs.utila.io/reference/service-accounts
- https://docs.utila.io/reference/transactions_initiatetransaction
- https://docs.utila.io/reference/blockchains_listnetworks
Build the flow:
1. Authenticate to Utila as a service account: build a locally-signed RS256 JWT (sub = service account email, aud = "https://api.utila.io/", short expiry) from the uploaded RSA key pair and send it as a Bearer token on every Utila call. Confirm the exact claim set and endpoint against the live auth docs before hardcoding it.
2. Register the wallet: call GET /v1/instances/{instance_id}/customers/{customer_id}/blockchain-wallets/sign-message to get the challenge, sign it by calling Utila's POST /v2/vaults/{vault_id}/transactions:initiate with details.evmPersonalSign (fromAddress, message), poll GetTransaction (or handle the equivalent webhook) until the signing transaction reaches its completed state, extract the signature, and POST it to /v1/instances/{instance_id}/customers/{customer_id}/blockchain-wallets to persist the returned bw_ ID against the customer record.
3. Request a quote: call POST /v1/instances/{instance_id}/quotes with bank_account_id, network, and token, and read back the contract object (address, abi, functionName "approve", blindpayContractAddress, amount, network, expires_at) without hardcoding any contract address.
4. Send the approve: ABI-encode the "approve" call from the quote's contract.abi and amount, then call Utila's POST /v2/vaults/{vault_id}/transactions:initiate with details.evmTransaction (network resolved via ListNetworks for the quote's chain, fromAddress, toAddress = contract.blindpayContractAddress, value "0", data = the encoded call, publish true). Poll GetTransaction (or handle the equivalent webhook) until the transaction reaches CONFIRMED before moving on, and surface Utila's policy-engine states (pending approval, co-signing) as visible statuses in this step since they add latency on top of the quote's expiry window.
5. Execute the payout: call POST /v1/instances/{instance_id}/payouts/evm with the quote_id and sender_wallet_address before the quote's expires_at. If the quote expires while the approve is still confirming, request a fresh quote for the same amount and skip re-sending the approve when the existing on-chain allowance already covers it.
6. Verify the webhook secret and handle payout.new, payout.update, and payout.complete with Svix signature verification (svix-id, svix-timestamp, svix-signature against whsec_) on the raw request body, updating payout state in the database on each event.
7. Run the full flow end to end on a development instance with USDB on a testnet, then repeat it once each with the $666.00 (66600 minor units) sentinel to confirm the failed webhook path and $777.00 (77700 minor units) to confirm the refunded webhook path.
Constraints:
- Keep BlindPay and Utila API keys server-side; never expose either to the client.
- Represent every amount as an integer in minor units end to end; never use floating point for money math.
- Never hardcode the BlindPay contract address, ABI, or Utila network resource name; read them from the quote response and ListNetworks respectively.
Deliverables: the Utila service-account auth helper, the wallet registration flow with the bw_ ID persisted, the quote-approve-execute orchestration with async Utila confirmation and the expiry race handled, Svix-verified webhook handlers, and a passing end-to-end test against a development instance covering the success and both sentinel-amount paths.