Skip to content

Node specification

Bellchain has exactly one specification for nodes. The 25 official nodes are built exactly the way community nodes will be built later. Once you understand how the RSI works, you can write your own node. The packages are @bellchain-app/node-spec (types, defineNode(), time series, chain format) and @bellchain-app/nodes (the official nodes), both MIT licensed.

hull-ma.ts
import { defineNode, l, series } from '@bellchain-app/node-spec';
export default defineNode({
id: 'community.hull-ma',
version: '1.0.0',
category: 'ind',
label: l('Hull Moving Average', 'Hull Moving Average'),
description: l('Schneller Durchschnitt mit wenig Verzögerung', 'Fast average with little lag'),
inputs: [{ id: 'in', type: 'series', label: l('Wert', 'Value') }],
outputs: [{ id: 'out', type: 'series', label: l('HMA', 'HMA'), unit: 'input' }],
settings: {
length: { type: 'number', label: l('Perioden', 'Periods'), default: 21, min: 2, max: 500, integer: true },
},
// How the node reads on the board
sentence: ({ settings }) => `HMA ${settings.length}`,
// Candles needed before the first reliable value
warmup: ({ settings }) => settings.length + Math.floor(Math.sqrt(settings.length)),
// Pure: no network, no storage, no clock
compute({ inputs, settings }) {
return { out: series.hma(inputs.in, settings.length) };
},
// Required: known values every version is tested against
tests: [{ fixture: 'binance-btcusdt-1h-500', settings: { length: 21 }, expectLast: { out: 81372.42333679654 } }],
});

defineNode() checks the definition right away and reports all problems at once. TypeScript derives the types of compute() from pins and settings: settings.length is a number, inputs.in a number series.

  • id with a namespace: core.rsi, community.hull-ma. core belongs to the official nodes.
  • version as a semantic version. If the result changes, the major version goes up; chains can pin it with core.rsi@1.
  • category: src sources, ind indicators, cond conditions, logic logic & timing, data data & text, act actions, tool tools.

Every visible text exists in German and English, built with l('Kurs', 'Price'): name, description, help, every pin, every setting, every option. If a language is missing, defineNode() rejects the definition.

Type Content In the editor
series number series, one value per candle; null means “no value yet” like na in Pine round
signal true or false per candle square
text message with placeholders, only from the message template to an action diamond

Inputs can be optional: true or grow: variadic: { min, max } creates the pins in0, in1 … Outputs can carry a unit: price appears as a price, percent with a percent sign, input like the input.

Kind Fields
number default, min, max, step, integer, unit
select default, options: [{ value, label }]
text default, minLength, maxLength, pattern, format (timezone or url), required, multiline, placeholder

All of them have label, optionally help and advanced: true (shown under “More”). The default must respect its own limits.

compute() receives whole series and returns whole series, one per output. For every computing node:

  1. Pure and deterministic. No network, no storage, no clock, no randomness. That’s what lets Bellchain share results across chains; the engine computes RSI 14 on BTC/USDT 1h once for everyone.
  2. No look-ahead. The value at candle i depends only on candles up to i. What a candle shows never changes again (no repainting).
  3. Don’t modify inputs.
  4. Every output series has exactly as many values as there are candles.

The time-series functions in series follow Pine: sma, ema, rma, wma, hma, rsi, macd, stdev, bb, highest, lowest, roc, change, crossover, crossunder, cross.

  • state instead of compute() for nodes with memory, such as the cooldown: init() and step() compute candle by candle, the state is JSON. That’s why replay and live operation match.
  • source for sources: { kind: 'candles', market(settings) } declares exchange, pair and timeframe; the engine loads the data, never the node.
  • effect for actions: { kind: 'notify', channel: 'telegram' } or { kind: 'log' }. Actions don’t compute; the engine fires on a rising edge and respects the cooldown.

Messages are filled in only when they fire, in the recipient’s language and time zone:

Placeholder German Content
{{chain}} {{kette}} name of the chain
{{values}} {{werte}} values of the nodes involved, such as “RSI 14: 27.69 · Price: $77,788”
{{price}} {{kurs}} closing price of the candle
{{pair}} {{paar}} trading pair such as BTC/USDT
{{coin}} base currency such as BTC
{{time}} {{zeit}} candle close
{{a}} to {{d}} values at inputs A to D of the message template

Unknown placeholders stay as they are so that typos stand out.

Every node that works with prices ships tests in its manifest: fixed price data in, known values out. The reference values of the official nodes come from an independent recalculation, not from the code they test. The test kit also checks determinism and causality.