Skip to main content
Adapters translate between NTL signals and external protocols.

The two things to know first

The trait is synchronous. ntl-core contains no async fn at all, holds no async runtime, and reaches for no ambient clock or randomness. That is what keeps it building for wasm32-unknown-unknown, which CI enforces on every commit. There is no .await anywhere on this page, and the trait has no start or stop. No adapter is implemented. adapters/web2, adapters/web3 and adapters/legacy each contain a single doc comment and no code, and all three are publish = false. There is no Web2Adapter, no Web3Adapter, no LegacyAdapter, and no node.register_adapter. Nothing on this page can be configured or run today; what it gives you is the contract to implement.
The trait is real; the adapters are not. The Adapter trait in ntl-core is normative (spec/adapter-contract) and exercised by the test suite. The adapter guides describe intended behaviour on top of it, and their configuration keys are proposed rather than parsed — no [adapter.*] table is read by any binary.Tracking: openNTL/ntl#14.
The crate is ntl_core — note the underscore in Rust paths. Adapter and AdapterHealth are re-exported at the root; ExternalPayload, Protocol and AdapterCapabilities live in ntl_core::adapter.

The Adapter trait

Five methods, all synchronous, quoted from runtime/ntl-core/src/adapter.rs:
crate::Result<T> is Result<T, ntl_core::Error>; translation failures are Error::Adapter(String). The Send + Sync bound and the &self receivers together mean an adapter holding mutable state — a sequence counter, an injected Rng — needs interior mutability. The implementation below uses a Mutex.

Types

All four are defined in runtime/ntl-core/src/adapter.rs and are shown here with their doc comments elided.

ExternalPayload

The generic container for external protocol data.

AdapterCapabilities

Declared by the adapter, not inferred by the runtime.

Protocol

AdapterHealth

Implementing an adapter

Every sample below is compiled and executed by runtime/ntl-core/tests/api_reference_adapter.rs. If the trait changes, that test stops building — which is the only way documentation like this stays true. Since no adapter ships, that file is also the reference implementation.

The struct

Time and randomness are fields, not ambient calls. ManualClock stands in for whatever clock the host injects; a production adapter would hold Arc<dyn Clock>.

ingest

An adapter builds an unsigned signal. It does not sign, and it does not choose the origin identity or the emission timestamp — Node::emit stamps those. build_unsigned_with takes the clock and RNG explicitly for exactly that reason.
The topic becomes the signal’s first tag, which is how Signal::data("...") behaves everywhere else in the crate.

emit

The metadata methods

Declare correlation: false unless the adapter really does hold the external connection open and match responses by correlation_id; the contract attaches requirements to that flag, including a timeout, in spec/adapter-contract.

Using one

The trait is object-safe, so an adapter can be held as &dyn Adapter. Nothing in ntl-core holds a registry of adapters — whoever owns the transport owns the adapter and calls ingest and emit directly.

What is not here yet

The last three are places where spec/adapter-contract is ahead of the code: it is normative about a registration and lifecycle surface that does not exist in Rust yet, and it will arrive with the first real adapter. Until then, cargo doc --open and the test above are the authority for the Rust API.
Last modified on September 11, 2026