---
updatedAt: 2026-07-22T15:38:41.000Z
agentTools:
  projectIndex: https://docs.liquid.net/llms.txt
---

# 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:

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

Example response:

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

## Public endpoints

| Interface     | URL                                        |
| ------------- | ------------------------------------------ |
| **REST**      | `https://<BASE_URL>/api/v1/zeroconf/:txid` |
| **WebSocket** | `wss://<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.

```mermaid
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.

```mermaid
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.

```mermaid
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:

| Field   | Meaning                                                           |
| ------- | ----------------------------------------------------------------- |
| `seen`  | Nodes in this tier that reported the transaction in their mempool |
| `total` | Nodes 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:

```plaintext
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.

|                | REST                                             | WebSocket                                    |
| -------------- | ------------------------------------------------ | -------------------------------------------- |
| **Best for**   | One-off checks, cron jobs, low-frequency lookups | Waiting for coverage thresholds in real time |
| **Connection** | Stateless HTTP                                   | Persistent connection                        |
| **Updates**    | Snapshot at request time; poll for changes       | Snapshot on subscribe, then push on change   |
| **Complexity** | Minimal                                          | Handle connect, reconnect, resubscribe       |
| **Latency**    | Polling interval adds delay                      | Push 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

| Name   | In   | Type   | Description                     |
| ------ | ---- | ------ | ------------------------------- |
| `txid` | path | string | 64-character hex transaction ID |

#### Example

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

#### Response (200)

```json
{
  "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):

```json
{ "action": "subscribe", "txid": "<txid>" }
```

##### Unsubscribe

Stop watching a txid:

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

#### Server messages

| `action`     | When sent                                     | Payload                |
| ------------ | --------------------------------------------- | ---------------------- |
| `subscribed` | Subscription created                          | `txid`, `message`      |
| `snapshot`   | On subscribe and whenever observations change | `txid`, `observations` |
| `expired`    | Subscription timed out                        | `txid`, `message`      |
| `error`      | Invalid request                               | `reason`, `message`    |

##### Subscribed

```json
{
  "action": "subscribed",
  "txid": "<txid>",
  "message": "subscription created"
}
```

##### Snapshot / update

```json
{
  "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.

```json
{
  "action": "expired",
  "txid": "<txid>",
  "message": "subscription expired"
}
```

##### Error

```json
{
  "action": "error",
  "reason": "bad_txid",
  "message": "TXID must be 64 hex characters"
}
```

### Example acceptance policy

This is illustrative — tune thresholds to your risk model:

```javascript
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)

```javascript
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)

```javascript
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](/docs/esplora-api) for confirmed transaction history and status

## Next steps

* [Supporting AMP2 Assets](/docs/support-amp2): if you want to support issuer-controlled assets