Refunds

How refunds work when a crosschain transfer expires without being filled.

Refunds occur when no relayer fills a deposit before its fillDeadline expires. This is rare on mainnet — the competitive relayer network fills most deposits in ~2 seconds — but it's important to handle in production integrations.

When Refunds Happen

A deposit becomes eligible for a refund when:

  1. The fillDeadline passes without any relayer filling the intent
  2. No partial fills were executed

Common causes include extremely large transfers, unusual token pairs, or temporary relayer downtime.

Refund Flow

Deposit Expires

The fillDeadline passes and the deposit status changes to expired in the /deposit/status API. The user's funds are still escrowed in the origin SpokePool.

Bundle Processing

The Across Dataworker includes the expired deposit in the next settlement bundle. Bundles are proposed to the HubPool every ~1.5 hours and must pass a challenge period via UMA's Optimistic Oracle.

Settlement

After the challenge period, the bundle is finalized and the refund root is transmitted to the appropriate chain via canonical bridges.

Refund Execution

The refund is executed on-chain, returning the escrowed funds to the refundAddress. The deposit status changes to refunded in the API.

Refunds are not instant. After a deposit expires, the refund goes through the bundle settlement process. With ~1.5-hour bundle intervals plus the challenge period and canonical bridge delays, refunds can take several hours to arrive. Do not tell users to expect immediate refunds.

Refund Parameters

Configure refund behavior when calling the Swap API.

ParameterTypeDefaultDescription
refundAddressstringdepositorAddress that receives the refund
refundOnOriginbooleantrueIf true, refund on origin chain. If false, refund on destination chain
swap-with-refund-config.ts
const params = new URLSearchParams({
  tradeType: "minOutput",
  originChainId: "42161",
  destinationChainId: "8453",
  inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
  outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  amount: "1000000000",
  depositor: "0xYourWalletAddress",
  // Refund configuration
  refundAddress: "0xYourWalletAddress",  // Where to send the refund
  refundOnOrigin: "true",               // Refund on Arbitrum (origin)
});

const response = await fetch(
  `https://app.across.to/api/swap/approval?${params}`
);

Checking Refund Status

Use the /deposit/status endpoint to monitor whether an expired deposit has been refunded.

check-refund.ts
import { createPublicClient, http } from "viem";
import { arbitrum } from "viem/chains";

const BASE_URL = "https://app.across.to/api";

async function checkRefundStatus(originChainId: number, depositId: number) {
  const params = new URLSearchParams({
    originChainId: originChainId.toString(),
    depositId: depositId.toString(),
  });

  const res = await fetch(`${BASE_URL}/deposit/status?${params}`);
  const data = await res.json();

  switch (data.status) {
    case "pending":
      console.log("Deposit still pending — waiting for fill");
      break;
    case "filled":
      console.log("Deposit filled successfully — no refund needed");
      break;
    case "expired":
      console.log("Deposit expired — refund is being processed");
      console.log("Refund will arrive after bundle settlement (~hours)");
      break;
    case "refunded":
      console.log("Refund complete — funds returned to refund address");
      break;
  }

  return data.status;
}

// Poll until refunded
async function waitForRefund(originChainId: number, depositId: number) {
  const intervalMs = 60_000; // Check every minute for refunds
  const maxAttempts = 360;   // Up to 6 hours

  for (let i = 0; i < maxAttempts; i++) {
    const status = await checkRefundStatus(originChainId, depositId);

    if (status === "refunded") {
      return "refunded";
    }

    if (status === "filled") {
      return "filled"; // Was filled after all — no refund needed
    }

    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }

  throw new Error("Refund polling timed out");
}

For refund polling, use a 60-second interval instead of the 10-second interval used for fill tracking. Refunds take hours, not seconds, so frequent polling is unnecessary.

On this page