Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Quickstart — bind many citizens from one agent

audience: integrators

This quickstart stands up an agent that submits and reads across three block-building lattices, one standalone attestation organism, one confluence, and one module (the coalition’s Atlas) — all bound through a single CoalitionConfig. It assumes familiarity with the builder integrators quickstart when the coalition includes block-building lattices; the coalition layer extends that pattern rather than replacing it.

If the coalition contains no lattices (for example, a composition of oracle organisms plus a cross-oracle confluence), the same pattern applies with different crates on the right-hand side of each handle.

What you need before you start

  • A CoalitionConfig from the coalition operator (struct definition, or a serialised fingerprint you copy into your crate).
  • A LatticeConfig per referenced lattice (inline in the CoalitionConfig the operator published, or pulled from each lattice operator’s handshake page).
  • A standalone-organism reference per referenced organism (role name + stable id + optional content hash + pointer to the organism’s own handshake).
  • A ConfluenceConfig per referenced confluence (from the confluence operator’s handshake page).
  • A ticket from each per-citizen operator you intend to write to. Per-citizen admission is per-citizen; the coalition operator does not issue per-citizen tickets.
  • A ticket from each confluence or module operator whose write-side primitive you intend to use. Usually none: most confluences and modules are read-only for external integrators.
  • Your TDX image (if any citizen or service you write to requires attested client admission).

Step 1 — declare the CoalitionConfig in your crate

Paste the operator’s published CoalitionConfig definition, or import it from the operator’s handshake crate if they publish one:

use builder::LatticeConfig;
use coalition::{CoalitionConfig, ConfluenceConfig, OrganismRef};

// Exactly the bytes the operator publishes.
const ETH_MAINNET:      LatticeConfig<'static> = /* from ethereum.mainnet operator */;
const UNICHAIN_MAINNET: LatticeConfig<'static> = /* from unichain.mainnet operator */;
const BASE_MAINNET:     LatticeConfig<'static> = /* from base.mainnet operator */;

// A standalone organism the coalition references — here,
// an attestation aggregator with its own stable id.
const ATTEST_AGG: OrganismRef<'static> = OrganismRef {
    role: "attest",
    stable_id: /* published by the attest-aggregator operator */,
    content_hash: None,
};

// The coalition's Atlas module (if shipped) and two
// referenced confluences.
const SUPERCHAIN_ATLAS:  ConfluenceConfig<'static> = /* from coalition operator */;
const INTENTS_CONF:      ConfluenceConfig<'static> = /* from confluence operator */;
const LEDGER_CONF:       ConfluenceConfig<'static> = /* from confluence operator */;

pub const ETH_SUPERCHAIN: CoalitionConfig<'static> = CoalitionConfig {
    name: "ethereum.superchain",
    lattices:    &[ETH_MAINNET, UNICHAIN_MAINNET, BASE_MAINNET],
    organisms:   &[ATTEST_AGG],
    confluences: &[SUPERCHAIN_ATLAS, INTENTS_CONF, LEDGER_CONF],
    ticket_issuer: None,
};

For a coalition-agnostic library, take &'static CoalitionConfig<'static> as a constructor argument and let the binary crate pick the constant.

Step 2 — bring up the network handle

Same UNIVERSE as every other mosaik service:

use std::sync::Arc;
use mosaik::Network;
use builder::UNIVERSE;

let network = Arc::new(Network::new(UNIVERSE).await?);

One Arc<Network> per agent regardless of how many citizens, confluences, or modules are bound.

Step 3 — bind the lattice handles you need

Indexes into lattices[] match the order in the CoalitionConfig. A cross-chain searcher that bids on all three:

use offer::Offer;

let eth_bid  = Offer::<Bundle>::bid (&network, &ETH_SUPERCHAIN.lattices[0].offer).await?;
let uni_bid  = Offer::<Bundle>::bid (&network, &ETH_SUPERCHAIN.lattices[1].offer).await?;
let base_bid = Offer::<Bundle>::bid (&network, &ETH_SUPERCHAIN.lattices[2].offer).await?;

let eth_wins  = Offer::<Bundle>::outcomes(&network, &ETH_SUPERCHAIN.lattices[0].offer).await?;
let uni_wins  = Offer::<Bundle>::outcomes(&network, &ETH_SUPERCHAIN.lattices[1].offer).await?;
let base_wins = Offer::<Bundle>::outcomes(&network, &ETH_SUPERCHAIN.lattices[2].offer).await?;

