Skip to content

Send on EVM

Every EVM send follows the same shape: you describe the transaction, the SDK builds a canonical intent, signs it with your API key, and submits it. XKOVA binds that signature to exactly what it broadcasts — an operator cannot change the recipient or amount after you approve.

ChainchainSandbox chainIdProduction chainId
Avalanche C-Chainavalanche_c43113 (Fuji)43114
Basebase84532 (Sepolia)8453

You pass the numeric chainId on each send; it is part of the signed intent, so a transaction can never be replayed onto a different chain.

const tx = await xkova.sendNative({
wallet,
chainId: 43113,
to: "0xRecipient…",
amount: "5000000000000000", // wei, decimal string
idempotencyKey: "payout-2026-09-05-001",
});

Amounts are base units as decimal strings (wei for native, the token’s smallest unit for ERC-20). Strings avoid the precision loss of JavaScript numbers; bigint is also accepted.

// ERC-20 transfer(to, amount)
await xkova.sendErc20({ wallet, chainId, token: "0xToken…", to, amount: "1000000", idempotencyKey });
// ERC-721 safeTransferFrom(wallet → to, tokenId)
await xkova.sendErc721({ wallet, chainId, token: "0xNft…", to, tokenId: "42", idempotencyKey });
// ERC-1155 safeTransferFrom(wallet → to, tokenId, amount)
await xkova.sendErc1155({ wallet, chainId, token: "0xNft…", to, tokenId: "7", amount: "3", idempotencyKey });

The from is always the wallet you name — you can only move assets the vault controls.

For an arbitrary call, encode the calldata yourself (e.g. with viem or ethers) and send the exact { to, value, data }:

import { encodeFunctionData } from "viem";
const data = encodeFunctionData({
abi,
functionName: "approve",
args: ["0xSpender…", 1000000n],
});
await xkova.sendTransaction({
wallet,
chainId: 43113,
to: "0xToken…",
data, // 0x-hex
value: 0n,
idempotencyKey: "approve-usdc-001",
});

What you sign is exactly { to, value, data } — nothing about the call is inferred or rewritten on our side.

You authorize intent — chain, recipient, amount, asset. Nonce, gas, and fee are mechanical and are filled by the operator at broadcast. To bound the fee, set max_fee (base units) on the send; a transaction that would exceed it is rejected rather than broadcast.

Every send carries an idempotencyKey you choose. Retrying with the same key returns the same transaction instead of sending twice — so a timeout or a retry never double-spends. Reusing a key with different contents is a 409 conflict. Use a key derived from your own operation ID, not a random value you forget on retry.

sendNative and friends return a Transaction in an early state. Follow it to a terminal state by polling getTransaction(id) or, better, with webhooks. The state machine and terminal outcomes are in Core concepts.