Skip to main content

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

PropertyWhat it gives you
The server never holds keysNo custodial risk. We can't sign on a user's behalf even if we wanted to.
The user signs with their own walletPrivy, MetaMask, WalletConnect, Rift, a raw key in a server-side bot — all work identically.
You broadcast through your own RPCYou control mempool / private-pool routing / MEV protection / failover.
Reads, builds, broadcasts are statelessThe 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
}
}
fieldtypemeaning
chainIdnumberThe chain this tx targets. Sign for this network.
to0x-addressRecipient. The Diamond proxy for hedge / oracle / governance calls; the ERC-20 token address for approve.
data0x-hexThe ABI-encoded function call (selector + args).
valuedecimal string in weiNative value. "0" for every hedge call (transfers are in USDC, not ETH).
functionstringHuman-readable function name. Useful for telling a wallet to render "buyProtection" instead of an opaque 0x-blob.
argsobjectPretty-printed view of the args. Echoed back for audit / UI display.
estimatedGasdecimal string or nullBest-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.
  • deadline is real. A buyProtection tx mined after deadline reverts. Set it to now + 5 minutes minimum.
  • Decimal strings, not floats. Token amounts, prices, and bigint fields are always passed as decimal strings to avoid JS precision loss. 100 USDC on 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").