Contract calls & approvals
From a web3 vault you can make arbitrary EVM contract calls and ERC-20 approvals. ZenVault signs them and (for the transfer path) broadcasts them; you supply gas and chainId, ZenVault owns the nonce.
Approve a spender
POST /v1/transfers/approve
Idempotency-Key: <uuid>
{
"srcAccountId": "8f1c...",
"tokenAssetCanonical": "USDT_ETHEREUM",
"spender": "0xdef1c0ded9bec7f1a1670819833240f027b25eff",
"amount": "1000000",
"evm": { "chainId": 1, "gasLimit": 60000,
"maxFeePerGas": "30000000000", "maxPriorityFeePerGas": "1500000000" }
}
Call a contract
POST /v1/transfers/contract-call
Idempotency-Key: <uuid>
{
"srcAccountId": "8f1c...",
"chainAssetCanonical": "ETH_ETHEREUM",
"contractAddress": "0xdef1c0ded9bec7f1a1670819833240f027b25eff",
"data": "0x38ed1739...",
"value": "0",
"evm": { "chainId": 1, "gasLimit": 220000,
"maxFeePerGas": "30000000000", "maxPriorityFeePerGas": "1500000000" }
}
- Supply one of
gasPrice(legacy) ormaxFeePerGas+maxPriorityFeePerGas(EIP-1559). chainIdandgasLimitare mandatory;nonceis server-assigned.
Get the signed bytes (self-broadcast paths)
If you broadcast yourself (swap fills, atomic windows), poll for the raw signed transaction after creating the call:
GET /v1/transfers/{id}/signed-tx
→ { "signedTransaction": "0x02f86a01...", "txHash": "0xaa11...", "state": "broadcasting" }
signedTransaction is null while signing is in flight — poll every couple of
seconds until it's a hex string, then submit it through your own RPC.
Time-sensitive swaps: pre-gas first
A web3 vault must hold native gas before its swap can broadcast. If the vault is
cold, the reactive top-up can take a minute — long enough to expire an RFQ quote.
Call POST /v1/gas/ensure
before you request the quote so the signed swap broadcasts immediately, inside
the quote window.
:::note Raw signing is different If you need to sign a message or a fully self-managed transaction (your own nonce, your own broadcast), use raw signing instead — it has no amount/destination policy, so gate it yourself. :::