0-Conf Service

The zero-confirmation (0-conf) service reports how widely a Liquid transaction has propagated across the network. Nodes in configured observation tiers report when they see a transaction in their mempool, and this API aggregates those reports into a single, queryable signal.

Use it as one input when deciding whether to accept a 0-conf payment — especially in swap flows, exchange deposits and other low-latency settlement paths where waiting two minutes (two Liquid confirmations) is too slow.

What this service does not do

It does not tell you a transaction is safe to accept. It reports mempool visibility only. Acceptance policy is entirely application-defined.

Quick start

Query current observation counts for a transaction:

curl "https://<BASE_URL>/api/v1/zeroconf/<txid>"

Example response:

{
  "txid": "<txid>",
  "observations": {
    "bridge": { "seen": 1, "total": 2 },
    "public": { "seen": 0, "total": 8 },
    "functionary": { "seen": 12, "total": 15 }
  }
}

Public endpoints

InterfaceURL
RESThttps://<BASE_URL>/api/v1/zeroconf/:txid
WebSocketwss://<BASE_URL>/ws/v1/zeroconf

How it works

As Liquid's technical provider, Blockstream operates the service. Nodes across observation tiers report when they see a transaction in their mempool; the service aggregates those sightings into tier-level seen / total counts.

sequenceDiagram
  participant N1 as Functionary nodes
  participant N2 as Bridge nodes
  participant N3 as Public nodes
  participant ZC as 0-Conf Service

  Note over N1,N3: Nodes report mempool sightings as they arrive
  N1->>ZC: tx seen
  N2->>ZC: tx seen
  N3->>ZC: tx seen
  ZC->>ZC: Aggregate counts by tier

Your backend reads that aggregated data over REST or WebSocket.

REST

One request returns a snapshot of current observations. Poll if you need updated counts.

sequenceDiagram
  participant App as Your Backend
  participant ZC as 0-Conf Service

  App->>ZC: GET /api/v1/zeroconf/:txid
  ZC-->>App: observations by tier

  Note over App,ZC: Optional: poll again for updated counts
  App->>ZC: GET /api/v1/zeroconf/:txid
  ZC-->>App: updated observations

WebSocket

Subscribe once and receive a snapshot immediately, then push updates whenever observation counts change.

sequenceDiagram
  participant App as Your Backend
  participant ZC as 0-Conf Service
  participant Nodes as Observation nodes

  App->>ZC: Connect wss://.../ws/v1/zeroconf
  App->>ZC: { "action": "subscribe", "txid": "<txid>" }
  ZC-->>App: { "action": "subscribed", ... }
  ZC-->>App: { "action": "snapshot", "observations": ... }

  Nodes->>ZC: More nodes report tx seen
  ZC-->>App: { "action": "snapshot", "observations": ... }

  Note over App: Coverage meets threshold — accept 0-conf

  App->>ZC: { "action": "unsubscribe", "txid": "<txid>" }

You query one API instead of building bespoke federation monitoring.

Observation tiers

Each API response includes an observations object. Keys are tier names; values report how many nodes in that tier have seen the transaction:

FieldMeaning
seenNodes in this tier that reported the transaction in their mempool
totalNodes configured for this tier

Tier names and node counts may change. Write integration logic against the seen / total ratio rather than hard-coded values.

Interpreting coverage

A common pattern is to require a minimum fraction of functionary nodes:

functionary_coverage = observations.functionary.seen / observations.functionary.total
accept if functionary_coverage >= threshold

REST vs WebSocket

Both interfaces return the same observation data. Choose based on how you consume updates.

RESTWebSocket
Best forOne-off checks, cron jobs, low-frequency lookupsWaiting for coverage thresholds in real time
ConnectionStateless HTTPPersistent connection
UpdatesSnapshot at request time; poll for changesSnapshot on subscribe, then push on change
ComplexityMinimalHandle connect, reconnect, resubscribe
LatencyPolling interval adds delayPush updates as observations change

Use REST when you check once before accepting, or query status infrequently.

Use WebSocket when you subscribe after broadcast and act as soon as seen counts cross your threshold — typical in swap and deposit flows.

