Skip to content

Live Substrate Event Streams

FranklinClient.subscribe(subject) is an async iterator yielding newly-appended substrate rows. Polling fallback works without NATS; full NATS bus path activates when ~/.gaiaftcl/nats.toml or GAIAFTCL_NATS_URL is configured.


Subjects

NATS subject Substrate table When it fires
gaiaftcl.substrate.franklin_heartbeat.sealed franklin_heartbeat_history Per heartbeat tick (60s default)
gaiaftcl.qc020.franklin.window_filtering.sealed franklin_window_filtering Per substrate measurement window-close
gaiaftcl.qc020.substrate.joint_variation_evidence.sealed qc020_joint_variation_evidence Per substrate measurement
gaiaftcl.substrate.healing_action.sealed substrate_healing_actions Per heal-cycle action

Wildcard matching follows NATS conventions (* for one token, > for tail).


Async iterator pattern

import asyncio
from gaiaftcl import FranklinClient

async def watch_heartbeats():
    with FranklinClient.connect() as franklin:
        async for event in franklin.subscribe(
                "gaiaftcl.substrate.franklin_heartbeat.sealed"):
            row = event["row"]
            print(f"{row['tick_at_iso']} "
                  f"obs={row['observations_count']} "
                  f"sov_auth={row['commissions_franklin_authorized']}")

asyncio.run(watch_heartbeats())

Output (one line per Franklin tick, ~60s cadence):

2026-06-01T16:27:30Z obs=654 sov_auth=0
2026-06-01T16:28:30Z obs=655 sov_auth=0
2026-06-01T16:29:30Z obs=656 sov_auth=0

Watching substrate measurements

V178 fires per substrate measurement (~3–4 measurements per second under joint variation):

import asyncio
from gaiaftcl import FranklinClient

async def watch_v178():
    with FranklinClient.connect() as franklin:
        async for event in franklin.subscribe(
                "gaiaftcl.qc020.substrate.joint_variation_evidence.sealed"):
            row = event["row"]
            if row["leading_zero_nibble_count"] > 1:
                print(f"interesting: lz={row['leading_zero_nibble_count']} "
                      f"extranonce={row['cursor_extranonce']}")

asyncio.run(watch_v178())

Verifying every event as it arrives

import asyncio
from gaiaftcl import FranklinClient
from gaiaftcl.federation.cosignature import verify_signature_quintet

async def watch_and_verify():
    with FranklinClient.connect() as franklin:
        async for event in franklin.subscribe(
                "gaiaftcl.substrate.franklin_heartbeat.sealed"):
            row = event["row"]
            ok = verify_signature_quintet(
                row["canonical_witness"],
                row["witness_hash_sha256"],
                row["signature_quintet"],
            )
            mark = "✓" if ok else "✗"
            print(f"{mark} {row['tick_at_iso']} {row['heartbeat_id']}")

asyncio.run(watch_and_verify())

Polling fallback

When NATS is unavailable, the client polls the substrate's SQLite at 1-second cadence and yields newly-appended rows by row_id ordering. Behavior is equivalent to the NATS path for read-only consumers.

Polling watermarks reset at subscribe() call time — only events newer than the call yield. To replay historical events, query the substrate read methods (heartbeat_history, research_telemetry, etc.) and process the returned list.


NATS bus configuration

~/.gaiaftcl/nats.toml:

[nats]
url = "nats://localhost:4222"
credentials = "/etc/gaiaftcl/nats.creds"

Or GAIAFTCL_NATS_URL environment variable.

When NATS is configured, the client connects on first subscribe() call; subsequent subscriptions reuse the connection.


Substrate cadence is yours to consume. Every event verifies; every event is replay-bit-exact.


Federation-cosigned

This page's source is sealed in the GaiaFTCL federation manifest — page SHA-256 527762dad39e56ea…, manifest witness a090592e0609adc8…, signed 2026-06-02T18:58:22Z by cell gaiaftcl-mac-cell. Verify with gaiaftcl wiki sign --all and compare wiki-all-signatures.json.