Sign inSign up
adder logo

blinklabs/adder

By blinklabs

•Updated about 16 hours ago

Adder is a tool for tailing the Cardano blockchain and emitting events

Image
0

10K+

blinklabs/adder repository overview

⁠Adder

Adder Logo

Adder is a tool for tailing the Cardano blockchain and emitting events for each block and transaction that it sees.

⁠How it works

Choose an input: chainsync reads a Cardano node over NtC or NtN, mempool polls the node's local transaction monitor, and utxorpc reads a FollowTip or WatchTx stream. The CLI selects one input and one output; the defaults are chainsync and log.

Events pass through the Cardano and event-type filters before reaching the selected output and API observer. Outputs apply backpressure; API delivery uses bounded buffers and can drop events when subscribers are slow.

Adder event flow: input plugins, ordered filters, output plugins, and API subscribers

⁠Event format

Events are created with a simple schema.

{
  "type": "event type",
  "timestamp": "wall clock timestamp of event",
  "context": "metadata about the event",
  "payload": "the full event specific payload"
}

The chainsync input produces input.block, input.rollback, input.transaction, input.governance, input.drep-registration, input.drep-update, and input.drep-retirement. The last three share the DRep certificate payload⁠. The abbreviated examples below show field shapes with placeholder values, not recorded ledger data. CBOR fields are included only when --input-chainsync-include-cbor is enabled.

input.block:

{
  "context": {
    "blockNumber": 123,
    "slotNumber": 1234567
  },
  "payload": {
    "blockBodySize": 123,
    "issuerVkey": "a712f81ab2eac...",
    "blockHash": "abcd123...",
    "blockCbor": "85828a1a000995c21..."
  }
}

input.rollback:

{
  "payload": {
    "blockHash": "abcd123...",
    "slotNumber": 1234567
  }
}

input.transaction:

{
    "context": {
        "blockNumber": 123,
        "slotNumber": 1234567,
        "transactionHash": "0deadbeef123...",
        "transactionIdx": 0
    },
    "payload": {
        "blockHash": "abcd123...",
        "transactionCbor": "a500828258200a1ad...",
        "inputs": [
          "abcdef123...#0",
          "abcdef123...#1"
        ],
        "outputs": [
            {
                "address": "addr1qwerty123...",
                "amount":  12345687,
                "assets": [
                    {
                        "name": "Foo",
                        "nameHex": "abcd123...",
                        "amount": 123,
                        "fingerprint": "asset1abcd...",
                        "policyId": "54321..."
                    }
                ]
            }
        ],
        "metadata": {
            "674": {
                "msg": [
                    "Test message"
                ]
            }
        },
        "fee": 1234567,
        "ttl": 123
    }
}

input.governance:

{
    "context": {
        "transactionHash": "1234abcd1234abcd...",
        "blockNumber": 123,
        "slotNumber": 1234567,
        "transactionIdx": 0,
        "networkMagic": 1
    },
    "payload": {
        "blockHash": "abcd123...",
        "transactionCbor": "a500828258200a1ad...",
        "proposalProcedures": [
            {
                "index": 0,
                "deposit": 1000000000,
                "rewardAccount": "stake1u9abcd...",
                "actionType": "ParameterChange",
                "actionData": {
                    "parameterChange": {
                        "prevActionId": {
                            "transactionId": "prev_tx_hash...",
                            "govActionIdx": 0
                        },
                        "policyHash": "abcd1234...",
                        "paramUpdate": {
                            "minFeeA": 44,
                            "maxTxSize": 16384
                        }
                    }
                },
                "anchor": {
                    "url": "https://example.com/proposal.json",
                    "dataHash": "abcd1234..."
                }
            }
        ],
        "votingProcedures": [
            {
                "voterType": "DRep",
                "voterHash": "81f156d98e1f02123abccdef5439a89d71fa9d8b76c8db028c7df0e1",
                "voterId": "drep1abcd...",
                "govActionTxId": "action_tx_hash...",
                "govActionIndex": 0,
                "vote": "Yes",
                "anchor": {
                    "url": "https://example.com/vote-rationale.json",
                    "dataHash": "81f156d98e1f02123abccdef5439a89d71fa9d8b76c8db028c7df0e1..."
                }
            }
        ],
        "drepCertificates": [
            {
                "certificateType": "Registration",
                "drepHash": "81f156d98e1f02123abccdef5439a89d71fa9d8b76c8db028c7df0e1",
                "drepId": "drep1abcd...",
                "deposit": 500000000,
                "anchor": {
                    "url": "https://example.com/drep.json",
                    "dataHash": "81f156d98e1f02123abccdef5439a89d71fa9d8b76c8db028c7df0e1..."
                }
            }
        ],
        "voteDelegationCertificates": [
            {
                "certificateType": "VoteDelegation",
                "stakeCredential": "81f156d98e1f02123abccdef5439a89d71fa9d8b76c8db028c7df0e1",
                "drepType": "KeyHash",
                "drepHash": "81f156d98e1f02123abccdef5439a89d71fa9d8b76c8db028c7df0e1",
                "drepId": "drep1abcd..."
            }
        ],
        "committeeCertificates": [
            {
                "certificateType": "AuthHot",
                "coldCredential": "81f156d98e1f02123abccdef5439a89d71fa9d8b76c8db028c7df0e1",
                "hotCredential": "81f156d98e1f02123abccdef5439a89d71fa9d8b76c8db028c7df0e1..."
            }
        ]
    }
}

