The calldata-builder pattern
This is the core abstraction of the integrator API. Read it once and the rest of the surface makes sense.
The flow
Why this design
| Property | What it gives you |
|---|---|
| The server never holds keys | No custodial risk. We can't sign on a user's behalf even if we wanted to. |
| The user signs with their own wallet | Privy, MetaMask, WalletConnect, Rift, a raw key in a server-side bot — all work identically. |
| You broadcast through your own RPC | You control mempool / private-pool routing / MEV protection / failover. |
| Reads, builds, broadcasts are stateless | The API horizontal-scales trivially. |
This is the same pattern Safe transactions, Privy, and most professional custody integrations use. We're just exposing it as a typed REST API covering all of BlockFinaX's contract surface.
The BuiltTx envelope
Every POST /v1/tx/... endpoint returns:
{
"data": {
"chainId": 8453,
"to": "0xbCC51E62C4948FD35ab505bd71804C849601e4Ef",
"data": "0x...abi-encoded calldata...",
"value": "0",
"function": "buyProtection",
"args": { "eventId": "12", "notional": "100000000", ... },
"estimatedGas": null
}
}
| field | type | meaning |
|---|---|---|
chainId | number | The chain this tx targets. Sign for this network. |
to | 0x-address | Recipient. The Diamond proxy for hedge / oracle / governance calls; the ERC-20 token address for approve. |
data | 0x-hex | The ABI-encoded function call (selector + args). |
value | decimal string in wei | Native value. "0" for every hedge call (transfers are in USDC, not ETH). |
function | string | Human-readable function name. Useful for telling a wallet to render "buyProtection" instead of an opaque 0x-blob. |
args | object | Pretty-printed view of the args. Echoed back for audit / UI display. |
estimatedGas | decimal string or null | Best-effort gas estimate via eth_estimateGas. Only attempted if you pass from in the request. |
Pass from if you want a gas estimate
By default, estimatedGas is null — many calls revert without prior
approvals or other state, and we'd rather return a successful build than
fail the whole request because gas couldn't be computed.
Pass the user's wallet address as from to enable estimation:
curl -X POST https://api.blockfinax.com/v1/tx/hedge/buy-protection \
-H "Content-Type: application/json" \
-d '{
"chainId": 8453,
"eventId": 12,
"notional": "100000000",
"maxCost": "110000000",
"deadline": "1782182400",
"from": "0xUserWallet..."
}'
If the simulated call reverts (e.g. user hasn't approved USDC yet),
estimatedGas falls back to null. The calldata is still built.
Signing examples
ethers.js
import { ethers } from "ethers";
const wallet = new ethers.Wallet(privKey, provider);
const res = await fetch("https://api.blockfinax.com/v1/tx/hedge/buy-protection", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chainId: 8453,
eventId: 12,
notional: "100000000",
maxCost: "110000000",
deadline: String(Math.floor(Date.now() / 1000) + 300),
}),
});
const { data: tx } = await res.json();
const txResp = await wallet.sendTransaction({
to: tx.to,
data: tx.data,
value: BigInt(tx.value),
// chainId is enforced by the provider; the value above is for sanity
});
await txResp.wait();
viem / wagmi (browser, with a connected wallet)
import { sendTransaction } from "@wagmi/core";
const tx = (await fetch("...buy-protection", { ... })).data;
const hash = await sendTransaction(config, {
to: tx.to,
data: tx.data,
value: BigInt(tx.value),
});
Privy
import { usePrivy } from "@privy-io/react-auth";
const { sendTransaction } = usePrivy();
const tx = (await fetchBuiltTx()).data;
const { hash } = await sendTransaction({
to: tx.to,
data: tx.data,
value: tx.value,
});
Server-side bot, raw signing + own RPC
import { ethers } from "ethers";
const wallet = new ethers.Wallet(process.env.BOT_KEY!);
const provider = new ethers.JsonRpcProvider(process.env.PRIVATE_RPC_URL!);
const { data: tx } = await (await fetch("...build")).json();
const signedTx = await wallet.connect(provider).populateTransaction({
to: tx.to,
data: tx.data,
value: BigInt(tx.value),
});
const signed = await wallet.signTransaction(signedTx);
await provider.broadcastTransaction(signed);
Or have us relay
If you want a quick path that doesn't require you to run an RPC, sign the tx locally and POST the bytes to our relay endpoint:
const res = await fetch("https://api.blockfinax.com/v1/tx/broadcast", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chainId: 8453, signedTx: rawSignedTx }),
});
const { data } = await res.json();
console.log("hash:", data.txHash);
The relay still doesn't sign — you must pre-sign locally.
Pitfalls to know
- Always approve USDC first for calls that pull tokens (
createEvent,deposit,buyProtection). The contract will revert with "Insufficient allowance" otherwise. deadlineis real. A buyProtection tx mined afterdeadlinereverts. Set it tonow + 5 minutesminimum.- Decimal strings, not floats. Token amounts, prices, and bigint fields
are always passed as decimal strings to avoid JS precision loss.
100 USDCon a 6-decimal token is"100000000". - Fixed-point conversions matter for
createEvent. Strikes, caps, and initial rates are 1e6 precision (11.40 ="11400000"). Premium rate is also 1e6 (2.3% ="23000").