A standalone service that tracks Stellar Network operations and streams notifications to the subscribers. The notifications are delivered as JSON-encoded HTTP POST requests.
- Highly configurable.
- Supports operations filtering by account, asset, transaction memo, and operation type.
- Guaranteed delivery with persistent storage, even if the notification recipient or the Notifier itself is down for some time.
- Supports public and private Stellar networks.
- Can be used as a shared microservice that streams events to multiple endpoints.
- Predictable performance – tested with thousands of subscriptions.
- Reliable tracking and consistency (tracking is resumed from the same transaction after restart).
- Does not require Stellar Core or Horizon node deployment.
- Notifications are signed with an ED25519 secret key to protect the recipient from spoofing.
- Web-based admin interface for subscription management and system monitoring.
- Payment processing.
Never miss a payment, even if a user decided to pay one week later, or you have some troubles with your server. - Anchors and ICOs.
Implement complex scenarios for your assets and track all user accounts. Track all operations involving your asset to gather statistics or calculate dividends. - Trading platforms.
Observe trading activity, aggregate market data, gather statistics, build market-making applications. - Monitoring tools.
Keep an eye on specific accounts or your favorite assets. Without pooling and other inefficient techniques. - Inflation pools.
Subscribe to inflation operation and distribute rewards when the subscription was triggered. - And more...
Notifier relies on an event stream from Horizon server, so you'll need to choose a public Horizon service (like https://horizon.stellar.org and https://horizon-testnet.stellar.org/), or your own hosted Horizon instance. The latter option is preferable for production setups. Nevertheless, public Horizon instances supported by SDF and other community members work just fine as well.
The service requires NodeJS 20 or later.
For the production deployment all notification queues are stored in the database.
MongoDB was selected as the most convenient option in terms of fast read/writes alongside with minimum memory/IO overhead.
Linux, Windows, MacOS.
Effectively any other platforms where you can run NodeJS and MongoDB.
This repository includes Dockerfile and docker-compose.yml.
Build and run locally:
docker build -t stellar_notifier:latest .
docker compose up -d
1. Clone the repository.
git clone https://github.com/Montelibero/operations-notifier
2. Install required NPM packages.
cd operations-notifier
npm i
3. (For production deployments) Ensure that MongoDB is running. Download it here in case if it has not been installed yet.
4. Set configuration parameters in app.config.json (see
Configuration section).
5. Start the service.
npm run start
All configuration parameters are located in app.config.json file.
The server also supports setting parameters using environment variables.
By default the server loads settings from app.config.json file, and
overwrites specific parameter if the corresponding environment variable found.
-
storageProvider
Storage provider for persistence layer (see details in Storage providers section).
Default value:"memory"
Env parameter:STORAGE_PROVIDER -
storageConnectionString
Connection string for storage provider.
Default value:""
Env parameter:STORAGE_CONNECTION_STRING -
apiPort
API exposed by the notifier.
Default value:4021
Env parameter:API_PORT -
apiHost
API bind host (useful for tests or restricted environments).
Default value:""(bind all interfaces).
Env parameter:API_HOST -
horizon
Horizon server URL.
Default value:"https://horizon.stellar.org".
Env parameter:HORIZON -
networkPassphrase
Stellar network passphrase used to parse transaction XDR.
Default value:"Public Global Stellar Network ; September 2015".
Env parameter:NETWORK_PASSPHRASE -
horizonAllowHttp
Allow insecure HTTP Horizon URLs (use for local mocks).
Default value:false.
Env parameter:HORIZON_ALLOW_HTTP -
reactionResponseTimeout
Maximum expected HTTP response timeout (in seconds). If the reaction URL takes more time to respond, the request is aborted and notification is marked as failed.
Default value:20.
Env parameter:REACTION_RESPONSE_TIMEOUT -
authorization
Server authorization mode.
disabledmode turns off the authentication (recommended for local installations).
Any other value enables ed25519 authentication (see API Authentication).
Default value:"disabled".
Env parameter:AUTHORIZATION -
adminAuthenticationToken
API authentication token for admin user. Leave empty to disable admin token auth.
Default Value:"".
Env parameter:ADMIN_AUTHENTICATION_TOKEN -
userTokens
List of allowed user tokens forauthorization: "token"mode. If empty, any token is accepted.
Default value:[].
Env parameter:USER_TOKENS(comma-separated) -
signatureSecret
Secret key used to sign the notifications. Do not forget to set your own secret. It can be easily generated using Stellar Laboratory. Do not use the default value or a secret key from the funded account.
Default value:"".
Env parameter:SIGNATURE_SECRET -
maxActiveSubscriptions
Maximum simultaneously tracked subscriptions.
Default value:10000.
Env parameter:MAX_ACTIVE_SUBSCRIPTIONS -
maxActiveSubscriptionsPerUser
Maximum simultaneously tracked subscriptions per user.
Default value:100.
Env parameter:MAX_ACTIVE_SUBSCRIPTIONS_PER_USER -
notificationConcurrency
Maximum concurrent notification threads (effectively, it equals the maximum parallel pending HTTP requests).
Default value:100.
Env parameter:NOTIFICATION_CONCURRENCY -
ledgerWorkers
Maximum number of parallel ledger fetch workers for transaction ingestion.
Default value:10.
Env parameter:LEDGER_WORKERS -
maxDeliveryFailures
Maximum consecutive delivery failures for a subscription before the current notification is dropped as lost.
Default value:100.
Env parameter:MAX_DELIVERY_FAILURES -
maxNotificationAgeSeconds
Maximum age (in seconds) of a notification before it is dropped as lost.
Default value:604800(7 days).
Env parameter:MAX_NOTIFICATION_AGE_SECONDS -
maxConsecutiveLostNotifications
Maximum consecutive lost notifications for a subscription before it is automatically removed.
Default value:10.
Env parameter:MAX_CONSECUTIVE_LOST_NOTIFICATIONS -
resetCursor
When set totrue, ignores the saved cursor on startup and begins from the live stream (current ledger). The cursor continues to be saved normally. Useful for skipping large backlogs after extended downtime.
Default value:false.
Env parameter:RESET_CURSOR -
logLevel
Log verbosity level (DEBUG, INFO, WARN, ERROR).
Default value:"INFO".
Env parameter:LOG_LEVEL
Lost notifications are dropped when they exceed the max age or max consecutive delivery failures. The lost counter resets to 0 after a successful delivery.
Notifier can store notification queues and operational information in the database or in memory. The latter option is suited only for testing or deployments without guaranteed delivery.
There are two built-in storage providers: mongodb and memory.
Other storage providers can be easily connected through the standard interface.
{
"authorization": "disabled",
"storageProvider": "mongodb",
"storageConnectionString": "mongodb://127.0.0.1:27017/notifier",
"apiPort": 4021,
"apiHost": "",
"horizon": "https://horizon.stellar.org",
"networkPassphrase": "Public Global Stellar Network ; September 2015",
"horizonAllowHttp": false,
"signatureSecret": "SDBT736EJIIRDC3RSN544NO6OSNMZAWAKRARLOMRP2XJAOGKTQLFFR3V",
"maxActiveSubscriptions": 10000,
"maxActiveSubscriptionsPerUser": 100,
"notificationConcurrency": 100,
"ledgerWorkers": 10,
"maxDeliveryFailures": 100,
"maxNotificationAgeSeconds": 604800,
"maxConsecutiveLostNotifications": 10,
"reactionResponseTimeout": 20,
"userTokens": [],
"adminAuthenticationToken": "98c12910bf35c79a800e9ea893a93b078ea92fc7a26ca76c0cd2f6003464d781"
}
If you run a local Horizon mock over HTTP (e.g., http://127.0.0.1:1234), set:
horizonAllowHttp: trueinapp.config.json, orHORIZON_ALLOW_HTTP=truein the environment.
With authorization config parameter set to disabled, there is no need to
bother about authentication. But it only works when the service is not exposed
to public networks (i.e. behind the firewall).
Authentication modes:
authorization: "disabled"— no auth checks (local/dev only).authorization: "ed25519"or any truthy value except"token"— ed25519 signatures with nonce.authorization: "token"— simple per-user token auth (no nonce/signatures).
ed25519 mode
All requests must include Authorization: ed25519 <public_key>.<signature> (or
X-Access-Token). The signature is computed over urlencoded request params
serialized in hex format. All requests must include a nonce param that is
greater than the previously used nonce for this public key.
token mode
All requests must include Authorization: <user_token> (or X-Access-Token).
The token identifies the user; changing the token gives you a new empty account.
To restrict tokens, set userTokens (or USER_TOKENS=token1,token2,token3).
Admin requests can use Authorization: <ADMIN_AUTHENTICATION_TOKEN> in any mode.
GET /api/status— lightweight health/info endpoint. Returns JSON with:version— service version.uptime— human-readable uptime.publicKey— signer public key.observing— whether the observer is running.subscriptions— active subscription count.lostNotifications— sum of consecutive lost notifications across active subscriptions.lastIngestedTx— paging token of the last processed transaction.stream— stream status (last ledger, last tx hash/paging token, queue length, reconnect delay).notifier— notifier status (in-flight notifications).
{
"version": "0.7.0",
"uptime": "3h 10m 42s",
"publicKey": "G...SIGNER...",
"observing": true,
"subscriptions": 123,
"lostNotifications": 0,
"lastIngestedTx": "1234567890",
"stream": {
"streaming": true,
"lastLedger": 123456,
"lastTxPagingToken": "1234567890",
"lastTxHash": "abcdef...",
"queueLength": 0,
"processing": false,
"reconnectDelay": 1000
},
"notifier": {
"inProgress": 1
}
}
GET /api/nonce
Returns the current nonce for the authenticated public key.
Sign the static payload nonce:<public_key> and send it as:
Authorization: ed25519 <public_key>.<signature>
Admin requests can query another user:
GET /api/nonce?pubkey=<public_key>
Authorization: <ADMIN_AUTHENTICATION_TOKEN>
const {Keypair} = require('@stellar/stellar-sdk')
//encode path components
function encodeUriParams(object) {
if (!object) return ''
return Object.keys(object)
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(object[k])}`)
.join('&')
}
//GET request
const payload = encodeUriParams({
nonce: Date.now(),
foo: 'foo'
})
const keyPair = Keypair.fromSecret('private_key'),
signature = keyPair.sign(payload).toString('hex')
const authHeader = `ed25519 ${keyPair.publicKey()}.${signature}`,
requestUrl = `<protected_api_endpoint>?${payload}`
//set header and make request
...
//POST request
const body = {
nonce: Date.now(),
foo: 'foo'
}
const keyPair = Keypair.fromSecret('private_key'),
signature = keyPair.sign(encodeUriParams(body)).toString('hex')
const authHeader = `ed25519 ${keyPair.publicKey()}.${signature}`
//set header, body and make request
POST /api/subscription
Parameters:
- reaction_url [string] (mandatory) - a full URL that should be requested by notification
- account [string] (optional) - an account address to track
- memo [string] (optional) - expected transaction memo
- asset_code [string] (optional) - asset code to track (specify XLM and no issuer to track operations involving XLM)
- asset_issuer [string] (optional) - asset issuer to track
- operation_types [Array<Number>] (optional) - operation types to track (currently 0..10), array or comma-separated string. You can find numbers at https://developers.stellar.org/docs/data/analytics/hubble/data-catalog/data-dictionary/bronze/history-operations
- expires [date/epoch] (optional) - expiration date for subscription
Note: if authentication is enabled, include a nonce param in the request body
that is greater than the previous nonce used for this public key.
Note: at least one operation filtering criteria should be specified.
Response example:
{
"id": "5ae88ef89a9e5d4a589cf27d",
"pubkey": "GCFX...PUBLIC_KEY...",
"status": 0,
"delivery_failures": 0,
"lost_notifications": 0,
"sent": 8,
"reaction_url": "http://localhost:4022/test/reaction",
"operation_types": [
0,
1
],
"created": "2018-05-01T15:59:52.475Z",
"updated": "2018-05-01T16:01:19.742Z"
}
The id value from response can be used to delete the subscription.
POST /api/subscription
The same endpoint accepts an array of subscription objects for batch creation. All subscriptions are created in a single database operation, which is significantly faster than creating them one by one.
Important: the request body must be a JSON array at the root level, not
an object with a nested array. Each element has the same parameters as single
creation above. Authorization is passed via the Authorization header as usual.
Request example:
POST /api/subscription
Authorization: <token>
Content-Type: application/json
[
{
"reaction_url": "http://example.com/webhook",
"account": "GCFX...ACCOUNT1...",
"operation_types": [0, 1]
},
{
"reaction_url": "http://example.com/webhook",
"account": "GDLV...ACCOUNT2...",
"operation_types": [0, 1]
},
{
"reaction_url": "http://example.com/webhook",
"account": "GDUQ...ACCOUNT3...",
"asset_code": "EURMTL",
"asset_issuer": "GACKTN5DAZGWXRWB2WLM6OPBDHAMT6SJNGLJZPQMEZBUR4JUGBX2UK7V"
}
]
Response: an array of created/existing subscription objects in the same order as the request.
[
{
"id": "5ae88ef89a9e5d4a589cf27d",
"pubkey": null,
"status": 0,
"reaction_url": "http://example.com/webhook",
"account": "GCFX...ACCOUNT1...",
"operation_types": [0, 1],
"lost_notifications": 0
},
{
"id": "5ae88f54397cbf2474ef27e1",
"pubkey": null,
"status": 0,
"reaction_url": "http://example.com/webhook",
"account": "GDLV...ACCOUNT2...",
"operation_types": [0, 1],
"lost_notifications": 0
},
{
"id": "5ae89a12397cbf2474ef28b3",
"pubkey": null,
"status": 0,
"reaction_url": "http://example.com/webhook",
"account": "GDUQ...ACCOUNT3...",
"asset_code": "EURMTL",
"asset_issuer": "GACKTN5DAZGWXRWB2WLM6OPBDHAMT6SJNGLJZPQMEZBUR4JUGBX2UK7V",
"lost_notifications": 0
}
]
Limits and deduplication:
- Maximum request body size: 2MB (~10000 subscriptions per request).
- Duplicates within the batch are deduplicated automatically.
- Subscriptions that already exist are returned as-is (not recreated).
- Subscription limits (
maxActiveSubscriptions,maxActiveSubscriptionsPerUser) are checked once for the entire batch. - An empty array returns 400 Bad Request.
DELETE /api/subscription/:subscription_id
The endpoint returns 200 in case of successful unsubscription, and an error code otherwise.
Parameters:
- subscription_id [string] (mandatory) - the subscription id to remove
GET /api/subscription
Response example:
[
{
"id": "5ae88ef89a9e5d4a589cf27d",
"pubkey": "GCFX...PUBLIC_KEY...",
"status": 0,
"delivery_failures": 0,
"lost_notifications": 0,
"sent": 8,
"reaction_url": "http://localhost:4022/test/reaction",
"operation_types": [
0,
1
],
"created": "2018-05-01T15:59:52.475Z",
"updated": "2018-05-01T16:01:19.742Z"
},
{
"id": "5ae88f54397cbf2474ef27e1",
"pubkey": "GCFX...PUBLIC_KEY...",
"status": 0,
"operation_types": [],
"delivery_failures": 4,
"lost_notifications": 0,
"sent": 0,
"reaction_url": "http://localhost:4022/test/reaction2",
"asset_type": 0,
"created": "2018-05-01T16:01:24.336Z",
"updated": "2018-05-01T16:02:01.036Z"
}
]
Each subscription object may include:
id— subscription idpubkey— owner public key (null for admin-created anonymous subscriptions)status— 0 active, 1 deletedreaction_url— webhook URLaccount/asset_*/memo/operation_types— filter fields (optional)delivery_failures— consecutive delivery failureslost_notifications— consecutive lost notifications (resets on success)sent— delivered notifications countcreated/updated— timestamps
GET /api/delivery-log
Returns the last 50 webhook deliveries (stored in memory, resets on restart). Admin users see all entries; regular users see only deliveries for their own subscriptions.
Response example:
[
{
"timestamp": "2026-02-06T20:44:09.955Z",
"status": "ok",
"subscriptionId": "698626823b066f0320172af7",
"pubkey": "JHG675FD3wLJ76GH4dSKY",
"reaction_url": "http://example.com/webhook",
"notificationId": "698651fc0c95143647a0580c",
"payload": {
"id": "698651fc0c95143647a0580c",
"subscription": "698626823b066f0320172af7",
"type": "operation",
"created": "2026-02-06T20:41:32.405Z",
"sent": "2026-02-06T20:44:09.955Z",
"operation": { "..." : "..." },
"transaction": { "..." : "..." }
}
}
]
Error entries also include an error field (e.g. "HTTP 500", "ECONNREFUSED").
The service includes a web-based admin interface for easy subscription management and system monitoring.
The admin UI is accessible at:
http://your-server:4021/admin
The admin interface will load without authentication, allowing you to enter your admin token in the web interface. Authentication to the API happens when you:
- Enter your admin token in the settings panel
- Save the configuration
Your token is stored securely in the browser's local storage for future sessions.
- System status monitoring (uptime, subscriptions count, ledger status)
- Create, view and delete subscriptions
- Real-time API logs
- Automatic server address detection
The admin UI is enabled by default. You can control it with the following configuration:
adminUiEnabledinapp.config.json(default:true)- or
ADMIN_UI_ENABLEDenvironment variable
The admin interface uses the existing adminAuthenticationToken for access control.
Notifications are sent as JSON-encoded POST requests with the following format:
{
"id": "5ae88ef89a9e5d4a589cf27d-757838947790641a6",
"subscription": "5ae88ef89a9e5d4a589cf29d",
"type": "operation",
"created": "2018-05-01T16:45:37.529Z",
"sent": "2018-05-01T16:45:37.991Z",
"operation": {
"id": "7578389477906432555",
"type_i": 1,
"type": "payment",
"destination": "GC3UA6FHLDD7IFG3MXA7JCNC2YPW62AKMKV5H2CEEBWCLUNJR3OFJSNV",
"asset": {
"asset_code": "ETH",
"asset_issuer": "GCNSGHUCG5VMGLT5RIYYZSO7VQULQKAJ62QA33DBC5PPBSO57LFWVV6P",
"asset_type": "1"
},
"amount": "0.0012",
"account": "GCUZSLQYXDXSOXC4DURSK3AJNAQEBDSYVGYE7BC7IXGTS32MJBW35QOU"
},
"transaction": {
"hash": "7580614b4703704496173f578092a0b6a56e5906d7ad5f83e529af14bab2cdd3",
"fee": 600,
"source": "GCUZSLQYXDXSOXC4DURSK3AJNAQEBDSYVGYE7BC7IXGTS32MJBW35QOU",
"paging_token": "757838947790643264",
"source_account_sequence": "7196037745321120376",
"created_at": "2018-05-01T16:45:37Z",
"memo": {
"type": "text",
"value": "105683157"
},
"result_xdr": "AAAAAAAAAJQA...",
"envelope_xdr": "AAAAAgAAAAB..."
}
}
See RESPONSE_EXAMPLES.md for examples of all operation types.
Request headers:
X-Request-ED25519-Signature– the ED25519 signature for the notification body (base64).X-Subscription– corresponding subscription id.
Run all tests with
npm run test