For detailed information about the governance schemas, supported actions, and fields, see the Governance Event Documentation⁠.

⁠Log output plugin

log is the default output plugin. It writes each filtered event as one text or JSON line. --output-log-level (environment OUTPUT_LOG_LEVEL, YAML plugins.output.log.level) controls both event output and application diagnostics. The default is info; accepted levels are debug, info, warn, and error. Event records are INFO-level, so warn and error consume events without writing them. API event delivery continues at every log level. Diagnostics go to stderr at their actual severity, including when another output plugin is selected.

For errors-only logging:

adder --output-log-level error

Each event is output individually. The log output supports two formats:

  • text (default) — human-readable, one line per event:

    2026-05-24 12:00:00 BLOCK        slot=2000       block=100      hash=abc123hash era=Babbage txs=5 size=1024
    2026-05-24 12:00:01 TX           slot=2000       block=100      tx=deadbeef12345678 fee=180000 inputs=2 outputs=3
    2026-05-24 12:00:02 ROLLBACK     slot=2000       hash=aabbccdd11223344
    2026-05-24 12:00:03 GOVERNANCE   slot=2000       block=100      tx=govtx12345678abc proposals=1 votes=2 certs=1
    

    These examples use placeholder values. Governance logs appear for qualifying transactions; rollback logs appear when the node reports a rollback. --filter-type input.block excludes the other event types.

  • json — newline-delimited JSON, one JSON object per event (suitable for piping to jq or other tooling). This abbreviated example is expanded for readability; the actual output occupies one line:

    {
      "type": "input.block",
      "timestamp": "2026-02-07T09:18:40Z",
      "context": { "blockNumber": 9876543, "slotNumber": 12345678 },
      "payload": { "blockHash": "abc12345..." }
    }
    

Select the format with --output-log-format:

adder --output-log-format json

Event data is written to stdout and application logs are written to stderr. Use --output-log-path to write events to a file instead. This means you can capture only event output:

# Save events to a file, see app logs in terminal
adder > events.txt

# Pipe events to jq, suppress app logs
adder --output-log-format json 2>/dev/null | jq .

# See only app logs, discard event data
adder > /dev/null
⁠Target-aware notification stream

Desktop integrations can reuse the same target-aware rules as adder-tray without linking a GUI toolkit. The notify-json output reserves stdout for versioned newline-delimited status and notification records and writes runtime logs to stderr:

adder \
  --input chainsync \
  --input-chainsync-network preview \
  --output notify-json \
  --output-notify-json-config ~/.config/adder/omarchy.json \
  --api-address 127.0.0.1 \
  --api-port 0

The Chainsync network must match network.name in the notification JSON. When using a custom node, pass the same host:port with --input-chainsync-address; Adder rejects mismatches before starting. The compatibility check uses the resolved JSON path, network, and address, following CLI > environment > YAML > defaults.