REST interface

GET /api/v1/zeroconf/:txid

Returns the transaction ID and current observation counts.

Parameters

NameInTypeDescription
txidpathstring64-character hex transaction ID

Example

curl "https://<BASE_URL>/api/v1/zeroconf/a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890"

Response (200)

{
  "txid": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890",
  "observations": {
    "bridge": { "seen": 1, "total": 2 },
    "public": { "seen": 0, "total": 8 },
    "functionary": { "seen": 12, "total": 15 }
  }
}

WebSocket interface

WS /ws/v1/zeroconf

Open a WebSocket connection, then send JSON messages to manage subscriptions.

Client messages

Subscribe

Start watching a txid (repeat for multiple txids on one connection):

{ "action": "subscribe", "txid": "<txid>" }
Unsubscribe

Stop watching a txid:

{ "action": "unsubscribe", "txid": "<txid>" }

Server messages

actionWhen sentPayload
subscribedSubscription createdtxid, message
snapshotOn subscribe and whenever observations changetxid, observations
expiredSubscription timed outtxid, message
errorInvalid requestreason, message
Subscribed
{
  "action": "subscribed",
  "txid": "<txid>",
  "message": "subscription created"
}
Snapshot / update
{
  "action": "snapshot",
  "txid": "<txid>",
  "observations": {
    "bridge": { "seen": 1, "total": 2 },
    "public": { "seen": 0, "total": 8 },
    "functionary": { "seen": 12, "total": 15 }
  }
}
Expired

Subscriptions expire after a server-configured timeout. Re-subscribe if you still need updates.

{
  "action": "expired",
  "txid": "<txid>",
  "message": "subscription expired"
}
Error
{
  "action": "error",
  "reason": "bad_txid",
  "message": "TXID must be 64 hex characters"
}

Example acceptance policy

This is illustrative — tune thresholds to your risk model:

function shouldAcceptZeroConf(observations, amountSats) {
  const fn = observations.functionary;
  if (!fn || fn.total === 0) return false;

  const coverage = fn.seen / fn.total;

  return coverage >= 0.67;
}

Using the API from code

JavaScript (REST)

const BASE_URL = process.env.LIQUID_ZEROCONF_URL; // e.g. https://<BASE_URL>

async function getZeroConfObservations(txid) {
  const res = await fetch(`${BASE_URL}/api/v1/zeroconf/${txid}`);
  if (!res.ok) throw new Error(`0-conf lookup failed: ${res.status}`);
  return res.json();
}

JavaScript (WebSocket)

const WS_BASE_URL = process.env.LIQUID_ZEROCONF_WS_URL;

function watchZeroConf(txid, { onSnapshot, onError }) {
  const ws = new WebSocket(`${WS_BASE_URL}/ws/v1/zeroconf`);

  ws.onopen = () => {
    ws.send(JSON.stringify({ action: "subscribe", txid }));
  };

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    if (msg.action === "snapshot") onSnapshot(msg.observations);
    if (msg.action === "error") onError(msg);
    if (msg.action === "expired") {
      ws.send(JSON.stringify({ action: "subscribe", txid }));
    }
  };

  return () => {
    ws.send(JSON.stringify({ action: "unsubscribe", txid }));
    ws.close();
  };
}

Data retention

The service holds recent mempool visibility data in memory. It is not a long-term transaction index.

  • Results may be missing or incomplete for transactions that already confirmed
  • Very old mempool transactions may have been evicted from the service
  • Use Esplora for confirmed transaction history and status

Next steps


Did this page help you?

The Liquid Network is a Bitcoin layer-2 enabling the issuance of security tokens and other digital assets.

© 2023 Liquid Network
All rights reserved.

Feedback and Content Requests

We'd be happy to hear your suggestions on how we can improve this site.

BuildOnL2 Community

The official BuildOnL2 community lives
at community.liquid.net. Join us and build the future of Bitcoin on Liquid.

Telegram

Community-driven telegram group where
most of the Liquid developers hang out.
Go to t.me/liquid_devel to join.