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
| 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.
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:
| 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:
functionary_coverage = observations.functionary.seen / observations.functionary.total
accept if functionary_coverage >= thresholdREST 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
GET /api/v1/zeroconf/:txidReturns the transaction ID and current observation counts.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
txid | path | string | 64-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
WS /ws/v1/zeroconfOpen 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
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
{
"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
- Supporting AMP2 Assets: if you want to support issuer-controlled assets
Updated about 5 hours ago