Validate the configuration before starting or restarting a frontend:

adder notifications validate \
  --config ~/.config/adder/omarchy.json \
  --json

The JSON configuration has schemaVersion: 1 and describes the Cardano network, monitored wallets/DReps/pools/assets/policies, alert categories, rate limit, and connection-staleness threshold. An empty target set is rejected unless monitor.everything is explicitly enabled.

⁠Prometheus metrics

The HTTP API serves Prometheus text at GET /metrics on the configured API_ADDRESS and API_PORT (defaults: 0.0.0.0:8080). The path is outside the versioned /v1 route group. Restrict access to the API listener at the network boundary if metrics should only be available to your monitoring system.

MetricOperator use
adder_pipeline_received_events_totalCount events when the pipeline reads them from input plugin channels; this does not count upstream events that an input has not emitted.
adder_pipeline_processed_events_totalCount events after every configured output channel accepts a copy. A pipeline with no outputs still drains and counts events; this counter does not confirm sink or /events delivery.
adder_pipeline_filtered_events_totalCount drop callbacks from the built-in event and Cardano filters. Custom filters contribute when they implement SetPipelineDropObserver(func()) and call the registered callback once per rejected event.
adder_pipeline_processing_duration_secondsHistogram from when the pipeline reads an input event through filter processing and channel waits until all configured output channels accept it. Only completed events retaining their pipeline ingress time produce samples. It does not measure external sink delivery.
adder_webhook_http_attempts_total{status_class}Logical HTTP delivery attempts when the webhook output is configured. Redirects within one attempt are counted once using the final response class. Classes are 1xx through 5xx, network, or other; no URL or event labels are used.
adder_webhook_delivery_outcomes_total{outcome}Final webhook event outcomes: success, terminal (permanent response or error, or a Retry-After beyond the maximum), and exhausted (retry limit reached). Counted even if the error channel is full.
adder_webhook_http_attempt_duration_secondsTime per HTTP attempt through response-body consumption or request failure.

Counters reset when the process restarts. Metrics have no labels from event types, addresses, hashes, clients, or other user data. A received event can be ahead of processed and filtered totals while it is in flight; events abandoned during shutdown are not counted as processed or filtered. Custom filters can also drop without reporting or emit more than one event per input, so the three counters are not a conservation equation. The counters and histogram are updated separately and a scrape can observe them between updates. Webhook delivery metrics report only in-process attempts and final outcomes, not restart-safe delivery or per-endpoint identity.

⁠Webhook retry policy

Webhook delivery makes one initial attempt and at most three retries by default. Transient transport failures and HTTP 408, 429, and 5xx responses use bounded exponential backoff (1s, 2s, 4s by default). A valid Retry-After can extend a wait up to the configured maximum; a longer requested wait causes a terminal failure. The HTTP client follows up to 10 redirects as before. For 301, 302, and 303 it changes POST to GET and drops the event body; a final 2xx response still counts as success. Use 307 or 308 to redirect a webhook POST with its body intact. A redirect that does not reach a final response fails. Nonretryable non-2xx responses fail immediately. Without state-path, Adder discards the event after the retry limit and reports a terminal error. Retries and queued events remain in memory and can be lost on shutdown. With state-path, a terminally failed entry stays in SQLite and replays after restart. Configure restart-safe webhook delivery⁠ to persist entries that reach the webhook output and replay unacknowledged ones.

With regular scrapes, increase(adder_pipeline_received_events_total[10m]) estimates how much the received counter advanced over ten minutes. If it stays zero when events are expected, investigate the input; a legitimately idle source also produces zero. Check Prometheus up separately for scrape failures.

The built-in filters preserve pipeline ingress time when they forward an event. Custom filters that construct a derived event can carry it with derived.WithPipelineIngressTime(source.PipelineIngressTime()). Events without that time do not enter the processing-duration histogram; compare its _count with adder_pipeline_processed_events_total to detect incomplete coverage.

⁠Configuration

Adder resolves core and plugin settings in this order: explicit command-line arguments, environment variables, YAML, then defaults. Unknown configuration keys are errors. See the configuration migration guide⁠ for the breaking Go API and precedence changes.

