Topic Managers

Setting up a backend with a Topic Manager

Read as Markdown

This deep-dive shows you how to build and wire up a custom Topic Manager for the BSV Overlay Services Engine, using the backend-project-template as your starting point and emulating the behavior of the hello-overlay example.


Why Topic Managers Matter

A Topic Manager listens for broadcasted on-chain events (UTXO creations, spends, etc.) and filters/admits those outputs into named “topics” that your Lookup Service and front-end can query. This separation makes your overlay:

  • Modular and testable
  • Scalable by having processes run independently
  • Easy to extend with new topics

Prerequisites

Before you begin, ensure you have the following:

  • Node.js and Docker
  • LARS & CARS installed and initialized
  • A BRC-100 wallet such as BSV Desktop installed and running
  • Git (optional, but recommended)

Structure your overlay backend

To scaffold your overlay backend, you can choose one of the following options:

Option 1: Quickstart

Follow the Adding a Backend Server to Your LARS and CARS Project. You will have to modify the backend/src directory to only include the sub-folders topic-managers and lookup-services along with any helper files that may be referenced in them.

To set up the topic-managers directory:

1mkdir -p src/topic-managers
2touch src/topic-managers/DemoTopicManager.ts
3touch src/topic-managers/DemoWorldTopicDocs.ts
Copied!

Option 2: Clone the template

Cloning either the template for LARS and CARS projects that only uses a backend or the one that uses both a frontend and backend

1git clone https://github.com/p2ppsr/backend-project-template.git my-project
2cd my-project/backend
3npm install
Copied!

After setting up your project, the general structure should resemble:

1my-project/
2├── backend/
3│   ├── src/
4│   │   ├── lookup-services/
5│   │   ├── topic-managers/
6│   │   │   ├── TemplateTopicDocs.md.ts
7│   │   │   └── TemplateTopicManager.ts
8│   │   └── types.ts
9│   └── package.json
10├── deployment-info.json
11└── package.json
Copied!

Implement the Topic Manager

HelloWorld Tokens in a nutshell

As mentioned before, this guide will cover how to implement an overlay service similar to the hello-overlay project. This topic manager will admit “hello-tokens” which can be created and broadcasted to be received by your topic manager from a React frontend by adding this:

1const wallet = new WalletClient('auto');
2const outputScript = await new PushDrop(wallet).lock(
3  [Utils.toArray(message)],
4  [1, "HelloWorld"],
5  "1",
6  "anyone",
7  true
8);
9
10const { tx } = await wallet.createAction({
11  outputs: [{
12    satoshis: 1,
13    lockingScript: outputScript.toHex(),
14    outputDescription: "New HelloWorld Token"
15  }],
16  options: { acceptDelayedBroadcast: false, randomizeOutputs: false },
17  description: "Create a HelloWorld token"
18});
19
20if (!tx) throw new Error('Failed to create transaction')
21
22const broadcaster = new TopicBroadcaster(['tm_demo'], { // change to use the name of your topic manager
23  networkPreset: (await wallet.getNetwork()).network
24});
25
26await broadcaster.broadcast(Transaction.fromAtomicBEEF(tx));
Copied!

Note that these guides work primarily with these hello-tokens but in the future you will likely want to create your own unique tokens with fields that are specific to your application.

DemoTopicManager.ts

Here's a working implementation of a topic manager that would admit a hello-token:

1import { AdmittanceInstructions, TopicManager } from '@bsv/overlay'
2import docs from './DemoTopicDocs.md.js'
3import { PushDrop, Signature, Transaction, Utils } from '@bsv/sdk'
4
5export default class DemoTopicManager implements TopicManager {
6  /** Identify which outputs to admit into the topic */
7  async identifyAdmissibleOutputs(
8    beef: number[],
9    previousCoins: number[]
10  ): Promise<AdmittanceInstructions> {
11    const outputsToAdmit: number[] = [];
12
13    try {
14      console.log("DemoTopicManager invoked");
15      const parsedTx = Transaction.fromBEEF(beef);
16
17      if (!Array.isArray(parsedTx.outputs) || parsedTx.outputs.length === 0) {
18        throw new Error("Missing parameter: outputs");
19      }
20
21      for (const [index, output] of parsedTx.outputs.entries()) {
22        try {
23          // decode a PushDrop “hello token” payload
24          const result = PushDrop.decode(output.lockingScript);
25          const signature = result.fields.pop();
26
27          // 1) Exactly one field before signature
28          if (!result.fields || result.fields.length !== 1) continue;
29
30          const message = Utils.toUTF8(result.fields[0]);
31          // 2) Message must be ≥2 chars
32          if (message.length < 2) continue;
33
34          // 3) Verify signature
35          if (!result.lockingPublicKey || !signature) continue;
36
37          const data = result.fields.flat();
38          const valid = result.lockingPublicKey.verify(
39            data,
40            Signature.fromDER(signature)
41          );
42          if (!valid) throw new Error("Invalid signature");
43
44          outputsToAdmit.push(index);
45        } catch (err) {
46          console.error(`Output ${index} skipped:`, err);
47        }
48      }
49
50      if (outputsToAdmit.length === 0) {
51        throw new Error("No outputs admitted");
52      }
53
54      console.log(
55        `Admitted ${outputsToAdmit.length} ` +
56        `${outputsToAdmit.length === 1 ? "output" : "outputs"}!`
57      );
58    } catch (err) {
59      if (outputsToAdmit.length === 0 && !previousCoins.length) {
60        console.error("Error identifying admissible outputs:", err);
61      }
62    }
63
64    return { outputsToAdmit, coinsToRetain: [] };
65  }
66
67  /** Provide topic-specific documentation */
68  async getDocumentation(): Promise<string> {
69    return docs;
70  }
71
72  /** Provide metadata for the topic (e.g. schema version) */
73  async getMetaData(): Promise<{
74    name: string
75    shortDescription: string
76    iconURL?: string
77    version?: string
78    informationURL?: string
79  }> {
80    return {
81      name: 'Demo Topic Manager',
82      shortDescription: 'Admit outputs into a topic'
83    }
84  }
85}
Copied!

Configure LARS/CARS

Before you deploy using either of the deployment tools, add your Topic Manager to deployment-info.json so LARS and CARS know what to load. Replace "tm_demo" with a unique name for your own topic manager:

1{
2  "schema": "bsv-app",
3  "schemaVersion": "1.0",
4  "topicManagers": {
5    "tm_demo": "./backend/src/topic-managers/DemoTopicManager.ts"
6  },
7  …,  
8}
Copied!

What's Next

Now that we have a functional Topic Manager, you can now move onto:

  • Implement a Lookup Service to store and query the admitted outputs.
  • Add proper documentation and rename the Topic Manager to better suit your project.
  • Integrate with your React front-end.
Was this page helpful?