Keep handles alive while needed; drop them when done. Each handle carries its own subscriptions and is cheap atop the shared Network.

Step 4 — bind the standalone-organism handles you need

Each OrganismRef carries the stable id; the concrete organism crate gives you the typed constructor:

use attest::Aggregator;

let attest = Aggregator::<Quote>::read(
    &network, ETH_SUPERCHAIN.organisms[0].config(),
).await?;

OrganismRef::config() resolves to the organism’s Config — either because the coalition operator embeds the full Config struct in their published CoalitionConfig, or because the organism crate ships a helper that rebuilds the Config from the published stable id and known role constants. Either way, the concrete organism crate is what gives you the typed handle.

Step 5 — bind the modules you want

Modules are coalition-scoped organisms with well-known shapes. CoalitionConfig exposes helpers:

use atlas::Atlas;
use almanac::Almanac;

// The Atlas lists every citizen in the coalition with operator metadata.
if let Some(atlas_cfg) = ETH_SUPERCHAIN.atlas() {
    let atlas = Atlas::<CitizenCard>::read(&network, atlas_cfg).await?;
    // Use the atlas as your canonical "what's in this coalition" reference.
}

// The Almanac is a shared tick beacon for timing correlations.
if let Some(almanac_cfg) = ETH_SUPERCHAIN.almanac() {
    let almanac = Almanac::<AlmanacTick>::read(&network, almanac_cfg).await?;
    // Use almanac ticks as the time axis for cross-citizen commits.
    // Each tick's `observed_at` is a Vec<(MemberId, SystemTime)>;
    // compute median or bracket yourself.
}

A coalition may ship zero, one, two, three, or all four modules. Always check the helper’s Option return; do not assume presence.

Step 6 — bind the confluence handles you need

The shared ledger is typical for a searcher seeking aggregated refund attribution:

use ledger::Shared;

let refunds = Shared::<Refund>::read(&network, &ETH_SUPERCHAIN.confluences[2]).await?;

tokio::spawn(async move {
    while let Some(r) = refunds.next().await {
        // Each entry carries (origin_citizen, slot, share, evidence).
        handle_refund(r);
    }
});

Step 7 — submit paired bundles

Cross-chain bundles are a searcher-level problem. The coalition layer surfaces the handles; reconciliation logic is the integrator’s responsibility.

async fn submit_paired_bundle(
    eth_bid: &Offer<Bundle>::Bid,
    uni_bid: &Offer<Bundle>::Bid,
    sell_on_eth: Bundle,
    buy_on_uni:  Bundle,
) -> anyhow::Result<()> {
    let eth_ack = eth_bid.send(sell_on_eth).await?;
    let uni_ack = uni_bid.send(buy_on_uni ).await?;
    // Reconcile: if only one leg commits, post-bond collateral or
    // on-chain HTLC logic applies. The coalition layer does NOT
    // provide cross-citizen atomicity. See
    // /contributors/atomicity.md.
    Ok(())
}

A paired-bundle correlator joining AuctionOutcome[L1, S] to AuctionOutcome[L2, S'] is the most common in-agent utility. Compute it in-memory as in builder’s Shape 1, or rely on a referenced intents confluence if one exists.

Step 8 — read the aggregated result

With a shared-ledger confluence, a single subscription surfaces refunds originating from any of the coalition’s lattices.

while let Some(entry) = refunds.next().await {
    tracing::info!(
        origin = %entry.origin_citizen,
        slot   = entry.slot,
        share  = %entry.share,
        "refund attributed",
    );
}

Without a shared-ledger confluence, open three per- lattice tally::Tally::<Attribution>::read handles and join in memory. Both paths are valid; the confluence is a shared precomputation.

What to do when things go wrong

  • ConnectTimeout on a lattice or organism handle. The per-citizen Config does not match what the citizen’s operator is running. Recompile against that operator’s latest handshake.
  • ConnectTimeout on a confluence or module handle. The ConfluenceConfig does not match, or the committee is down and has not yet published peers on the universe, or the confluence has retired (check for a RetirementMarker on its public stream) and the coalition operator should have sent an updated CoalitionConfig.
  • Admission denied on a write-side stream. Ticket missing or expired. Contact the per-citizen operator, not the coalition operator.
  • Module helper returns None. The coalition does not ship that module. See contributor basic services for the spec if proposing addition.

Cross-references