You can get a list of all available commandline arguments by using the --help flag.

$ ./adder --help

Usage:
  adder [flags]

Flags:
      --config string                 path to config file to load
      --input string                  input plugin to use, 'list' to show available (default "chainsync")
      --input-chainsync-address string
                                      specifies the TCP address of the node to connect to
...
      --output string                 output plugin to use, 'list' to show available (default "log")
      --output-log-level string       logging threshold: debug/info emit events; warn/error suppress events; also filters diagnostics (default "info")
      --output-log-format string      specifies the output format: text (human-readable, default) or json (machine-parseable) (default "text")
      --output-log-path string        specifies the file path to write logs to (default is stdout)
  -h, --help                          help for adder

Core setting flags and registered plugin options have corresponding environment variables. Command controls such as --config, --version, and --help do not. For example, the --input option has the INPUT environment variable, the --input-chainsync-address option has the INPUT_CHAINSYNC_ADDRESS environment variable, and --output has OUTPUT.

For plugin options, the environment variable and the config file key are always built from the plugin type and name (<TYPE>_<NAME>_<OPTION>), even when the flag itself uses a shortened form. Most plugin flags spell out the plugin name, so the two match — but the filter flags do not. See Filter flags, environment variables, and config keys⁠ for the exact names.

⁠Environment Variables

Application and plugin settings can be set using environment variables:

  • INPUT - Input plugin to use (default: "chainsync")
  • OUTPUT - Output plugin to use (default: "log")
  • KUPO_URL - URL for Kupo service integration
  • OUTPUT_LOG_LEVEL - Log plugin threshold, also used for application diagnostics (default: "info")
  • API_ADDRESS - API server listen address (default: "0.0.0.0")
  • API_PORT - API server port (default: 8080)
  • DEBUG_ADDRESS - Debug server address (default: "localhost")
  • DEBUG_PORT - Debug server port (default: 0)

Genesis configuration can also be controlled via environment variables:

Network Transition:

  • SHELLEY_TRANS_EPOCH - Epoch number when Shelley era begins (default: 208 for mainnet)

Byron Genesis:

  • BYRON_GENESIS_END_SLOT - End slot for Byron era
  • BYRON_GENESIS_EPOCH_LENGTH - Slot length of Byron epochs (default: 21600)
  • BYRON_GENESIS_BYRON_SLOTS_PER_EPOCH - Byron slots per epoch

Shelley Genesis:

  • SHELLEY_GENESIS_EPOCH_LENGTH - Slot length of Shelley epochs (default: 432000)

You can also specify each option in the config file.

input: chainsync

output: log

Plugin arguments can be specified under a special top-level key in the config file.

plugins:
  input:
    chainsync:
      network: preview

  output:
    log:
      level: info
      format: text

⁠Filtering

Adder supports filtering events before they are output using multiple criteria. Each filter supports specifying multiple possible values separated by commas. When specifying multiple values for a filter, only one of the values specified must match an event.

Different filters are combined with AND — an event must match every configured filter to be emitted — with one deliberate exception: --filter-pool and --filter-drep are combined with OR. See Combining filters⁠ below.

Adder Tray applies target-oriented notification semantics rather than the generic pipeline rules described below. See Adder Tray Filtering and Notification Semantics⁠ for target expressions, DRep and pool behavior, and the current ChainSync notification inventory.

You can get a list of all available filter options by using the -h/--help flag.

$ ./adder --help
...
      --filter-address string   specifies address(es) to filter on (comma-separated)
      --filter-asset string     specifies asset fingerprint(s) to filter on (comma-separated)
      --filter-drep string      specifies DRep ID(s) to filter on (comma-separated, hex or bech32)
      --filter-policy string    specifies asset policy ID(s) to filter on (comma-separated)
      --filter-pool string      specifies Pool ID(s) to filter on (comma-separated)
      --filter-type string      specifies event type to filter on
...

The following filters are available:

