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").