DEV Community

jackymenCZ (jackymenCZ)
jackymenCZ (jackymenCZ)

Posted on Edited on

Discuss+ What If AI Agents Had a Shared Market Feed? Sentinel Market Relay

Building Sentinel Market Relay: An Information Layer Between Exchanges and AI Agents

Thanks for support..
Release: "Sentinel-IR-MCP v0.2.2 on GitLab" (https://gitlab.com/jakubrojicek11/sentinel-ir-mcp/-/tree/v0.2.2?ref_type=tags)

Package: "sentinel-ir-mcp@0.2.2 on npm" (https://www.npmjs.com/package/sentinel-ir-mcp/v/0.2.2)

One exchange connection. Multiple independent agents. One shared view of what is happening.

This is not another trading bot. It is a proposal for something different: a read-only information relay that helps independent AI agents work with the same market evidence.

I had an idea today while thinking about how trading bots work.

What if we stopped trying to build another bot that predicts prices or executes trades?

What if we built something that helps all those bots and agents understand the same information instead?

I started exploring this idea as a possible new direction inspired by my existing Sentinel-IR project.

Everything below is an architectural proposal. The prototype code has been tested offline, but Sentinel Market Relay does not yet exist as a released product.

There are three reading levels:

  • 30 seconds: The idea.
  • 10 minutes: An explanation for people who don't work in software.
  • 20 minutes: Architecture, Node.js, tests, MCP, NATS and the actual engineering challenges.

Let's start with the problem.


Part 1: The 30-second version

Imagine having four AI agents.

One watches Bitcoin prices.

Another monitors trading volume.

A third looks at liquidity.

A fourth reads market news.

They all need information about the same market.

But what happens when every agent builds its own connection to an exchange, processes the same data independently and creates its own interpretation of what is happening?

We can end up with duplicated work, inconsistent data formats and agents working with observations from different moments.

There are other ways to share a connection, of course. The problem is not unavoidable.

But what if we created a dedicated information layer?

Something that:

  1. Connects to public market data sources.
  2. Receives and validates incoming events.
  3. Converts them into a shared format.
  4. Records where the information came from and when it arrived.
  5. Makes the information available to multiple agents.

The agents remain independent.

They can analyze the data differently.

They can disagree.

They can have completely different objectives.

The relay does not buy, sell or make investment decisions.

It simply makes information available.

Working name:

Sentinel Market Relay

"Exchange → Market Relay → Multiple AI Agents"

That's the idea.

Now let's understand why it might be useful.


Part 2: The 10-minute explanation

  1. First, what is the difference between a trading bot and an AI agent?

This distinction matters.

People sometimes use the words bot, agent and AI interchangeably.

They are not necessarily the same thing.

A traditional trading bot

Imagine a small automated employee sitting in front of a trading terminal.

You give it instructions:

  • When the price crosses a certain threshold, check the conditions.
  • When a volume indicator reaches a specified value, generate a signal.
  • When a configured risk limit is reached, stop trading.

The bot follows its programming.

It doesn't necessarily need an AI model.

A traditional trading bot can be a fairly ordinary program executing mathematical calculations and predefined rules.

An AI agent

Now imagine a different employee.

Instead of receiving only fixed rules, it has access to tools, information sources and perhaps a language model.

You ask it:

"Investigate what is happening with Bitcoin today."

It might:

  • Retrieve market data.
  • Compare recent observations.
  • Read relevant announcements.
  • Ask another agent to investigate something.
  • Prepare a summary with supporting evidence.

An agent can combine several capabilities rather than simply executing a predefined trading rule.

It might also be autonomous, but autonomy is not a requirement for every agent.

And here is an important point:

Not every AI agent should be responsible for trading.

Sometimes its only responsibility should be collecting and explaining information.

  1. Imagine a newsroom instead of a trading desk

Let's use an example.

Imagine a small newsroom with four journalists.


Newsroom with journalists working at desks
Journalist A watches economic reports.

Journalist B monitors financial markets.

Journalist C reads company announcements.

Journalist D prepares the final article.

Would you ask all four journalists to independently download every report, contact every source and reconstruct the same information?

Probably not.

You would create an information desk.

It receives information, records its origin, organizes it and makes it available to the people who need it.

The journalists still perform their own work.

The information desk does not decide what the article should say.

That is how I imagine Sentinel Market Relay.

The agents are the journalists.

The exchange is one of their sources.

Sentinel is the information desk.

And no, the information desk doesn't get to shout at the journalists because they disagree. :)

  1. What would the agents actually receive?

Let's imagine the following situation.

A cryptocurrency exchange publishes a new BTCUSDT trade.

Sentinel receives the event.

It creates a structured record:

{
"source": "Binance",
"market": "spot",
"symbol": "BTCUSDT",
"event": "trade",
"price": "65000.10",
"quantity": "0.002",
"status": "observed"
}

This is an illustrative example, not live market data.

The information is then made available to three independent agents.

Agent A: Observer

It watches changes in trading activity.

Agent B: Analyst

It compares the incoming observation with earlier data.

Agent C: Reporter

It turns interesting developments into a readable summary.

All three receive the same original observation.

But their conclusions may differ.

That is perfectly fine.

We don't want Sentinel to manufacture agreement between independent agents.

We want it to preserve a common factual starting point.

  1. Why not let every agent connect directly to the exchange?

Good question.

And the honest answer is that sometimes they absolutely should.

A direct connection can be perfectly reasonable when an application has only one consumer or needs highly specialized access.

A shared relay becomes more interesting when:

  • Many agents need the same information.
  • Different agents use different frameworks.
  • Each agent currently has its own exchange adapter.
  • Data formats are inconsistent.
  • Source health and event history need to be shared.
  • We want one place to inspect what was actually received.

Think of it this way.

Five people can all subscribe to five separate newspapers.

Or one office can receive the newspapers and distribute the relevant pages.

Neither arrangement is automatically better.

But when those five people constantly need the same information, the second arrangement becomes worth investigating.

There is also a possible disadvantage: the relay introduces another processing step.

It might make delivery slower.

We should measure that instead of promising that it will be faster.

  1. What makes this different from an ordinary market data API?

An ordinary market data API gives you access to information.

Sentinel Market Relay would try to solve another problem.

It would provide a common information format and a consistent way to make observations available to multiple AI consumers.

For example, an event would carry:

  • Which exchange supplied it.
  • Which market it belongs to.
  • When the exchange generated it.
  • When Sentinel received it.
  • What type of event it represents.
  • A stable event identifier.

Imagine two agents receiving what appears to be the same price update.

One receives it immediately.

The other receives it several seconds later.

Without timestamps and source information, both might assume they are looking at the same current state.

With explicit metadata, they can recognize the difference.

This is important because a plausible answer based on old information is still an answer based on old information.

  1. What about security?

This is one reason I would start with a read-only system.

The first version would connect only to public market data.

No trading account.

No wallet.

No private API keys.

No withdrawal permissions.

No order execution.

The relay would observe information that the exchange makes publicly available.

For example, Binance provides a market-data-only WebSocket endpoint:

"wss://data-stream.binance.vision"

According to Binance's documentation, this endpoint supports market data only, not user data streams.

Source: "Binance WebSocket Streams" (https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md).

This gives us a clean starting point.

We don't need to build a system that can touch anybody's money just to demonstrate that several agents can share information.


Part 3: The 20-minute developer study

Now let's get into the engineering.

There are several technologies that might initially look interchangeable.

They aren't.

Before writing code, we need to establish the boundaries.

  1. The proposed architecture

I would separate the system into five main responsibilities.

           MARKET DATA SOURCES
             /           \
            /             \
       BINANCE           COINBASE
          |                 |
          v                 v
    +-----------------------------+
    |       EXCHANGE ADAPTERS     |
    |                             |
    | Parse source-specific data  |
    +--------------+--------------+
                   |
                   v
    +-----------------------------+
    |      NORMALIZATION          |
    |                             |
    | Validate event structure    |
    | Standardize field names     |
    | Preserve source metadata    |
    | Assign stable event IDs     |
    +--------------+--------------+
                   |
                   v
    +-----------------------------+
    |       MARKET RELAY          |
    |                             |
    | Current market snapshots    |
    | Recent event history        |
    | Source health                |
    | Event routing                |
    +--------------+--------------+
                   |
         +---------+---------+
         |                   |
         v                   v
   +-----------+       +------------+
   | MCP API   |       | EVENT BUS  |
   |           |       |            |
   | Queries   |       | Live feed  |
   +-----+-----+       +------+-----+
         |                    |
         +---------+----------+
                   |
         +---------+---------+
         |         |         |
         v         v         v
      Agent A   Agent B   Agent C
Enter fullscreen mode Exit fullscreen mode

This is a proposed architecture.

It is not a description of existing Sentinel functionality.

Let's examine each component.

Exchange adapters

Every exchange has its own message format.

The Binance adapter understands Binance.

The Coinbase adapter understands Coinbase.

We should not put exchange-specific parsing rules into the rest of the system.

That would be like making every employee speak a different language depending on which newspaper delivered the information.

The adapter translates the source message into our internal format.

Normalization

This layer is responsible for making events consistent.

For example:

Binance might identify a market as "BTCUSDT".

Coinbase might identify a product as "BTC-USD".

These are not the same market.

One uses USDT as the quote asset, the other USD.

We must not blindly convert both into a generic "BTC" record and lose the distinction.

The internal model needs to preserve the original exchange and instrument identifiers.

Cross-exchange mapping should use explicit exchange metadata and a maintained instrument mapping table.

Market Relay

This is the central component.

It maintains the latest available state, stores a bounded history if configured and routes events to the appropriate consumers.

It should also know whether the source is currently healthy.

The relay is responsible for the information pipeline, not the trading strategy.

  1. What data can we actually obtain?

Let's use Binance Spot as the first source.

Binance documents a trade WebSocket stream using this format:

"@trade"

For BTCUSDT:

"btcusdt@trade"

Example endpoint:

wss://data-stream.binance.vision/ws/btcusdt@trade

The documented trade event contains:

Field| Meaning
"e"| Event type
"E"| Event timestamp
"s"| Trading symbol
"t"| Trade identifier
"p"| Price
"q"| Quantity
"T"| Trade timestamp
"m"| Whether the buyer is the market maker
"M"| Ignore

The exchange uses milliseconds for timestamps by default.

The source documentation also describes connection limits and connection lifecycle.

For example, a Spot WebSocket connection is limited to 24 hours, so our production implementation must expect disconnections and reconnect safely.

A connection can subscribe to up to 1,024 streams, and Binance documents a limit of 300 connection attempts per five minutes per IP.

These are not details to ignore when designing a long-running service.

Official source:

"Binance Spot WebSocket Streams" (https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md).

Coinbase offers another example.

Its Advanced Trade WebSocket feeds include ticker, ticker batch and level2 channels.

The level2 channel is particularly useful for obtaining updates to an order book.

Source:

"Coinbase Advanced Trade WebSocket Feeds" (https://help.coinbase.com/en/developer-platform/websocket-feeds/advanced-trade).

For the first prototype, however, I would use only one Binance trade stream.

One source is enough to test the basic architecture.

  1. Designing a common event format

This is one of the most important design decisions.

Suppose every agent receives a slightly different representation of the same observation.

Agent A calls the field "timestamp".

Agent B calls it "eventTime".

Agent C uses "receivedAt".

We have created a translation problem before the actual analysis even begins.

I would start with a canonical structure:

{
"schemaVersion": 1,
"eventId": "binance:spot:trade:BTCUSDT:12345",

"source": {
"venue": "binance",
"market": "spot"
},

"type": "trade",
"instrument": "BTCUSDT",

"sourceEventTimeMs": 1700000000001,
"occurredAtMs": 1700000000000,
"receivedAtMs": 1700000000010,

"payload": {
"price": "65000.10",
"quantity": "0.002",
"buyerIsMaker": false
}
}

This example uses illustrative values.

Notice three different timestamps.

sourceEventTimeMs

When the exchange generated the event.

occurredAtMs

When the trade occurred according to the exchange.

receivedAtMs

When our application received it.

Those values should not be confused.

An event received later is not necessarily a new event.

The event identifier is also important.

If the same exchange event arrives twice, downstream consumers need a way to recognize that it is a duplicate.

The relay proposes a deterministic identifier based on:

  • Exchange.
  • Market.
  • Event type.
  • Instrument.
  • Exchange trade ID.

This gives us a stable identity for the event.

It does not automatically implement deduplication. Consumers or persistent storage still need to maintain their own deduplication state.

Why keep prices as strings?

This is another small but important detail.

JavaScript's regular "Number" type uses binary floating-point arithmetic.

Many decimal fractions cannot be represented exactly in binary floating point.

Financial values should not be casually converted into ordinary JavaScript numbers.

For the initial adapter, I would preserve the original decimal strings.

Actual financial calculations would need an explicit decimal arithmetic strategy.

The relay's job is to preserve the information accurately, not introduce rounding mistakes during ingestion.


  1. A minimal working normalization prototype

Before creating the entire platform, let's test the smallest useful part.

We will:

  1. Accept a Binance trade event.
  2. Validate its structure.
  3. Convert it into our proposed event format.
  4. Reject malformed data.
  5. Make the normalized event available to multiple local listeners.

We will use Node.js 22.4.0 or newer.

Why?

Node.js includes a stable built-in WebSocket client from version 22.4.0.

That means we don't need an external WebSocket library for this example.

Documentation:

"Node.js WebSocket API" (https://nodejs.org/download/release/v22.16.0/docs/api/globals.html#websocket).

Project structure

sentinel-market-relay/
│
├── package.json
├── market-event.js
├── market-event.test.js
└── relay.mjs

Step 1: package.json

{
"name": "sentinel-market-relay-prototype",
"version": "0.1.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22.4.0"
},
"scripts": {
"test": "node --test",
"start": "node relay.mjs"
}
}

Notice something.

There are no external dependencies.

We don't need "npm install" to fetch a WebSocket package.

Step 2: market-event.js

This file contains the normalization logic.

const DECIMAL_PATTERN =
/^(?:0|[1-9][0-9]*)(?:.[0-9]+)?$/;

function isPositiveDecimal(value) {
return typeof value === "string" &&
DECIMAL_PATTERN.test(value) &&
/[1-9]/.test(value);
}

export function normalizeBinanceTrade(
raw,
receivedAtMs = Date.now()
) {
if (
!raw ||
typeof raw !== "object" ||
Array.isArray(raw)
) {
throw new TypeError("Invalid event");
}

if (raw.e !== "trade") {
throw new TypeError("Unsupported event type");
}

if (
typeof raw.s !== "string" ||
!/^[A-Z0-9]{1,30}$/.test(raw.s)
) {
throw new TypeError("Invalid symbol");
}

if (
!Number.isSafeInteger(raw.t) ||
raw.t < 0
) {
throw new TypeError("Invalid trade ID");
}

if (
!Number.isSafeInteger(raw.E) ||
raw.E < 0 ||
!Number.isSafeInteger(raw.T) ||
raw.T < 0
) {
throw new TypeError("Invalid timestamp");
}

if (!isPositiveDecimal(raw.p)) {
throw new TypeError("Invalid price");
}

if (!isPositiveDecimal(raw.q)) {
throw new TypeError("Invalid quantity");
}

if (typeof raw.m !== "boolean") {
throw new TypeError("Invalid maker flag");
}

if (
!Number.isSafeInteger(receivedAtMs) ||
receivedAtMs < 0
) {
throw new TypeError(
"Invalid receive timestamp"
);
}

return {
schemaVersion: 1,

eventId:
  `binance:spot:trade:${raw.s}:${raw.t}`,

source: {
  venue: "binance",
  market: "spot"
},

type: "trade",
instrument: raw.s,

sourceEventTimeMs: raw.E,
occurredAtMs: raw.T,
receivedAtMs,

sequence: String(raw.t),

payload: {
  price: raw.p,
  quantity: raw.q,
  buyerIsMaker: raw.m
}
Enter fullscreen mode Exit fullscreen mode

};
}

export function parseBinanceTradeMessage(
message,
receivedAtMs = Date.now()
) {
let raw;

try {
raw = JSON.parse(message);
} catch {
throw new TypeError("Invalid JSON message");
}

return normalizeBinanceTrade(
raw,
receivedAtMs
);
}

A note about numeric identifiers:

The example rejects trade IDs that cannot safely be represented as JavaScript integers.

A production implementation should use a lossless JSON parser if the exchange ever supplies identifiers outside that range.

That is preferable to silently corrupting an identifier.

Step 3: market-event.test.js

Now let's test it.

No live exchange connection is necessary to test the parser.

We use a recorded fixture that follows the documented Binance trade event structure.

import test from "node:test";
import assert from "node:assert/strict";

import {
normalizeBinanceTrade,
parseBinanceTradeMessage
} from "./market-event.js";

const sampleTrade = {
e: "trade",
E: 1700000000001,
s: "BTCUSDT",
t: 12345,
p: "65000.10",
q: "0.002",
T: 1700000000000,
m: false,
M: true
};

test("normalizes a Binance trade fixture", () => {
const event = normalizeBinanceTrade(
sampleTrade,
1700000000010
);

assert.equal(event.schemaVersion, 1);

assert.equal(
event.eventId,
"binance:spot:trade:BTCUSDT:12345"
);

assert.equal(event.instrument, "BTCUSDT");

assert.equal(
event.payload.price,
"65000.10"
);

assert.equal(
event.payload.quantity,
"0.002"
);

assert.equal(
event.receivedAtMs,
1700000000010
);
});

test("keeps price and quantity as decimal strings", () => {
const event = normalizeBinanceTrade(sampleTrade);

assert.equal(
typeof event.payload.price,
"string"
);

assert.equal(
typeof event.payload.quantity,
"string"
);
});

test("rejects unsupported event types", () => {
assert.throws(
() => normalizeBinanceTrade({
...sampleTrade,
e: "aggTrade"
}),
/Unsupported event type/
);
});

test("rejects malformed prices", () => {
assert.throws(
() => normalizeBinanceTrade({
...sampleTrade,
p: "not-a-price"
}),
/Invalid price/
);
});

test("rejects zero quantity", () => {
assert.throws(
() => normalizeBinanceTrade({
...sampleTrade,
q: "0.000"
}),
/Invalid quantity/
);
});

test("rejects unsafe numeric trade IDs", () => {
assert.throws(
() => normalizeBinanceTrade({
...sampleTrade,
t: Number.MAX_SAFE_INTEGER + 1
}),
/Invalid trade ID/
);
});

test("rejects missing trade identifiers", () => {
assert.throws(
() => normalizeBinanceTrade({
...sampleTrade,
t: undefined
}),
/Invalid trade ID/
);
});

test("produces a stable event identifier", () => {
const first =
normalizeBinanceTrade(sampleTrade);

const second =
normalizeBinanceTrade(sampleTrade);

assert.equal(
first.eventId,
second.eventId
);
});

test("parses a Binance JSON trade message", () => {
const event = parseBinanceTradeMessage(
JSON.stringify(sampleTrade),
1700000000010
);

assert.equal(event.type, "trade");
assert.equal(event.source.venue, "binance");
});

test("rejects malformed JSON", () => {
assert.throws(
() => parseBinanceTradeMessage("{not-json"),
/Invalid JSON message/
);
});

test("fans out an event to two in-process consumers", async () => {
const { EventEmitter } =
await import("node:events");

const bus = new EventEmitter();

const receivedByA = [];
const receivedByB = [];

const event =
normalizeBinanceTrade(sampleTrade);

bus.on("market-event", (item) => {
receivedByA.push(item.eventId);
});

bus.on("market-event", (item) => {
receivedByB.push(item.eventId);
});

bus.emit("market-event", event);

assert.deepEqual(
receivedByA,
[event.eventId]
);

assert.deepEqual(
receivedByB,
[event.eventId]
);
});

Run:

npm test

The Node.js test runner is documented here:

"Node.js Test Runner" (https://nodejs.org/download/release/v22.16.0/docs/api/test.html).

What did the actual test run show?

I ran the project in a clean temporary directory.

Environment:

Node.js: v22.16.0
npm: 10.9.2
Dependencies: none

Actual test result:

1..11

tests 11

pass 11

fail 0

cancelled 0

skipped 0

todo 0

All 11 tests passed.

The syntax checks for the JavaScript files also passed.

This verifies the offline normalization and local event distribution logic.

It does NOT verify the live Binance connection, exchange availability or production performance.

That distinction matters.


  1. Connecting the prototype to a public exchange stream

We can now add a small live ingestion example.

Create "relay.mjs":

import { EventEmitter } from "node:events";

import {
parseBinanceTradeMessage
} from "./market-event.js";

const eventBus = new EventEmitter();

// Consumer A: Observer
eventBus.on("market-event", (event) => {
console.log(
"[Observer]",
event.eventId
);
});

// Consumer B: Reporter
eventBus.on("market-event", (event) => {
console.log(
"[Reporter]",
${event.instrument} trade: +
${event.payload.price} x +
${event.payload.quantity}
);
});

const endpoint =
"wss://data-stream.binance.vision/ws/btcusdt@trade";

const socket = new WebSocket(endpoint);

socket.addEventListener("open", () => {
console.log(
"Connected to Binance public market-data stream."
);
});

socket.addEventListener("message", ({ data }) => {
try {
const event = parseBinanceTradeMessage(
String(data)
);

eventBus.emit("market-event", event);
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error(
"Rejected incoming message:",
error.message
);
}
});

socket.addEventListener("error", () => {
console.error(
"WebSocket connection error."
);
});

socket.addEventListener("close", ({ code, reason }) => {
console.error(
WebSocket closed: code=${code}, +
reason=${String(reason)}
);
});

Run:

npm start

The expected behavior is that the client connects to the public BTCUSDT Spot trade stream and sends normalized events to two local consumers.

There is an important limitation here.

"EventEmitter" distributes messages inside one Node.js process.

It does not connect independent agents running on separate machines.

It is only a demonstration of the fan-out principle.

Also, EventEmitter listeners execute synchronously. A slow listener can delay other listeners. Production systems need bounded queues and proper consumer isolation.

So we have demonstrated a small piece of the idea, not the complete Market Relay.

The live WebSocket connection still needs an actual network smoke test.


  1. MCP, A2A and NATS: which technology belongs where?

Now we get to a very interesting architectural question.

There are three technologies worth examining.

They solve different problems.

MCP: Give an agent access to information

The Model Context Protocol allows an AI client to discover and invoke tools.

For Sentinel Market Relay, I would initially expose:

get_market_snapshot
get_recent_events
get_source_status

An agent could ask:

"Give me the latest BTCUSDT snapshot."

Or:

"Show me the last 20 trade events."

Or:

"Is the Binance source currently connected?"

The official MCP specification describes tools and their input and output schemas.

It also supports resources for exposing structured information.

Interestingly, the current MCP specification also provides resource subscriptions. A server can notify subscribed clients when a resource changes.

That means we should not claim MCP cannot support updates.

It can.

However, there is an important difference between notifying an agent that a resource has changed and operating a durable, high-volume event log with replay, independent consumer positions and delivery guarantees.

For the Market Relay proposal, I would use MCP for structured agent access.

Official documentation:

NATS: Distribute events

For continuous event delivery to independent services, I would investigate NATS.

NATS provides publish-subscribe messaging.

A publisher sends an event to a subject.

Every interested subscriber receives a copy.

This is exactly the basic distribution pattern we are exploring.

But there are two important modes.

Core NATS

Messages are delivered to currently connected subscribers.

Core NATS does not persist them for later replay.

It provides at-most-once delivery.

NATS JetStream

JetStream adds persistent storage and consumer tracking.

It supports at-least-once delivery and message replay.

The distinction is important because at-least-once delivery means a consumer may receive a message more than once.

We still need idempotent event processing.

Official documentation:

I would not introduce JetStream in the first prototype.

A small application does not need a distributed messaging infrastructure merely because it looks good in an architecture diagram.

First establish that the problem exists.

Then introduce the appropriate infrastructure.

A2A: Let independent agents collaborate

The Agent2Agent protocol solves another problem.

Imagine Agent A asks Agent B:

"Investigate why today's market activity differs from yesterday."

Agent B performs its own investigation and returns a result.

That is a collaboration between agents.

A2A is designed for this type of communication.

The distinction is:

Technology| Main purpose
MCP| Agent to tools and data
NATS| Service to service event distribution
A2A| Agent to agent collaboration

The official A2A documentation describes how A2A and MCP complement each other.

"A2A and MCP" (https://a2a-protocol.org/latest/topics/a2a-and-mcp/).

For the first version of Sentinel Market Relay, I would not implement A2A.

It is not necessary for distributing market observations.

We can investigate it later if the agents themselves need to delegate work to each other.


  1. The difficult part: what happens when information becomes unreliable?

This is probably more important than the initial connection itself.

Imagine Binance disconnects.

The relay still has the last received price stored in memory.

An agent asks:

"What is the current price?"

Should we return that old value?

Not without making its age and status visible.

I would introduce explicit source states.

{
"venue": "binance",
"market": "spot",

"connection": "disconnected",

"lastEventReceivedAtMs": 1700000000010,

"lastEventAgeMs": 12000,

"dataStatus": "stale"
}

Illustrative example.

The relay should distinguish:

  • "connected"
  • "disconnected"
  • "reconnecting"
  • "stale"
  • "healthy"

The important thing is that the last known value is not automatically the current value.

Freshness should be configurable per feed.

A frequently updated trade stream and a slower statistics feed should not necessarily use the same threshold.

Duplicate events

Imagine the same trade is delivered twice.

Agent A counts it twice.

Agent B recognizes the duplicate.

Now their conclusions differ.

That is why stable event identifiers matter.

Consumers should maintain idempotency, particularly when messages are replayed or redelivered.

Missing updates

A source may disconnect or deliver an incomplete sequence.

The relay must not silently interpret missing information as evidence that nothing changed.

This is something I care about in my existing Sentinel projects as well.

An incomplete analysis must remain visibly incomplete.

The same principle applies to market observations.

Missing information should stay missing.

It should not magically transform into a confident answer.

What about the order book?

This is another reason to start small.

A market trade stream is relatively straightforward to normalize.

A local order book is more complicated.

Binance documents a specific procedure:

  1. Open a depth WebSocket stream.
  2. Buffer incoming updates.
  3. Request an order book snapshot.
  4. Compare update identifiers.
  5. Apply updates in the correct order.
  6. Rebuild the local book if an update gap is detected.

This is not something we should improvise.

Official documentation:

"How to manage a local order book correctly" (https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md).

I would leave full order book synchronization out of version one.


  1. How would we measure whether Sentinel Market Relay is actually useful?

Now we reach the part I find most interesting.

We should not assume that a shared relay is better.

We should compare it with a direct connection approach.

Scenario A: Independent agents

Three agents each maintain their own market data connection and exchange adapter.

Scenario B: Shared relay

One relay connects to the exchange.

Three agents receive the normalized information from it.

Both scenarios must receive the same underlying exchange events.

Otherwise, the comparison is meaningless.

What should we measure?

Metric| What it tells us
Upstream connections| Whether duplicate connections decrease
Event count| Whether events are missing
Duplicate rate| Whether consumers process events more than once
Delivery latency| Time from relay receipt to consumer receipt
p50 latency| Typical delivery speed
p95 latency| Slower delivery cases
Source freshness| Whether agents receive current data
Recovery time| How quickly the feed recovers after disconnection
CPU and memory| Infrastructure overhead
Agent input size| Whether structured observations reduce unnecessary context

For example:

Relay received event: 14:32:10.100
Agent A received event: 14:32:10.104
Agent B received event: 14:32:10.109
Agent C received event: 14:32:10.111

These are example timestamps, not benchmark results.

They would allow us to calculate relay-to-consumer delivery time.

For fair measurements, local processing durations should use a monotonic clock such as "performance.now()", rather than assuming clocks on different machines are perfectly synchronized.

A particularly interesting experiment

I would also measure how much duplicate work the relay removes.

Suppose three agents each request the same market history.

With direct integrations, each might retrieve and normalize the same records independently.

With the relay, one shared collection might serve all three.

But this is a hypothesis.

An optimized direct implementation could already share an exchange connection and cache.

We need to compare against that too.

The point is not to manufacture a victory for Sentinel.

The point is to find out whether it actually provides value.


  1. What would the first version look like?

I would keep the first milestone deliberately small.

Component| Version 0.1 proposal
Exchange| Binance Spot
Instruments| BTCUSDT, ETHUSDT, SOLUSDT
Access| Public market data
Connection| WebSocket
Events| Trades
Normalization| Canonical event schema
Duplicate identity| Stable event ID
Storage| Bounded in-memory history
Agent interface| MCP
Event distribution| Local prototype first
Trading permissions| None
Order execution| Not implemented

Three MCP tools are enough:

get_market_snapshot
get_recent_events
get_source_status

Then we can build the following roadmap.

Stage 1: Reliable ingestion

One exchange.

One trade stream.

Validate incoming messages.

Record timestamps and source status.

Stage 2: Multiple consumers

Create two or three independent test consumers.

Verify that they receive the same event.

Test duplicate handling and event ordering.

Stage 3: Separate event distribution

Evaluate NATS if the consumers need to run in separate processes.

Introduce persistence only when the experiment demonstrates a need for it.

Stage 4: MCP access

Expose snapshots and recent history through MCP.

Allow compatible agents to inspect the relay without implementing their own Binance adapter.

Stage 5: Real comparison

Compare direct connections with the shared relay.

Measure latency, duplicate work, dropped events, resource use and recovery behavior.

Only after that should we consider additional exchanges.


  1. Where does this fit into the existing Sentinel ecosystem?

This is where I want to be especially careful.

I don't want to turn Sentinel-IR into a giant project that tries to solve every possible problem.

Sentinel-IR already has a defined responsibility.

It extracts structured information from JavaScript and TypeScript source code.

Sentinel-IR-MCP makes that information available to compatible AI clients.

The Market Relay would be a separate component.

Something like this:

SENTINEL ECOSYSTEM
│
├── Sentinel-IR
│ │
│ └── Structural facts about source code
│
├── Sentinel-IR-MCP
│ │
│ └── Agent access to code analysis
│
└── Sentinel Market Relay
│
├── Exchange adapters
│
├── Event normalization
│
├── Source health
│
├── Event distribution
│
└── Agent access to market information

Same architectural philosophy.

Different responsibilities.

Existing projects:

Related reading:

The connection between these projects is not that they analyze the same type of data.

It is the idea that independent agents should not have to rediscover or reconstruct the same information unnecessarily.


  1. What we have not solved yet

I want to make the limitations explicit.

There is no deployed Sentinel Market Relay.

The prototype currently demonstrates:

  • Binance trade-event normalization.
  • Validation of a documented event structure.
  • Preservation of price and quantity strings.
  • Generation of stable event identifiers.
  • Local distribution to multiple listeners.
  • Offline automated tests.

It does not yet demonstrate:

  • A successful live exchange connection.
  • Multiple exchanges.
  • Persistent event history.
  • Distributed message delivery.
  • Production-grade reconnection.
  • A complete MCP server.
  • A2A communication.
  • Measured performance improvements.
  • Reduced API costs or token consumption.
  • Improved trading results.

Those claims would require separate experiments.

There are also operational questions around exchange terms, public data redistribution, rate limits, consumer authorization, memory limits and long-running connection recovery.

A public market-data feed does not automatically grant unrestricted rights to redistribute its data commercially.

That needs to be checked separately before offering a public service.

And one more point:

A shared information layer is not automatically more accurate than its sources.

It can preserve provenance and expose missing data.

It cannot magically make an incorrect exchange observation correct.


Final thoughts: What if the most useful agent never makes a decision?

I started thinking about this because trading bots already exist in enormous numbers.

We don't need to pretend that another application receiving Bitcoin prices is a revolutionary invention.

But I became interested in a different problem.

As we build more independent AI agents, they increasingly need access to common information.

Some observe.

Some investigate.

Some calculate.

Some report.

Some ask other agents for help.

And some make decisions.

I don't think every one of them should have to connect to every external system independently.

Maybe we need more infrastructure that sits between information sources and the agents that use them.

Not another artificial brain.

Not another autonomous trader.

A reliable information layer.

Something that makes it easier to answer three simple questions:

Where did this information come from?

When was it received?

Can another agent work with exactly the same observation?

That is what I would like to investigate with Sentinel Market Relay.

I don't know yet whether the idea will be useful enough to justify a separate project.

The prototype is deliberately small, and the performance claims remain unproven.

But I think the architectural question is worth asking.

Perhaps the next useful step in multi-agent systems is not always making individual agents smarter.

Perhaps it is making their shared information infrastructure better.


Developer references and further reading

All technical claims and implementation choices above can be independently checked against the following documentation.

Exchange APIs

  1. "Binance Spot WebSocket Streams" (https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md)
  2. "Binance Market Data Only Endpoint" (https://github.com/binance/binance-spot-api-docs/blob/master/faqs/market_data_only.md)
  3. "Binance Local Order Book Synchronization" (https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md)
  4. "Coinbase Advanced Trade WebSocket Feeds" (https://help.coinbase.com/en/developer-platform/websocket-feeds/advanced-trade)

Agent interoperability

  1. "Model Context Protocol: Tools" (https://modelcontextprotocol.io/specification/2026-07-28/server/tools)
  2. "Model Context Protocol: Resources" (https://modelcontextprotocol.io/specification/2026-07-28/server/resources)
  3. "A2A Protocol Documentation" (https://a2a-protocol.org/latest/)
  4. "A2A and MCP: Detailed Comparison" (https://a2a-protocol.org/latest/topics/a2a-and-mcp/)

Event distribution

  1. "NATS Publish-Subscribe" (https://docs.nats.io/concepts/pub-sub-basics)
  2. "NATS JetStream" (https://docs.nats.io/concepts/jetstream)

Node.js implementation

  1. "Node.js v22.16.0 WebSocket API" (https://nodejs.org/download/release/v22.16.0/docs/api/globals.html#websocket)
  2. "Node.js Test Runner" (https://nodejs.org/download/release/v22.16.0/docs/api/test.html)
  3. "Node.js Events API" (https://nodejs.org/download/release/v22.16.0/docs/api/events.html)

Existing Sentinel work

  1. "Sentinel-IR" (https://gitlab.com/jakubrojicek11/sentinel-ir)
  2. "Sentinel-IR-MCP" (https://gitlab.com/jakubrojicek11/sentinel-ir-mcp)
  3. "Why I Spent a Year Building Sentinel-IR?" (https://dev.to/jackymencz/why-i-spent-a-year-building-sentinel-ir-2j64)

Thanks for support..
Release: "Sentinel-IR-MCP v0.2.2 on GitLab" (https://gitlab.com/jakubrojicek11/sentinel-ir-mcp/-/tree/v0.2.2?ref_type=tags)

Package: "sentinel-ir-mcp@0.2.2 on npm" (https://www.npmjs.com/package/sentinel-ir-mcp/v/0.2.2)


Architecture proposal and offline-tested prototype. This project is not a trading system, investment recommendation or market prediction service. No real-money operations are implemented.

One feed. Multiple agents. Shared evidence.


Json prototype Sentinel Market Relay 0.1.0

{
"project": "Sentinel Market Relay",
"version": "0.1.0",
"status": "local_read_only_prototype",
"files": {
"README.md": "# Sentinel Market Relay — local prototype\n\nThis is a small, read-only architecture demonstration, not a production service and not a feature of Sentinel-IR.\n\n## Requirements\n\n- Node.js 22.4.0 or newer (the built-in WebSocket client is stable from Node.js 22.4.0).\n- No third-party npm dependencies.\n\n## Verify the parser and local fan-out\n\n

bash\nnpm test\n

\n\nThe included fixture-based suite was run successfully with Node.js v22.16.0 and npm 10.9.2: 11 tests passed, 0 failed.\n\n## Connect to the public Binance trade stream\n\n

bash\nnpm start\n

\n\nThis connects to Binance's public market-data-only WebSocket endpoint for the BTCUSDT spot trade stream. It does not authenticate, read account data, or place orders.\n\nThe live exchange connection was not tested in the environment that produced this archive. The relay script was syntax-checked; the parsing and local in-process fan-out were tested offline.\n\n## Important limitations\n\n- One Binance spot trade stream only.\n- No reconnect/backoff, persistent storage, replay, durable delivery, API server, MCP server, broker, authentication, metrics, or production backpressure handling.\n- The EventEmitter demonstration fans out only inside this one Node.js process. It is not a multi-process message broker.\n- The stable event ID can help downstream consumers deduplicate events, but this prototype does not persist a deduplication store.\n- Unsafe numeric trade IDs are rejected. A production adapter that needs to support IDs outside JavaScript's safe-integer range should use a lossless JSON parser.\n- This software is not a trading system, market predictor, or financial advice.\n",
"package.json": "{\n \"name\": \"sentinel-market-relay-prototype\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"engines\": { \"node\": \">=22.4.0\" },\n \"scripts\": { \"test\": \"node --test\", \"start\": \"node relay.mjs\" }\n}\n",
"market-event.js": "const DECIMAL_PATTERN = /^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/;\n\nfunction isPositiveDecimal(value) {\n return typeof value === 'string' &&\n DECIMAL_PATTERN.test(value) &&\n /[1-9]/.test(value);\n}\n\nexport function normalizeBinanceTrade(raw, receivedAtMs = Date.now()) {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw new TypeError('Invalid event');\n }\n\n if (raw.e !== 'trade') {\n throw new TypeError('Unsupported event type');\n }\n\n if (typeof raw.s !== 'string' || !/^[A-Z0-9]{1,30}$/.test(raw.s)) {\n throw new TypeError('Invalid symbol');\n }\n\n if (!Number.isSafeInteger(raw.t) || raw.t < 0) {\n throw new TypeError('Invalid trade ID');\n }\n\n if (!Number.isSafeInteger(raw.E) || raw.E < 0 ||\n !Number.isSafeInteger(raw.T) || raw.T < 0) {\n throw new TypeError('Invalid timestamp');\n }\n\n if (!isPositiveDecimal(raw.p)) {\n throw new TypeError('Invalid price');\n }\n\n if (!isPositiveDecimal(raw.q)) {\n throw new TypeError('Invalid quantity');\n }\n\n if (typeof raw.m !== 'boolean') {\n throw new TypeError('Invalid maker flag');\n }\n\n if (!Number.isSafeInteger(receivedAtMs) || receivedAtMs < 0) {\n throw new TypeError('Invalid receive timestamp');\n }\n\n return {\n schemaVersion: 1,\n eventId: binance:spot:trade:${raw.s}:${raw.t},\n source: { venue: 'binance', market: 'spot' },\n type: 'trade',\n instrument: raw.s,\n sourceEventTimeMs: raw.E,\n occurredAtMs: raw.T,\n receivedAtMs,\n sequence: String(raw.t),\n payload: {\n price: raw.p,\n quantity: raw.q,\n buyerIsMaker: raw.m\n }\n };\n}\n\nexport function parseBinanceTradeMessage(message, receivedAtMs = Date.now()) {\n let raw;\n try {\n raw = JSON.parse(message);\n } catch {\n throw new TypeError('Invalid JSON message');\n }\n return normalizeBinanceTrade(raw, receivedAtMs);\n}\n",
"market-event.test.js": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport {\n normalizeBinanceTrade,\n parseBinanceTradeMessage\n} from './market-event.js';\n\nconst sampleTrade = {\n e: 'trade',\n E: 1700000000001,\n s: 'BTCUSDT',\n t: 12345,\n p: '65000.10',\n q: '0.002',\n T: 1700000000000,\n m: false,\n M: true\n};\n\ntest('normalizes a Binance trade fixture', () => {\n const event = normalizeBinanceTrade(sampleTrade, 1700000000010);\n assert.equal(event.schemaVersion, 1);\n assert.equal(event.eventId, 'binance:spot:trade:BTCUSDT:12345');\n assert.equal(event.instrument, 'BTCUSDT');\n assert.equal(event.payload.price, '65000.10');\n assert.equal(event.payload.quantity, '0.002');\n assert.equal(event.receivedAtMs, 1700000000010);\n});\n\ntest('keeps price and quantity as decimal strings', () => {\n const event = normalizeBinanceTrade(sampleTrade);\n assert.equal(typeof event.payload.price, 'string');\n assert.equal(typeof event.payload.quantity, 'string');\n});\n\ntest('rejects unsupported event types', () => {\n assert.throws(\n () => normalizeBinanceTrade({ ...sampleTrade, e: 'aggTrade' }),\n /Unsupported event type/\n );\n});\n\ntest('rejects malformed prices', () => {\n assert.throws(\n () => normalizeBinanceTrade({ ...sampleTrade, p: 'not-a-price' }),\n /Invalid price/\n );\n});\n\ntest('rejects zero quantity', () => {\n assert.throws(\n () => normalizeBinanceTrade({ ...sampleTrade, q: '0.000' }),\n /Invalid quantity/\n );\n});\n\ntest('rejects unsafe numeric trade IDs', () => {\n assert.throws(\n () => normalizeBinanceTrade({ ...sampleTrade, t: Number.MAX_SAFE_INTEGER + 1 }),\n /Invalid trade ID/\n );\n});\n\ntest('rejects missing trade identifiers', () => {\n assert.throws(\n () => normalizeBinanceTrade({ ...sampleTrade, t: undefined }),\n /Invalid trade ID/\n );\n});\n\ntest('produces a stable event identifier', () => {\n const first = normalizeBinanceTrade(sampleTrade);\n const second = normalizeBinanceTrade(sampleTrade);\n assert.equal(first.eventId, second.eventId);\n});\n\ntest('parses a Binance JSON trade message', () => {\n const event = parseBinanceTradeMessage(\n JSON.stringify(sampleTrade),\n 1700000000010\n );\n assert.equal(event.type, 'trade');\n assert.equal(event.source.venue, 'binance');\n});\n\ntest('rejects malformed JSON', () => {\n assert.throws(\n () => parseBinanceTradeMessage('{not-json'),\n /Invalid JSON message/\n );\n});\n\ntest('fans out an event to two in-process consumers', async () => {\n const { EventEmitter } = await import('node:events');\n const bus = new EventEmitter();\n const receivedByA = [];\n const receivedByB = [];\n const event = normalizeBinanceTrade(sampleTrade);\n\n bus.on('market-event', (item) => receivedByA.push(item.eventId));\n bus.on('market-event', (item) => receivedByB.push(item.eventId));\n bus.emit('market-event', event);\n\n assert.deepEqual(receivedByA, [event.eventId]);\n assert.deepEqual(receivedByB, [event.eventId]);\n});\n",
"relay.mjs": "import { EventEmitter } from 'node:events';\nimport { parseBinanceTradeMessage } from './market-event.js';\n\nconst eventBus = new EventEmitter();\n\n// Two in-process consumers to demonstrate fan-out.\neventBus.on('market-event', (event) => {\n console.log('[Observer]', event.eventId);\n});\n\neventBus.on('market-event', (event) => {\n console.log(\n '[Reporter]',\n ${event.instrument} trade: ${event.payload.price} x ${event.payload.quantity}\n );\n});\n\nconst endpoint =\n 'wss://data-stream.binance.vision/ws/btcusdt@trade';\nconst socket = new WebSocket(endpoint);\n\nsocket.addEventListener('open', () => {\n console.log('Connected to Binance public market-data stream.');\n});\n\nsocket.addEventListener('message', ({ data }) => {\n try {\n const event = parseBinanceTradeMessage(String(data));\n eventBus.emit('market-event', event);\n } catch (error) {\n console.error('Rejected incoming message:', error.message);\n }\n});\n\nsocket.addEventListener('error', () => {\n console.error('WebSocket connection error.');\n});\n\nsocket.addEventListener('close', ({ code, reason }) => {\n console.error(\n WebSocket closed: code=${code}, reason=${String(reason)}\n );\n});\n"
}
}

Top comments (0)