FlagFilters onApplies to event types
--filter-typeTop-level event typeall
--filter-addressPayment or stake addressinput.transaction, input.governance
--filter-policyAsset policy IDinput.transaction
--filter-assetAsset fingerprint (asset1…)input.transaction
--filter-poolStake pool (SPO) IDinput.block, input.transaction, input.governance
--filter-drepDRep ID (hex or bech32)input.transaction, input.governance

An event type that a given filter does not apply to is passed through unaffected by that filter. For example, an input.block event is never removed by --filter-policy, and an input.governance event is never removed by --filter-asset.

Note: long flags require the double-dash form (--filter-type). The single-dash form (-filter-type) is parsed as a cluster of shorthand flags and is rejected.

⁠Combining filters

Filters of different kinds are combined with AND: an event must satisfy every configured filter to be emitted. For example, --filter-address together with --filter-drep emits only events that match both.

The one exception is --filter-pool combined with --filter-drep. Pool IDs and DRep IDs identify independent actors, so when both are set they are combined with OR — a transaction or governance event matching either the pool or the DRep is emitted. Any other configured filters still apply with AND on top of that pair.

Note that input.block events are matched on the pool alone, because --filter-drep does not apply to block events. Setting both flags therefore does not widen which blocks are emitted.

⁠Filter flags, environment variables, and config keys

Filter flags use a shortened form that omits the plugin name, but the environment variable and the config file key are still built from the plugin type and name. The three spellings therefore differ, and using the flag name as an environment variable (for example FILTER_ADDRESS) has no effect:

FlagEnvironment variableConfig file key
--filter-addressFILTER_CARDANO_ADDRESSplugins.filter.cardano.address
--filter-assetFILTER_CARDANO_ASSETplugins.filter.cardano.asset
--filter-policyFILTER_CARDANO_POLICYplugins.filter.cardano.policy
--filter-poolFILTER_CARDANO_POOLplugins.filter.cardano.pool
--filter-drepFILTER_CARDANO_DREPplugins.filter.cardano.drep
--filter-typeFILTER_EVENT_TYPEplugins.filter.event.type

The --filter-type option belongs to the separate event filter plugin, which is why its environment variable and config key use event rather than cardano.

The same filters expressed in a config file:

plugins:
  filter:
    cardano:
      address: addr1qyht4ja0zcn45qvyx477qlyp6j5ftu5ng0prt9608dxp6l2j2c79gy9l76sdg0xwhd7r0c0kna0tycz4y5s6mlenh8pq4jxtdy
      pool: pool1z5uqdk7dzdxaae5633fqfcu2eqzy3a3rgtuvy087fdld7yws0xt
      drep: drep1p4h4ea7y70ede2wy7x3t83x4umm63wwq68308f94cmt7szexmnr

    event:
      type: input.governance

Each value is a single string; use a comma-separated list for multiple values, exactly as on the command line.

⁠Using Adder as a Library

Adder can be used as a Go library to build custom blockchain indexers and applications. The library examples⁠ provide downloadable Go source and guides for:

⁠Pipeline startup and shutdown

Outputs start first, then filters in reverse chain order, then inputs. This makes downstream consumers ready before sources begin producing events.

Pipeline lifecycle: start outputs, reverse filters, and inputs; stop in reverse startup order

Stop cancels and joins forwarding workers before stopping plugins in reverse startup order. Call it even after canceling the startup context. Shutdown can leave events in upstream buffers; it does not guarantee delivery of every in-flight event. See the plugin authoring guide⁠ for resource ownership and cancellation requirements.

⁠Example usage

⁠Native using remote node
export INPUT_CHAINSYNC_NETWORK=preview
./adder

Alternatively using equivalent commandline options:

./adder \
  --input-chainsync-network preview
⁠In Docker using local node

First, follow the instructions for Running a Cardano Node⁠ in Docker.

docker run --rm -ti \
  -v node-ipc:/node-ipc \
  ghcr.io/blinklabs-io/adder:main
⁠ChainSync connection and events

For the node connections above, ChainSync completes the handshake, starts its protocol clients, selects an intersection, and starts synchronization. Node-to-node headers trigger Bl

Tag summary

Content type

Image

Digest

sha256:ad625003d…

Size

30.9 MB

Last updated

about 16 hours ago

docker pull blinklabs/adder:main