BchainPay logoBchainPay
EngineeringL2ArbitrumReliabilitySequencer

L2 Sequencer Downtime: Forced Inclusion for Payment Gateways

Detect L2 sequencer outages via Chainlink uptime feeds, trigger delayed-inbox forced inclusion, and keep Arbitrum, Optimism, and Base payment flows resilient.

By Cipher · Founding engineer, BchainPay10 min read
Illustration for L2 Sequencer Downtime: Forced Inclusion for Payment Gateways

The Arbitrum sequencer went silent for roughly an hour in late 2023. Optimism has had similar incidents. Base has had brief hiccups. When a sequencer stops accepting transactions, payments submitted against it hang in a state that is neither confirmed nor failed — the worst possible outcome for a checkout flow. The user sees nothing. The merchant sees nothing. Your internal monitoring sees a transaction that was never sequenced.

This post covers three things: how to detect a sequencer outage fast, how to force-include transactions through L1 when the sequencer is unavailable, and how to design your payment gateway's state machine to handle both the outage and the recovery window.

Why this failure mode is nastier than a normal node outage#

When an RPC node fails, you get connection errors, timeouts, or stale responses. You already have failover logic for that — switch to a secondary RPC, retry the submission, and the transaction either lands or you get an explicit rejection.

When the sequencer itself is down, the L1 contracts are still functioning normally. The chain's canonical state keeps advancing via the batch-posting and fraud-proof window. But no new transactions are being ordered. Your transactions queue in your local mempool, in the user's wallet, or get rejected by the sequencer with a rate-limit or timeout error that is easy to misread as a transient RPC glitch.

The particularly nasty variant: a sequencer that accepts your transaction but stops publishing batches. The transaction appears pending to your local node, which is still processing the sequencer's last known state. You never see a rejection; you just never see a receipt.

Detecting a sequencer outage#

Chainlink publishes a Sequencer Uptime Feed on every major EVM L2. These are on-chain Aggregator contracts reporting whether the sequencer is up or down, updated within seconds of a confirmed outage by the oracle network.

Contract addresses (verified against Chainlink docs, April 2026):

Chain Sequencer Uptime Feed
Arbitrum One 0xFdB631F5EE196F0ed6FAa767959853A9F217697D
Optimism 0x371EAD81c9102C9BF4874A9075FFFf170F2D5C68
Base 0xBCF85224fc0756B9Fa45aA7892530B47e10b6082

The feed returns a standard latestRoundData() response. The answer field is 0 when the sequencer is up, 1 when it is down. The startedAt timestamp records when the current status was first reported — critical for distinguishing a fresh outage from one that has been running for two hours.

import { createPublicClient, http, parseAbi } from "viem";
import { arbitrum } from "viem/chains";
 
const SEQUENCER_FEED = "0xFdB631F5EE196F0ed6FAa767959853A9F217697D";
 
const FEED_ABI = parseAbi([
  "function latestRoundData() external view returns " +
    "(uint80 roundId, int256 answer, uint256 startedAt, " +
    "uint256 updatedAt, uint80 answeredInRound)",
]);
 
// IMPORTANT: use an L1 RPC here, not the L2 RPC.
// If the L2 sequencer is down, the L2 RPC may be unreachable.
const l1Client = createPublicClient({
  chain: arbitrum,
  transport: http(process.env.L1_RPC_URL),
});
 
async function isSequencerDown(): Promise<{ down: boolean; since: Date }> {
  const [, answer, startedAt] = await l1Client.readContract({
    address: SEQUENCER_FEED,
    abi: FEED_ABI,
    functionName: "latestRoundData",
  });
 
  return {
    down: answer === 1n,
    since: new Date(Number(startedAt) * 1000),
  };
}

The grace period after recovery#

Chainlink's own documentation recommends a grace period of 3,600 seconds after a sequencer comes back online before you re-enable dependent logic. For payment gateways, a 10-to-20 minute window balances caution with user experience: L2 nodes need time to re-sync the sequencer's backlog, and oracle price feeds that paused during the outage need to re-warm.

Scale the grace period to your transaction value floor: a $50 checkout can accept 10 minutes; a $50,000 treasury sweep should wait the full hour.

const GRACE_SECONDS = 15 * 60; // 15 minutes
 
async function isSequencerSafe(): Promise<boolean> {
  const { down, since } = await isSequencerDown();
  if (down) return false;
 
  const secondsSinceRecovery = (Date.now() - since.getTime()) / 1000;
  return secondsSinceRecovery >= GRACE_SECONDS;
}

Forced inclusion: the L1 escape hatch#

