Reference integration between Buzz and OriginTrail DKG v10: a standalone daemon that joins a Buzz channel as an external member, turns explicitly signalled conversations into layered DKG memory (WM → SWM → VM), and answers in-room questions using evidence exclusively from the room's designated Context Graph.
Canonical specification: SPEC.md · Design: docs/DESIGN.md ·
Verified interfaces: INTERFACES.md · Gate reports: docs/gates/.
Status: all stages executed (A–E), 2026-07-26. Source audit → isolated spike → daemon (74 tests, zero-mock acceptance demo) → production validation → one operator-approved SWM share → one operator-approved on-chain publication.
- UAL:
did:dkg:base:8453/0x633e5a7c5e612d9981538f60d824cc03be97e2ab/2201— tx0x6daf3e0bad8cba13f7508f69c5550750a30294e997bf5e7fdffe9a24170dbb38(block 49145748), published from a signed Buzz thread through the full WM → finalize → SWM → ✅-approval → VM lifecycle into the "FIFA World Cup 2026" Context Graph (on-chain id 7). - Screen recordings:
docs/media/buzz-dkg-live-loop.gif(the live channel: thread → pin → SWM receipt → ✅ → VM receipt → grounded Q&A + refusal) anddocs/media/buzz-dkg-node-ui.gif(the node UI: knowledge pipeline, DecisionCluster KA, provenance trail). - Every stage gate has a report + transcript under
docs/gates/(A through E), including the §6 approval-invariant preflights and spend accounting.
-
Capture: a pin (kind 40004) or bot mention followed by
distill(@dkg distill, or the configured Buzz display name) snapshots the thread as of the trigger (full signed events), distills it deterministically into one PROV-O decision cluster, walks it throughcreate → write → finalize (seal) → full SWM share, verifies with a scoped read-back, and replies in-thread with a receipt (assertion coordinate, KA name, source digest). Mention authorization is identity-bound, not label-bound: the signed event must carry the integration service pubkey in aptag. Any channel member who can mention that service may requestdistill; the display-name alias makes the existing capability easier to discover but does not widen who may invoke it. -
Approval: an authorized promoter's ✅ on that receipt is validated against the nine SPEC §6 invariants in code (including a re-read of the shared graph's source-set digest at approval time). Publication is mode-gated by
BDI_PUBLISH_MODE:disabled(default) — approvals are recognised and the §6 invariants are evaluated, but publication is refused;devnet— publishes only when the node reports chainevm:31337;mainnet— publishes only when the node reportsbase:8453, additionally bounded byBDI_MAX_PUBLISHES_PER_DAY(a rolling-24h ceiling; a non-numeric value fails startup).mainnetspends real ETH and writes irreversibly to Base — measured cost per publish is recorded indocs/gates/GATE_D3_REPORT.md. It is the only mode with node-side prerequisites beyond "the node is running": the node must have been set up on Base (dkg init --network mainnet-base— the DKG default ismainnet-gnosis, which reportsgnosis:100and makes this daemon refuse to start), and its operational wallet needs funding for gas.
On success the UAL is posted as a second in-thread receipt. Every rejection posts its reason back into the room so a promoter can tell "accepted" from "not authorised" / "budget exhausted" / "relay blip". A publish whose outcome the node cannot confirm is never announced as anchored: it lands in a terminal
publish_unconfirmedstate with an honest in-room note, counts against the 24h ceiling (gas may already have been spent), and is never auto-retried. -
Ask:
@dkg ask <question>retrieves only from the channel's bound Context Graph (server-enforced scoped SPARQL), answers extractively with validated citations, and refuses explicitly when evidence is insufficient — no model required, no fallback to any other graph. Note the scoping guarantees no other graph is read, not that only channel-authored content is returned: every member of a bound channel is effectively a reader of that Context Graph, so bind only graphs whose contents the whole channel may see.
- Node.js ≥ 22.13 for this daemon — it relies on
node:sqlite(unflagged in 22.13),--experimental-strip-types(22.6) and--env-file-if-exists(22.9). - A DKG v10 node, ≥ 10.0.8, running and reachable over HTTP — the version
this integration is validated against (
docs/gates/GATE_D1_REPORT.md). - A Buzz NIP-29 relay you can reach, or one built from the pinned checkout (see Run (isolated stack) below).
An edge node — the default role — is enough: no on-chain node profile, no staking. The live publication above was made from one.
npm install -g @origintrail-official/dkg
dkg init # interactive: network, node name, role, triple store, API port
dkg start # daemon on http://127.0.0.1:9200dkg init defaults to API port 9200, which is also what BDI_DKG_API
defaults to. It also defaults to the mainnet-gnosis network — fine for
everything this daemon does by default, but see the mainnet note under
What it does → Approval before enabling on-chain publishing. To check what
you actually have (public route, no token needed):
curl -s http://127.0.0.1:9200/api/status # → version, chain.chainId, nodeRoleWallets are generated during dkg init and only need funding for the Verifiable
Memory publish path. Everything the daemon does by default — Working Memory
writes, sealing, Shared Working Memory sharing, and the whole grounded-answering
path — is off-chain and free.
Node docs: OriginTrail/dkg.
Operator quickstart and beta acceptance checklist: docs/BETA_ONBOARDING.md.
The Beta V1a installer starts from a Buzz Relay that is already serving a community. On a supported Linux relay host, a tagged release is installed and started with one command:
curl -fsSL https://github.com/OriginTrail/buzz-dkg-integration/releases/latest/download/install.sh | sudo shPrerequisites are Docker with the Compose plugin and the GitHub CLI (gh); no
GitHub account, login, or token is required. The bootstrap detects linux/x64
or linux/arm64, downloads the corresponding versioned bundle, checksum, and
signed attestation bundle, then uses gh attestation verify to authenticate its
GitHub Actions build provenance before any root extraction. It atomically activates the CLI under
/usr/local/bin/buzz-dkg and reconnects the guided installer to /dev/tty.
The CLI remains available after setup:
sudo buzz-dkg plan
sudo buzz-dkg status
sudo buzz-dkg logs
sudo buzz-dkg smoke
sudo buzz-dkg removeThe guided installer discovers common containerized Buzz Relay deployments. It
preserves the relay's advertised community URL for tenant routing and NIP-98
authentication, while using a loopback host mapping only for readiness and
NIP-11 discovery probes when the public endpoint is private-network gated. It
also accepts an explicit
--relay wss://community.example.com. It validates and adopts that
endpoint without replacing the relay process, database, identity, domain, or
TLS configuration. It then reuses a compatible DKG node on 127.0.0.1:9200,
currently v10.0.11 or v10.0.12, or invokes the supported DKG npm installer and
setup wizard for a managed v10.0.12 Edge
(default) or Core node. A fresh guided install defaults to DKG testnet; a fresh
unattended install must explicitly pass --dkg-network testnet,
mainnet-gnosis, or mainnet-base. Finally it creates the managed Web of Trust
channel and Context Graph, starts the integration sidecar with Verifiable Memory
disabled, and runs an end-to-end smoke check.
For automation or a relay that cannot be inferred from its container metadata:
sudo buzz-dkg install \
--relay wss://community.example.com \
--dkg-role edge \
--dkg-network testnetremove stops only the integration sidecar. It does not delete Buzz history,
integration state, DKG state, or an operator-managed node. Release bundles pin
their own Node runtime; the host does not need Node.js preinstalled. The
sidecar adds no public port, but it deliberately joins the Linux host network
to reach loopback-only Buzz and DKG APIs. It runs with the uid/gid that owns the
mounted DKG token rather than defaulting to container root. Until the
first installer tag is published, the releases/latest/download URL above is
expected to return 404.
Before the first installer release, repository maintainers must configure the
installer-release GitHub environment with required reviewers and protect the
v* tag pattern. The workflow additionally scopes write permission to the
gated release job and refuses tags whose commit is not already on main.
The M0 launcher packages a pinned Buzz relay, an isolated one-node DKG 10.0.11
devnet, and this integration into one local lifecycle. It generates stable
identities, creates one buzz-dkg-canary channel, creates exactly one
deterministically named Context Graph for that channel, adds the integration as
a bot member, writes the binding, and starts the daemon with VM publication
disabled.
Prerequisites are Docker, pnpm, the Buzz CLI, a compatible Node.js runtime
(>=22.13 <23 or >=23.4), and a sibling DKG checkout named
dkg-v10.0.11. Override the DKG checkout, Node binary, or Buzz CLI with
BDI_MVP_DKG_REPO, BDI_MVP_NODE, or BDI_BUZZ_CLI when needed. The launcher
checks the Buzz CLI before it creates local state or starts any service.
Buzz itself runs from an immutable GHCR image pinned in
deploy/mvp/compose.yml.
./buzz-dkg up
./buzz-dkg status
./buzz-dkg smokesmoke drives the real relay and node: it posts and pins a synthetic decision,
waits for one SWM receipt, checks that no VM receipt was produced, asks a
supported question and verifies its scoped citation, then checks an unsupported
question is refused. No approval or on-chain publication is attempted.
./buzz-dkg logs
./buzz-dkg downSecrets and retained runtime state live under .mvp/ with private permissions
and are ignored by Git. A checkout-wide ignored .buzz-dkg-m0-control/
lock/journal safely serializes lifecycle commands even when the state-directory
override is used. up is convergent: rerunning it reuses the healthy relay, DKG
node, channel, graph, membership, binding, and daemon. down stops only
M0-owned processes and containers and does not delete secrets or Docker
volumes. If a process is killed while acquiring the lock, ./buzz-dkg unlock
provides a supported recovery path; it refuses while either the recorded
launcher or DKG child is still alive. This is a local development devnet,
however, so its Hardhat chain and
graph contents must not be treated as durable production data.
This is the deployment the integration advertises: a Buzz NIP-29 relay and a DKG v10 node you can already reach (see Prerequisites), with this daemon running alongside them.
-
Install and configure
npm install cp .env.example .env # then edit — every var is documented inline -
Generate the bot's member identity (
BDI_SERVICE_KEY) — a fresh Nostr secret key, never reused from the spike:node -e "import('nostr-tools/pure').then(n=>{const sk=n.generateSecretKey();\ console.log('BDI_SERVICE_KEY=', Buffer.from(sk).toString('hex'));\ console.log('service pubkey =', n.getPublicKey(sk))})"
Put the hex secret in
.envasBDI_SERVICE_KEY. The printed pubkey is the identity a channel admin must add (next step); it is also logged asservicePubkeyin thedaemon startedline. -
Get the bot into the channel. The daemon only subscribes to bound channels — it never self-adds. A NIP-29 channel admin must add the bot's service pubkey as a member of the target channel. The channelId is the channel's NIP-29 group id (the
htag on its messages / the id in the Buzz channel URL). -
Point at a Context Graph. Each channel binds to exactly one Context Graph, which must already exist on the node — the daemon probes every bound graph at startup and refuses to start if one is missing. Creating one is free and touches no chain:
dkg context-graph create my-room # any name you likeThe graph is created under your own node's agent address: a bare slug is auto-prefixed, so the printed
ID:line comes back as0x<YourAgentAddress>/my-room. Copy that line intobindings.json— the id is matched as an exact IRI, so the checksummed casing and your full address both matter, and an address copied from anywhere else will fail the startup probe.dkg context-graph listreprints it. (Thedevnet-testexample below is only valid on the isolated devnet.) -
Provide the DKG token. The node generates a bearer token on first start and writes it to
~/.dkg/auth.token— a comment header plus the token on the last line, which is whatBDI_DKG_TOKEN_PATHexpects (the daemon reads the last non-comment line).dkg auth statusprints the path the node actually resolved, which differs if you setDKG_HOMEor run from a monorepo checkout:dkg auth status # → Token file: /home/you/.dkg/auth.tokenBDI_DKG_TOKENtakes a pasted token instead; prefer the path form so a token rotation needs only a daemon restart, not a.envedit. -
Bind and run. Write
bindings.json(see below) and start:npm start # loads .env via --env-file-if-existsPublication stays
disableduntil you deliberately setBDI_PUBLISH_MODE(see What it does → Approval). Common first-run silent failures: the bot isn't a channel member (starts clean, sees no events);BDI_DKG_APIpoints at the wrong port (ECONNREFUSED); a promoter pubkey in the wrong format (npub is accepted and decoded; anything else fails fast at startup).
Reproduces the full demo end-to-end with no external dependencies. Prereqs: Node ≥ 22.13, Docker, pnpm, Rust (for the relay binary).
# 1. Isolated stacks (full details + port map: phase0/ISOLATION.md, phase0/README.md)
# Buzz: postgres/redis/minio containers + relay built from the pinned checkout
cd phase0 && docker compose -f docker-compose.spike.yml up -d postgres redis minio minio-init
./run-relay-host.sh & # needs phase0/.env.spike (see .env.spike.example)
# DKG devnet (from the pinned OriginTrail/dkg clone):
API_PORT_BASE=9420 LIBP2P_PORT_BASE=10401 HARDHAT_PORT=8655 \
DEVNET_OXIGRAPH_BASE=7920 DEVNET_BLAZEGRAPH_PORT=19999 \
DEVNET_OXIGRAPH_SERVER_PORT_5=7931 DEVNET_OXIGRAPH_SERVER_PORT_6=7932 \
UI_PORT=5573 DEVNET_ENABLE_PUBLISHER=1 ./scripts/devnet.sh start 6
# 2. Daemon
npm install
cp .env.example .env # fill BDI_SERVICE_KEY, BDI_DKG_TOKEN_PATH, bindings
npm start
# 3. Everything at once, self-checking (the Gate C acceptance demo):
node scripts/acceptance.mjs # writes docs/acceptance-transcript.mdbindings.json (one channel ↔ one Context Graph, per SPEC §4.3):
[{ "channelId": "<uuid>", "contextGraphId": "devnet-test", "promoters": ["<hex-pubkey-or-npub>"] }]contextGraphIdisdevnet-testonly on the isolated devnet; against a real node use the production form0x<CuratorAddress>/<name>(see Deploy §4).promotersaccept a 64-char hex pubkey or annpub1…(decoded at load); any other format fails fast at startup rather than silently ignoring approvals.
npm run typecheck && npm run lint && npm test # 74 tests, no network
npm run formatTests use in-memory doubles for the relay and node; the acceptance demo uses
no mocks at all. phase0/ contains the earlier spike (bridge scripts + real
transcript) that de-risked every interface the daemon relies on.