You are integrating BlindPay payouts funded from Dfns MPC wallets into my application. Dfns holds the key shares for our wallets; my server calls the Dfns API to sign and broadcast on their behalf.
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 payouts guide)
- The OpenAPI spec: curl https://api.blindpay.com/doc
- https://docs.dfns.co/d/api-docs/wallets/generate-signature-from-wallet
- https://docs.dfns.co/d/api-docs/wallets/broadcast-transaction
- https://docs.dfns.co/d/api-docs/authentication
Build the flow:
1. Call GET /v1/instances/{instance_id}/customers/{customer_id}/blockchain-wallets/sign-message to get the registration challenge, hex-encode it, and sign it with the Dfns wallet via POST /wallets/{walletId}/signatures using {"blockchainKind":"Evm","kind":"Eip191","message":"0x<hex>"}, authenticated with the Dfns Service Account Token bearer header plus an X-DFNS-USERACTION header built from the User Action Signature flow (submit the intent, sign the returned challenge with the registered credential, attach the signature). Confirm the exact User Action Signature request/response shape against docs.dfns.co/d/api-docs/authentication rather than assuming a field layout. Take the resulting signature, call POST /v1/instances/{instance_id}/customers/{customer_id}/blockchain-wallets, and persist the returned bw_ ID against the customer record.
2. Request a quote with POST /v1/instances/{instance_id}/quotes for the bank_account_id, network, and token, and hold the contract object (address, abi, functionName "approve", blindpayContractAddress, amount, network) exactly as returned in the response, ready to build the approve calldata in step 3.
3. Build the ERC-20 approve calldata from that contract object and send it through POST /wallets/{walletId}/transactions with {"kind":"Transaction","transaction":{"to":"<token contract address>","data":"<approve calldata>","maxFeePerGas":...,"maxPriorityFeePerGas":...},"externalId":"<idempotency key>"}, again with the bearer token and a fresh X-DFNS-USERACTION header. Poll the returned transaction-request ID until its status reaches Confirmed before moving on, and treat Failed or Rejected as a stop condition that surfaces to the caller.
4. Call POST /v1/instances/{instance_id}/payouts/evm with quote_id and sender_wallet_address before the quote's expires_at (about 5 minutes). If Dfns's Pending to Confirmed cycle (plus any policy-engine approval delay on the transaction) eats into that window and the quote expires first, re-quote and reuse the existing on-chain allowance when it already covers the new amount instead of re-approving from zero.
5. Verify a Svix-signed webhook handler (svix-id, svix-timestamp, svix-signature against the whsec_ secret, HMAC over the raw body) updates payout state on payout.new, payout.update, and payout.complete, and run the full flow end to end on a development instance with USDB on base_sepolia, including one payout at $666.00 (66600 minor units) that lands in failed and one at $777.00 (77700) that lands in refunded.
Constraints:
- Keep the Dfns Service Account Token and the BlindPay API key server-side only; never expose either to a browser or mobile client.
- Carry all amounts as integer minor units through both the BlindPay and Dfns calls; never do floating-point money math.
- If Dfns's docs describe a policy engine or approval workflow on your account, check whether transaction requests require a second approver before Confirmed, since that adds directly to the quote-expiry race in step 4.
Deliverables: the wallet registration flow (challenge signing plus bw_ persistence), the approve-and-poll module built on POST /wallets/{walletId}/transactions, quote-execute orchestration with the expiry race handled, the Svix webhook handler, and a passing end-to-end test against the development instance covering the two sentinel amounts.