Every optimistic rollup with a correctly designed trust model must allow users to bypass the sequencer and submit transactions directly to L1. Without this, a malicious or offline sequencer could censor transactions permanently. Both Arbitrum and the OP Stack implement forced inclusion, with different mechanics and very different latency profiles.

Arbitrum: the Delayed Inbox#

Arbitrum exposes a Inbox contract on Ethereum mainnet. Calling sendL2Message on it enqueues your transaction in the Delayed Inbox. The sequencer is required by protocol to include it within the "force-inclusion timeout" — currently around 24 hours for Arbitrum One. After that window passes without inclusion, anyone can call forceInclusion to push it through.

This is not fast. Forced inclusion is a censorship-resistance backstop, not a latency optimization. For a payment gateway, the relevant scenario is: the sequencer has been down long enough that you need a guarantee the transaction eventually lands rather than being silently lost.

import { parseEther, parseAbi } from "viem";
 
// Arbitrum Inbox on Ethereum mainnet (Arbitrum One)
const L1_INBOX = "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f";
 
const INBOX_ABI = parseAbi([
  "function sendL2Message(bytes calldata messageData) external returns (uint256)",
]);
 
/**
 * encodedL2Tx: RLP-encoded L2 transaction bytes
 * (use @arbitrum/sdk ArbitrumProvider.getRawTransaction to produce this)
 */
async function submitViaDelayedInbox(
  l1WalletClient: any,
  encodedL2Tx: `0x${string}`
): Promise<`0x${string}`> {
  return l1WalletClient.writeContract({
    address: L1_INBOX,
    abi: INBOX_ABI,
    functionName: "sendL2Message",
    args: [encodedL2Tx],
    value: parseEther("0.001"), // L1 gas donation included with the message
  });
}

Optimism and Base: OptimismPortal.depositTransaction#

The OP Stack uses OptimismPortal.depositTransaction() for L1-forced inclusion. Unlike Arbitrum's 24-hour window, OP Stack deposits are required to appear in the next available L2 block — typically within 1-2 minutes. The mechanism is faster but uses a separate deposit queue with different gas semantics from regular L2 transactions.

// OptimismPortal on Ethereum mainnet (OP Mainnet)
const OP_PORTAL = "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed";
 
const PORTAL_ABI = parseAbi([
  "function depositTransaction(" +
    "address _to, uint256 _value, uint64 _gasLimit, " +
    "bool _isCreation, bytes calldata _data) external payable",
]);
 
async function forceIncludeOnOptimism(
  l1WalletClient: any,
  to: `0x${string}`,
  calldata: `0x${string}`,
  gasLimit: bigint
): Promise<`0x${string}`> {
  return l1WalletClient.writeContract({
    address: OP_PORTAL,
    abi: PORTAL_ABI,
    functionName: "depositTransaction",
    args: [to, 0n, gasLimit, false, calldata],
    value: 0n,
  });
}

For Base, swap OP_PORTAL for Base's portal address (0x49048044D57e1C92A77f79988d21Fa8fAF74E97e on mainnet) — the ABI is identical.

Payment gateway state machine#

Normal L2 payment flows have two states: pending and confirmed. A sequencer-aware gateway needs at least six:

type L2PaymentStatus =
  | "pending"               // submitted to sequencer, awaiting receipt
  | "sequencer_down"        // outage detected before or during submission
  | "pending_grace_period"  // sequencer recovered, within the cooldown window
  | "forced_inclusion"      // submitted to L1 inbox/portal, awaiting L2 inclusion
  | "confirmed"             // L2 tx mined with sufficient confirmations
  | "expired";              // outage exceeded payment TTL; re-issue required

The transition rules:

pending ──── sequencer detected down ──► sequencer_down
                                              │
                    outage > payment_ttl ─────┼──► expired
                                              │
               sequencer recovers ───────────►│
                                       pending_grace_period
                                              │
                grace window ends ────────────┼──► forced_inclusion ──► confirmed
                                              │
         (re-queue if grace window ends) ─────┘

The expired state is important. If the outage lasts longer than your payment's time-to-live — typically 15-30 minutes for a checkout session — the merchant experience is cleaner if you expire the payment and issue a new one when the sequencer recovers, rather than force-including a stale transaction that may reference outdated pricing or an abandoned user session.

const FORCE_INCLUDE_THRESHOLD_MS = 10 * 60 * 1000;  // 10 minutes
const PAYMENT_TTL_MS              = 20 * 60 * 1000;  // 20 minutes
 
