@odatano/watch
Watches Cardano addresses, payment credentials and minting policies from inside CAP, emitting events with full UTxO payloads and persisting them so a missed window can be replayed.
Persistence for replay is what makes it a CAP plugin rather than a webhook: the chain does not resend, so the record of what was seen has to live somewhere the application can re-read.
Facts
Where it sits
Who is behind it
In the atlas: maintains 4@ODATANOmaintainerMaxContributes to this
In the atlas: contributes to 4@maxalexweber11 contributor
Before choosing this
Conditions
LicenceSAP Developer License Agreement 3.2 CAP§ 1, 2, 3A cds-plugin is used with SAP CAP materials, or an artifact is explicitly identified as a CAP plugin or application.
- § 1 License. Productive customer applications are limited to SAP BTP or another SAP-licensed platform, and mass extraction to a non-SAP product is restricted except where solely required for SAP interoperability.
- § 2 Intellectual Property. The agreement retains SAP's rights in the covered materials and limits making APIs, tools or software available to third parties, while preserving ownership of customer applications subject to SAP's underlying rights.
- § 3 Artificial Intelligence Training. The agreement prohibits use of the covered materials and customer applications for training artificial-intelligence models or systems.
Also, from Async processing§ 1: Messaging is not automatically mass extraction, but an external broker can cross the boundary named in section 1. The data flow, volume and interoperability purpose therefore need to be checked for the concrete design.
What it declares
Every artifact
- Type
- CDS plugin
- Origin
- Independently maintained
- Runtime
- Node.js
As a CDS plugin
- Declared for
- >=9.0.0
- Distributed through
- npm
Connections
Doing work outside the request that triggered it, with retries
Download
Install
npm add @odatano/watchDerived from the package name. No recipe recorded; the README has the configuration.
README
quoted from ODATANO/ODATANO-WATCH under Apache-2.0, read
ODATANO-WATCH
CAP plugin for monitoring the Cardano blockchain. Watches addresses, payment credentials, and minting policies; emits events with rich UTxO payloads (assets, inline datums, reference scripts) and persists them for replay.
Install
npm add @odatano/watch
Configure
package.json:
{
"cds": {
"requires": {
"watch": {
"network": "preview",
"blockfrostApiKey": "preview_YOUR_KEY",
"autoStart": true
}
}
}
}
Credential and policy watching are off by default — opt in via credentialPolling / policyPolling. Credential watching also needs a (free-tier-OK) Koios endpoint. See Quick Start for the full config.
Secrets via environment variables
To keep the API key out of package.json, leave the field unset and export the env var instead — the plugin picks it up as a fallback:
{ "cds": { "requires": { "watch": { "network": "preview" } } } }
export BLOCKFROST_API_KEY=preview_YOUR_KEY
Other recognized env vars: BLOCKFROST_CUSTOM_BACKEND, KOIOS_API_KEY, OGMIOS_URL, WATCHER_BACKEND. cds.env.requires.watch.<field> always wins when set.
Self-hosted Blockfrost (Dolos / cardano-node)
Polling burns through the Blockfrost free-tier daily quota quickly. To route the SDK at a self-hosted Blockfrost-compatible endpoint instead, set blockfrostCustomBackend (and drop blockfrostApiKey):
{
"cds": {
"requires": {
"watch": {
"network": "mainnet",
"blockfrostCustomBackend": "http://localhost:3100/api/v0",
"credentialPolling": { "enabled": true, "interval": 30 }
}
}
}
}
http://localhost:3100/api/v0 is the default for Dolos's MiniBF. Equivalent env var: BLOCKFROST_CUSTOM_BACKEND. Public Blockfrost stays the default when this field is unset.
Subscribe to events
import type {
NewTransactionsEvent,
CredentialNewTransactionsEvent,
PolicyAssetMintedEvent,
} from "@odatano/watch";
cds.on("cardano.newTransactions", async (e: NewTransactionsEvent) => {
// e.address, e.tag?, e.transactions[], e.utxosCreated[], e.utxosSpent[]
for (const utxo of e.utxosCreated) {
if (utxo.inlineDatumHex) await applyDatum(utxo.inlineDatumHex);
}
});
cds.on("cardano.credential.newTransactions", async (e: CredentialNewTransactionsEvent) => { /* ... */ });
cds.on("cardano.policy.assetMinted", async (e: PolicyAssetMintedEvent) => { /* ... */ });
Other emitted events: cardano.policy.assetBurned, and (Phase 2 / Ogmios only) cardano.rollback.
Read Event Delivery Semantics before building stateful consumers — there are guarantees we don't make (no cross-handler ordering, no retry, no backpressure) and a checklist of patterns to follow.
Add a watch
POST /odata/v4/cardano-watcher-admin/addWatchedAddress
Content-Type: application/json
{
"address": "addr_test1...",
"tag": "my-pool",
"includesAssetsJson": "[{\"policyId\":\"...\",\"assetNameHex\":\"\"}]",
"coalesceMs": 2000
}
Plus addWatchedCredential, addWatchedPolicy, removeWatched*. Optional fields:
tag— echoed back on each emit for dispatch routing.includesAssetsJson— asset allowlist; non-matching txs are skipped.coalesceMs— batch bus emits into one event per window with cumulative deltas (1 – 300_000).
Docs
Replay missed events
POST /odata/v4/cardano-watcher-admin/getEventsSince
Content-Type: application/json
{ "scope": "address", "key": "addr_test1...", "fromBlock": 12345 }
Returns persisted BlockchainEvent rows ordered by blockHeight asc. Use the last returned blockHeight as the next call's fromBlock for cursor pagination.
Phase 2 — Ogmios chainSync
Set backend: 'ogmios' and ogmiosUrl: 'ws://localhost:1337' to switch from polling to a chainSync stream with native rollback signal. Backfill from Blockfrost runs once per new watch. See Architecture → Phase 2 for caveats.
License
History
Breaking 2, Changed 2, Fixes 1
Notes
Bugfix release. Two issues prevented consumers from running the plugin against the README-documented config: the plugin patched the wrong requires-key, and env-var fallback names did not match the README naming.
Fixed
- Plugin requires-key mismatch — entities not deployed (Blocker). README documented
cds.requires.watchbutsrc/plugin.tsregistered the kind ascardano-watcherand patchedcds.env.requires['cardano-watcher']. Theif (reqEntry)guard never fired for consumers using the README config, so the plugin'smodelarray was never set on the requires entry. Result:cds deploynever generated DDL forWatcherCursor/WatchedCredential/WatchedAddress/WatchedPolicy/BlockchainEvent/TransactionSubmission/WatcherConfig, and the polling loop crashed withno such table: CardanoWatcherAdminService_WatchedCredentials. Plugin now registers the kind aswatchand patchescds.env.requires['watch'], matching the README.
Changed
- Env-var fallback names aligned with README naming.
BLOCKFROST_KEY→BLOCKFROST_API_KEY,KOIOS_KEY→KOIOS_API_KEY.BLOCKFROST_CUSTOM_BACKEND,OGMIOS_URL,WATCHER_BACKENDunchanged. Consumers can now leave the field out ofpackage.jsonand export the env var instead — the plugin reads it directly whencds.env.requires.watch.<field>is unset. Documented inREADME.mdanddocs/SETUP.md. - Docs: removed broken
${VAR}substitution pattern. Previousdocs/SETUP.mdsuggested"blockfrostApiKey": "${BLOCKFROST_KEY}"— CAP's env loader does not substitute${...}placeholders insidecds.requires.*, so this pattern always produced a literal"${BLOCKFROST_KEY}"string at runtime. Replaced with the env-var fallback path described above.
Breaking (semantic)
- Consumers who were using
cds.requires.cardano-watcheras the requires key (i.e. matching the pre-0.1.8 plugin code rather than the README) must rename tocds.requires.watch. Anyone following the documented config is unaffected. - Consumers using
BLOCKFROST_KEY/KOIOS_KEYenv vars must rename toBLOCKFROST_API_KEY/KOIOS_API_KEY. No alias is kept.
Features 3
Notes
Adds support for routing the Blockfrost SDK at a self-hosted Blockfrost-compatible endpoint (e.g. Dolos's MiniBF) instead of the public Blockfrost service. Polling-heavy deployments would otherwise burn through the public free-tier daily quota; pointing at a local MiniBF removes the ceiling entirely.
Added
blockfrostCustomBackendconfig field (src/config.ts,src/blockfrost.ts). When set, the Blockfrost SDK is constructed withcustomBackendinstead of the public-network preset, andblockfrostApiKeybecomes optional. Env fallback:BLOCKFROST_CUSTOM_BACKEND. Default for Dolos's MiniBF ishttp://localhost:3100/api/v0.- README and
docs/SETUP.md/docs/QUICKSTART.mdupdated with the self-hosted Blockfrost setup. - Unit tests in
test/unit/blockfrost.test.ts,test/unit/config.test.ts,test/unit/watcher.test.tscovering the custom-backend code path (constructor wiring, config resolution, API-key-optional case).
Fixes 1, Other 1
Notes
Hotfix release. 0.1.5 shipped with a stale path in the CAP plugin entry — the package boot-crashes on
npm installfor any consumer.Fixed
cds-plugin.jsreferences the pre-0.1.5 layout (Blocker). The 0.1.5 packaging migration updatedpackage.json.mainfromdist/src/index.jstosrc/index.js, butcds-plugin.js(a separate CAP plugin entry, not generated from TS) was missed and still required./dist/src/plugin— a path that no longer exists after the in-place build. Result:MODULE_NOT_FOUNDon every CAP startup. One-line fix torequire('./src/plugin').
Maintenance
.gitignore: ignore the in-place build artifacts (src/**/*.js,src/**/*.d.ts,srv/**/*.js,srv/**/*.d.ts) so they're shipped in the npm tarball but not tracked in git.
Breaking 1, Changed 3, Fixes 5
Notes
Bugfix release covering four blocker/major issues that prevented a clean out-of-the-box experience after the 0.1.4 feature wave. No new features.
Fixed
- Packaging —
srv/admin-servicenot loadable from a fresh install (Blocker). Previous tarball shippedsrv/admin-service.tsonly; CAP's@impl: '@odatano/watch/srv/admin-service'couldn't resolve a.jsfile. Switched to in-place compilation via newtsconfig.build.json(outDir: "."). Nowsrv/admin-service.jsships alongside the source. - Packaging —
#cds-models/...runtime require failed for consumers (Blocker). Replacedrequire('#cds-models/CardanoWatcherAdminService')with literal relative pathrequire('../@cds-models/CardanoWatcherAdminService/index.js').@cds-models/directory now ships in the tarball. Consumers no longer need to mirror animportsfield in their ownpackage.json. - Koios endpoint
/credential_addressdoes not exist (Blocker).getAddressesByCredentialnow hits/credential_utxos(the only Koios endpoint that returns addresses for a credential), dedups byrow.address, and paginates viaRangeheader. Caveat: returns only currently-unspent addresses — sufficient for credential watching's filter-set use case. - Hot script credentials triggered genesis-backfill stampede (Major).
addWatchedAddress/addWatchedCredential/addWatchedPolicynow defaultlastCheckedBlockto the current Blockfrost tip (wasnull, which made the first poll fetch from genesis). Hits Koios's pagination cap on creds with millions of historical txs (Minswap V2, etc.) are no longer the OOTB experience. Under Ogmios mode the cursor staysnullso the chainSync-cap'd backfill can still run. backfill.tsESLint failure — overengineeredinfer _conditional type replaced with direct type imports.
Changed
- Build pipeline:
tsc→tsc -p tsconfig.build.json. Newbuild:typecheckscript runs the original (no-emit) tsc for IDE-style verification. package.json:main:dist/src/index.js→src/index.jstypes:dist/src/index.d.ts→src/index.d.tsfilesarray: explicit globs (*.js,*.d.ts,*.cds) instead of directory entries — strips.tssource from the published tarball (65 → 45 files).
- TypeScript module resolution:
moduleResolution: "node"(deprecated alias) →"node16".modulealigned to"node16". Two dynamicimport('./index')calls insrc/plugin.tsupdated toimport('./index.js'). Jest gains amoduleNameMapperto strip.jssuffixes back to TS source for compile-on-the-fly tests.
Breaking (semantic)
- Watch-add no longer backfills history by default. A freshly-added watch sees only forward activity (from the current Blockfrost tip onward). This is the corrected behavior — previous "default to genesis" caused the OOTB stampede. Consumers needing history can call
backfill.backfillAddress()/backfillCredential()/backfillPolicy()programmatically, or PATCH the watch entity to set an earlierlastCheckedBlock.
- Packaging —
Features 29, Changed 4, Fixes 2, Other 5
Notes
Added
Watch scopes
WatchedCredentialentity (P1) — watch a payment credential across every bech32 derivative that shares it. Required for shared-script watching (Indigo CDP-manager, Minswap V2 pools, etc.). Backed by Koios for credential→addresses resolution.WatchedPolicyentity (P4) — watch a minting policy for mint/burn events. Suited to stablecoin/utility-token policies; high-asset NFT policies refused viapolicyAssetCap(default 100).
Event payloads (P2 / P3 / P5)
cardano.newTransactionspayload extended in-place withtag,utxosCreated[],utxosSpent[]. UTxO entries carrylovelace,assets[], and (when present)inlineDatumHexandreferenceScriptHash.- New event
cardano.credential.newTransactions— same UTxO-rich shape, scoped to a payment credential. - New events
cardano.policy.assetMintedandcardano.policy.assetBurned— per-event semantics, payload{ policyId, tag?, assetNameHex, quantity, txHash, blockHeight }. - All events carry an optional consumer-supplied
tagfor dispatch routing.
Asset filter (P8)
includesAssetsJsoncolumn onWatchedAddressandWatchedCredential. JSON array of{policyId, assetNameHex}. Non-matching txs are skipped (cursor still advances).addWatchedAddress/addWatchedCredentialaccept the filter.- Public helpers
parseAssetFilter,matchesAssetFilter, typeAssetFilterEntry.
Coalesce window (P9)
coalesceMscolumn onWatchedAddressandWatchedCredential(1 – 300_000 ms). Bus events fire at most once per window with cumulative deltas;BlockchainEventrows persist immediately. Tumbling-window semantics.stopWatcherflushes pending buffers.
Replay API
- New action
getEventsSince(scope, key, fromBlock?, limit?).scope:'address' | 'credential' | 'policy'. Returns persistedBlockchainEventrows ordered byblockHeightascending. Cursor pagination viafromBlock(exclusive); default limit 1000, hard cap 10_000.
Phase 2 — Ogmios chainSync backend (
backend: 'ogmios')src/ogmios.ts— chainSync wrapper with reconnect / exponential backoff, intersection negotiation.src/ogmios-watcher.ts— per-block filter pass: in-memory watch index (refreshed every 10 s), derivesWatchedUtxo/ mint / burn shapes fromSchema.Block, persistsBlockchainEventrows taggedbackend: 'ogmios'withslot, advancesWatcherCursor.src/backfill.ts— Blockfrost-based one-shot history fetch on watch-add under Ogmios mode. Capped at the chainSync cursor's tip. Backfill rows taggedbackend: 'blockfrost'so rollback never touches them.RollBackwardhandler: deletesbackend: 'ogmios'rows past the rollback slot, recomputes per-watchlastCheckedBlock, drops pending coalesce buffers, emitscardano.rollback { fromSlot, toSlot, affectedTxHashes }.- New entity
WatcherCursor(single-row, id'chainsync') persists chainSync state across restarts.
Schema
BlockchainEvent.slot(Integer64, nullable) — populated by Ogmios backend.BlockchainEvent.backend(String) —'blockfrost'or'ogmios'. Scopes rollback to Ogmios rows only.BlockchainEvent.address,.credential,.policyassociations for replay scope dispatch.WatcherActionResult,WatcherBackend,PaymentCredHex,PolicyId,AssetNameHextypes.
Configuration
backend: 'blockfrost' | 'ogmios'(default'blockfrost').ogmiosUrl: string(default'ws://localhost:1337'); env fallbackOGMIOS_URL.koiosApiKey: string(optional; env fallbackKOIOS_KEY). Required only for credential watching.credentialPolling,policyPollingpolling configs (off by default).policyAssetCap: number(default 100).
Validators
isPaymentCredHex,isPolicyId,isAssetNameHex,isAssetFilterJson,isCoalesceMs,isWatchScope.
Public types
WatchedUtxo,SpentUtxoRef,WatchedAsset,PolicyAssetEvent,AssetFilterEntry,CredentialNewTransactionsEvent,PolicyAssetMintedEvent,PolicyAssetBurnedEvent,RollbackEvent.
Documentation
- New Event Delivery Contract section in
ARCHITECTURE.md— guarantees, non-guarantees, consumer guidance for state-machine handlers, replay patterns, coalesce+replay interaction. - README rewritten ~600 → ~100 lines; full reference content lives in
docs/. QUICKSTART.md,ARCHITECTURE.md,SETUP.mdrefreshed for the current entity / event / action surface.
Changed
- Pagination correctness:
getCredentialTxsSincenow paginates Koios via PostgRESTRangeheaders (1000-row pages, 100k-row safety cap per poll). Previously silently dropped rows past the first page on busy credentials. - Pagination correctness:
fetchPolicyAssetEventsswitched fromassetsHistory(100-row default) toassetsHistoryAll. Previously truncated mint/burn history on high-velocity policies. extractUtxoDeltassignature: takesSet<string>of watched addresses (was a single string) so the credential watcher can pass every bech32 derivative at once. Address watchers wrap their single address.- Polling section now describes four independent paths (address, transaction, credential, policy) instead of two.
Fixed
- Genesis cursor bug:
processAddressno longer treatslastCheckedBlock = 0ornullas "no cursor". Fresh-added watches now actually fire on first poll; block 0 is honored as a real cursor. fetchAddressTransactionsfalsy-check fix:if (fromBlock != null ...)instead ofif (fromBlock ...)to allow block 0.
Known limitations
- Ogmios backend has no
utxosSpentdetection: Ogmios'sinputsare outpoints, not (address + value). The polling backend continues to detect spends; switching to Ogmios is currently output-only. referenceScriptHashnot populated under Ogmios: Ogmios delivers full script body; consumer can hash if needed.- Coalesce buffers not threaded through Ogmios chainSync emits in this release —
coalesceMsworks under Blockfrost mode only. cardano.transactionConfirmedevent declared in the public type surface but still not emitted; planned alongside Phase 2 follow-up work.- Backfill cap uses
getLatestBlockrather than slot→block translation. Rare overlap window with chainSync at the boundary; documented.
Fixes 2
Notes
Fixed
- CDS Model Auto-Discovery: Plugin's CDS models (schema, admin service) were not loaded by consumer apps. CAP's
_link_required_services()runs during env construction beforecds-plugin.jsloads, so themodelarray set oncds.env.requires.kindswas never merged into the requires entry. Fixed by settingmodeldirectly oncds.env.requires['cardano-watcher']in addition to the kind registration. - @impl Path Resolution:
CardanoWatcherAdminServiceused a relative@implpath (srv/admin-service) which CAP resolved against the consumer app root instead of the plugin package root, causingCannot find moduleerrors. Fixed by using the package-qualified path@odatano/watch/srv/admin-service.
- CDS Model Auto-Discovery: Plugin's CDS models (schema, admin service) were not loaded by consumer apps. CAP's
Features 5, Changed 3, Fixes 4, Other 3
Notes
Added
- GitHub Actions CI workflow for automated testing on Node.js 20.x and 22.x
- Test coverage reporting with Codecov integration
test:coveragenpm script for generating coverage reportscds:typesnpm script for CDS type generation- Test and coverage badges in README
Changed
- Renamed
eslint.config.jstoeslint.config.mjsto eliminate Node.js module type warning - Updated
preparescript to include CDS type generation before build - Enhanced README with test workflow and coverage badges
Fixed
- Type Safety: Replaced all
anytypes with proper TypeScript types across the codebase- Added
BlockfrostAmountinterface for Blockfrost API responses - Changed
Functiontype to proper function signatures in tests - Used
unknowntype for error handling with proper type guards - Used
Servicetype from@sap/cdsfor database operations - Fixed optional parameter handling in
initializeClient
- Added
- Fixed ESLint warnings about unsafe function types
- Fixed TypeScript strict mode compliance in error handling
- Added CDS type generation step to CI workflow
Infrastructure
- GitHub Actions workflow with matrix testing (Node 20.x, 22.x)
- Automated type checking, linting, and testing in CI
- Coverage reporting pipeline
Breaking 1, Removed 2, Changed 3, Fixes 3
Notes
Changed
- Migrated to native CDS logger (
cds.log()) instead of custom logger implementation - Simplified API key configuration:
blockfrostApiKeyin config,BLOCKFROST_KEYenv variable as fallback for development - Updated all documentation (README, QUICKSTART, SETUP, ARCHITECTURE) to reflect new configuration approach
Removed
- Custom
logger.tsutility (replaced by CDS logger) - Unused configuration options:
blockfrostProjectId,batchSize,enableWebhooks,webhookEndpoint
Fixed
- BREAKING: Config key inconsistency - now consistently uses
cds.env.requires.watch(previously mixed usage) - Important: Configuration priority - CDS config always takes precedence over environment variables
- Consistent logger naming: unified to
ODATANO-WATCH
- Migrated to native CDS logger (
Features 19
Notes
Added
- Initial release of @odatano/watch CAP plugin
- Cardano blockchain monitoring via Blockfrost API
- Address monitoring with configurable polling intervals
- Transaction tracking and confirmation detection
- Independent polling paths for addresses (30s) and transactions (60s)
- Event-based architecture with CAP event bus integration
- OData Admin Service for management and monitoring
- Multi-network support (mainnet, preview, preprod)
- TypeScript-first implementation with full type definitions
- Comprehensive error handling and logging
- Complete documentation (README, QUICKSTART, SETUP, ARCHITECTURE)
- Unit and integration tests
- CAP plugin auto-registration
- Environment variable configuration support
Features
- Events:
cardano.newTransactionsandcardano.transactionConfirmed - Admin Actions: Start/stop watcher, add addresses, track transactions
- Data Models: WatchedAddress, TransactionSubmission, BlockchainEvent, Transaction
- API Integration: Blockfrost API client with retry logic
- Configuration: Flexible configuration via package.json or environment variables
Others providing
Compare all 9 on the Async processing page →- ABAP-PRCABAP-PRCA framework for custom multi-step ABAP processes: a process class declares the states, and the framework runs each document through them and monitors the run.independent, abapFramework for multi-step ABAP processes with monitoringfrom ABAP
- Event MeshEvent MeshManaged queues and topics over AMQP, MQTT and a REST gateway, and the bus SAP's own applications publish their business events to.sap official, btpManaged queues and topics over AMQP, MQTT and RESTfrom BTP/BAIP
- com.sap.cds:cds-services-bomcom.sap.cds:cds-services-bomThe Maven bill of materials for CAP Java, pinning every com.sap.cds module in a release, from the API and core runtime to the Spring Boot starters and the CQN engine, to the same version.sap official, capMaven bill of materials pinning CAP Java module versionsfrom CAP
- @cap-js-community/event-queue@cap-js-community/event-queueTransactional processing of asynchronous and periodic events, with instant dispatch over Redis Pub/Sub and load spread across every application instance.community org, capTransactional processing of asynchronous and periodic CAP eventsfrom CAP
- @cap-js-community/sap-afc-sdk@cap-js-community/sap-afc-sdkSDK for building SAP Advanced Financial Closing scheduling providers on CDS.community org, capSDK for SAP Advanced Financial Closing scheduling providers on CDSfrom CAP
- cds-event-monitoringcds-event-monitoringA CAP plugin that stores every event received on its configured messaging topics in a table, with a Fiori-annotated service to browse them, resend events and drain a dead-message queue.independent, capStores, browses and resends messages a CAP service receivesfrom CAP
- @sap/cds@sap/cdsThe CAP Node.js runtime, which compiles the CDS model, serves the services declared in it, answers generic requests before any handler runs, and loads every cds-plugin.sap official, capCAP Node.js runtimefrom CAP
- cds-natscds-natsNATS as a messaging service for the CAP Node.js runtime, the broker behind cds.connect for emit and subscribe, for teams already running NATS rather than Event Mesh or Redis.independent, capNATS messaging service for the CAP Node.js runtimefrom CAP
Details
Sources
Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.
Compatibility and provenance
Contributors
History in the atlas
- First seen.
For your agent
- Ref
artifact:odatano-watch- JSON
- /artifacts/odatano-watch.json
- MCP
- Connecting an agent