Skip to main content
When you set fee: 0n and nativeFee: 0n, relayers won’t process your transfer. You finalize it yourself on the destination chain. When to use this:
  • Cost savings on high-volume transfers
  • Running your own relayer infrastructure
  • Specific timing or ordering requirements

The Process

The steps depend on your source chain: From NEAR:
  1. Initiate transfer (ft_transfer_call)
  2. Call signTransfer to trigger MPC signing
  3. Parse the SignTransferEvent from logs
  4. Submit finalization on destination chain
From EVM/Solana:
  1. Initiate transfer on source chain
  2. Wait for proof to become available (Merkle proof or Wormhole VAA)
  3. Submit finalization on NEAR
The proof type and wait time depends on the source:
SourceProof TypeWait Time
NEARMPC Signature~1-5 min
EthereumMerkle Proof~15-20 min
L2s (Base, Arb, etc.)Wormhole VAA~1 min
SolanaWormhole VAA~15 sec

When is Finalization Ready?

Poll the API for the Signed status:
import { BridgeAPI } from "@omni-bridge/core"

const api = new BridgeAPI("mainnet")

const statuses = await api.getTransferStatus({ transactionHash: hash })

if (statuses.includes("Signed")) {
  // Ready to finalize!
}
For Ethereum → NEAR specifically, you need to wait for the light client to sync the block (check Initialized status and wait ~15-20 min).

From NEAR to EVM

NEAR → EVM transfers require three steps: initiate, sign, then finalize.

Step 1: Initiate the Transfer

import { createBridge } from "@omni-bridge/core"
import { createNearBuilder, toNearKitTransaction } from "@omni-bridge/near"

const bridge = createBridge({ network: "mainnet" })
const nearBuilder = createNearBuilder({ network: "mainnet" })

const signerId = "alice.near"

// Validate and build the transfer
const validated = await bridge.validateTransfer({
  token: "near:wrap.near",
  amount: 1_000_000_000_000_000_000_000_000n,
  sender: `near:${signerId}`,
  recipient: "eth:0x...",
  fee: 0n,
  nativeFee: 0n,
})

const initTx = nearBuilder.buildTransfer(validated, signerId)
const initResult = await toNearKitTransaction(near, initTx).send()

Step 2: Sign the Transfer

After the transfer is initialized, you must call signTransfer to trigger MPC signing:
// Extract the transfer ID from the InitTransferEvent in the logs
const initEvent = parseInitTransferEvent(initResult)

const signTx = nearBuilder.buildSignTransfer(
  {
    origin_chain: "Near",
    origin_nonce: initEvent.nonce,
  },
  signerId
)

const signResult = await toNearKitTransaction(near, signTx).send()

Step 3: Finalize on EVM

Parse the SignTransferEvent from the sign transaction logs and finalize on EVM:
import { createEvmBuilder } from "@omni-bridge/evm"
import { ChainKind } from "@omni-bridge/core"
import { MPCSignature } from "@omni-bridge/near"

const evm = createEvmBuilder({ network: "mainnet", chain: ChainKind.Eth })

// Parse SignTransferEvent from signResult logs
const signEvent = parseSignTransferEvent(signResult)

// Convert signature for EVM (adds 27 to recovery ID)
const signature = MPCSignature.fromSignTransferEvent(signEvent).toBytes(true)

const tx = evm.buildFinalization(signEvent.message_payload, signature)
await walletClient.sendTransaction(tx)
The MPC signature needs format conversion for EVM — use MPCSignature.toBytes(true) to add 27 to the recovery ID. For Solana, use toBytes(false).

From EVM to NEAR

Ethereum (Merkle Proof)

Ethereum uses the NEAR light client for verification:
import { createNearBuilder, toNearKitTransaction } from "@omni-bridge/near"
import { getEvmProof, ProofKind } from "@omni-bridge/evm"

const nearBuilder = createNearBuilder({ network: "mainnet" })

// Wait for light client to sync (~15-20 min after source tx confirms)

// Generate Merkle proof
const proof = await getEvmProof(txHash, ChainKind.Eth)

// Serialize for NEAR
const proverArgs = nearBuilder.serializeEvmProofArgs({
  proof_kind: ProofKind.InitTransfer,
  proof,
})

// Build and send finalization
const tx = nearBuilder.buildFinalization(ChainKind.Eth, proverArgs, signerId)
await toNearKitTransaction(near, tx).send()

L2s (Wormhole VAA)

Base, Arbitrum, Polygon, and BNB use Wormhole:
import { getWormholeVaa } from "@omni-bridge/core"

// Wait for Wormhole guardians to sign (~1 min)
const vaa = await getWormholeVaa(txSignature, "Mainnet")

// Serialize for NEAR
const proverArgs = nearBuilder.serializeWormholeProofArgs({
  proof_kind: ProofKind.InitTransfer,
  vaa,
})

// Finalize
const tx = nearBuilder.buildFinalization(ChainKind.Base, proverArgs, signerId)
await toNearKitTransaction(near, tx).send()

From Solana to NEAR

Solana also uses Wormhole VAAs:
import { getWormholeVaa } from "@omni-bridge/core"

const vaa = await getWormholeVaa(solanaSignature, "Mainnet")

const proverArgs = nearBuilder.serializeWormholeProofArgs({
  proof_kind: ProofKind.InitTransfer,
  vaa,
})

const tx = nearBuilder.buildFinalization(ChainKind.Sol, proverArgs, signerId)
await toNearKitTransaction(near, tx).send()

Finalization Builders

Each destination has a buildFinalization method:
DestinationMethod
EVMevmBuilder.buildFinalization(payload, signature)
NEARnearBuilder.buildFinalization(chain, proverArgs, signerId)
SolanasolanaBuilder.buildFinalization(payload, signature, payer)

Working Examples

For complete, runnable examples:

Consider Using Relayers Instead

Manual finalization adds complexity. Use relayer fees if:
  • You want fire-and-forget simplicity
  • The fee cost is acceptable
  • You don’t need precise timing control