async function handlePendingPayment(payment: Payment): Promise<void> {
  const outageMs = Date.now() - payment.sequencerDownAt.getTime();
 
  if (outageMs >= PAYMENT_TTL_MS) {
    await updatePaymentStatus(payment.id, "expired");
    return;
  }
 
  if (outageMs >= FORCE_INCLUDE_THRESHOLD_MS && !payment.forcedInclusionQueued) {
    const l1Hash = await submitViaDelayedInbox(l1Client, payment.encodedTx);
    await updatePaymentStatus(payment.id, "forced_inclusion", { l1Hash });
  }
}

BchainPay API integration#

BchainPay surfaces sequencer state through the payment object's l2_sequencer field:

{
  "id": "pay_3Qx8kWrnT9aM",
  "status": "forced_inclusion",
  "chain": "arbitrum",
  "l2_sequencer": {
    "status": "down",
    "down_since": "2026-04-27T14:12:05Z",
    "force_inclusion_queued": true,
    "l1_tx_hash": "0xabc123…",
    "estimated_inclusion_at": "2026-04-27T14:22:00Z"
  }
}

Webhook event payment.sequencer_state_changed fires on each status transition. Subscribe to it to drive frontend updates — replace the generic "Waiting for confirmation" spinner with "Sequencer outage detected. Your payment is secured on L1 and will process shortly." once force_inclusion_queued is true. A factual status message during an outage converts a support ticket into a handled experience.

To query current L2 health across all chains:

curl https://api.bchainpay.com/v1/network/health \
  -H "Authorization: Bearer $BCHAINPAY_KEY" \
  | jq '.l2s[] | {chain, sequencer_status, down_since}'

Monitoring setup#

Poll the Chainlink feed every 30 seconds from your backend using a dedicated L1 RPC — not from the L2 RPC, which may be unreachable during the outage itself. This is the most common operational mistake: teams that check sequencer status via the L2 endpoint see the check itself time out and mistake it for a connectivity problem rather than a sequencer problem.

# Cron probe, 30-second interval
0/1 * * * * curl -sf https://internal.bchainpay.com/probe/l2-sequencer \
  | grep -q '"down":true' && alert pagerduty "l2_sequencer_down"

Configure two alert thresholds:

  • Warn at 2 minutes of confirmed downtime — enough to filter false positives from brief oracle update gaps.
  • Page at 5 minutes — activates the forced-inclusion pipeline and triggers user-facing status page updates.

The mean time to recovery across documented Arbitrum and Optimism incidents has been under 45 minutes. A payment gateway SLA of "settlement within 90 minutes" is achievable with forced inclusion as the backstop. "Within 5 minutes" is not, and should be disclosed as dependent on sequencer liveness in your merchant agreement.

What we got wrong#

A few things we had to learn after shipping:

  1. Checking sequencer status via the L2 RPC. We initially polled the Chainlink feed via the Arbitrum RPC. When the sequencer went down, the feed check timed out, and our alert fired as "RPC node down" not "sequencer down" — two entirely different runbooks. The fix: route all sequencer health checks through a dedicated L1 endpoint.

  2. Not enforcing the grace period. After the sequencer recovered, we re-enabled payments immediately. Two merchants had in-flight ERC-20 transfers that referenced a stale oracle price from before the outage. The grace period exists for this reason: wait for oracle feeds to re-warm, not just for the sequencer to come back online.

  3. Exposing "forced inclusion" as a status to end users. Merchants do not know what this means. The right UX is a plain-English status message and a realistic ETA. The internal state machine label is for your engineers.

  4. Using the same PAYMENT_TTL for all payment types. Subscription renewals and high-value B2B invoices can have a 2-hour TTL without degrading UX. Checkout sessions should expire in 15-20 minutes. Do not use a single constant.

Key takeaways#

  • L2 sequencer outages are a production reality. Every major optimistic rollup has logged at least one multi-minute incident.
  • Chainlink Sequencer Uptime Feeds are the fastest on-chain detection signal. Poll them on a 30-second cycle, always via an L1 RPC — not the L2 RPC.
  • Enforce a 10-to-20 minute grace period after a sequencer comes back online. Oracle price feeds need time to re-warm; payments submitted immediately after recovery can reference stale state.
  • Arbitrum's Delayed Inbox provides a 24-hour inclusion guarantee at the cost of high latency. OP Stack's depositTransaction is faster (1-2 minutes) but uses a separate deposit queue.
  • Model at least six payment states: pending, sequencer_down, pending_grace_period, forced_inclusion, confirmed, and expired.
  • Expire short-TTL payments rather than force-including them if the outage exceeds the session window. A fresh payment after recovery is cleaner than a stale forced-inclusion.
  • Never check sequencer status through the L2 RPC. If the sequencer is down, the RPC check will fail for a different reason and fire the wrong alert.

Try it yourself

Spin up a sandbox merchant in under 60 seconds.

One REST endpoint, signed webhooks, five chains. No credit card required.

Related reading