diff --git a/apps/ecash-herald/config.js b/apps/ecash-herald/config.js
--- a/apps/ecash-herald/config.js
+++ b/apps/ecash-herald/config.js
@@ -52,6 +52,10 @@
         shrimp: 0,
     },
     emojis: {
+        agora: 'πŸ›',
+        agoraBuy: 'πŸ’°',
+        agoraList: '🏷',
+        agoraCancel: '❌',
         alias: 'πŸ‘Ύ',
         alp: 'πŸ—»',
         invalid: '❌',
diff --git a/apps/ecash-herald/package-lock.json b/apps/ecash-herald/package-lock.json
--- a/apps/ecash-herald/package-lock.json
+++ b/apps/ecash-herald/package-lock.json
@@ -15,6 +15,8 @@
                 "cache-manager": "^5.5.2",
                 "chronik-client": "file:../../modules/chronik-client",
                 "cron": "^3.1.7",
+                "ecash-agora": "file:../../modules/ecash-agora",
+                "ecash-lib": "file:../../modules/ecash-lib",
                 "ecash-script": "file:../../modules/ecash-script",
                 "ecashaddrjs": "file:../../modules/ecashaddrjs",
                 "node-telegram-bot-api": "^0.66.0"
@@ -62,6 +64,56 @@
                 "typescript": "^4.5.2"
             }
         },
+        "../../modules/ecash-agora": {
+            "version": "0.1.1",
+            "license": "MIT",
+            "dependencies": {
+                "chronik-client": "file:../chronik-client",
+                "ecash-lib": "file:../ecash-lib"
+            },
+            "devDependencies": {
+                "@istanbuljs/nyc-config-typescript": "^1.0.2",
+                "@types/chai": "^4.3.14",
+                "@types/chai-as-promised": "^7.1.8",
+                "@types/mocha": "^10.0.6",
+                "@types/node": "^20.12.7",
+                "chai": "^4.4.1",
+                "chai-as-promised": "^7.1.1",
+                "eslint-plugin-header": "^3.1.1",
+                "mocha": "^10.4.0",
+                "mocha-junit-reporter": "^2.2.1",
+                "nyc": "^15.1.0",
+                "source-map-support": "^0.5.21",
+                "ts-node": "^10.9.2",
+                "tsx": "^4.7.2",
+                "typescript": "^5.4.3",
+                "typescript-eslint": "^7.6.0"
+            }
+        },
+        "../../modules/ecash-lib": {
+            "version": "0.2.1",
+            "license": "MIT",
+            "dependencies": {
+                "ecashaddrjs": "file:../ecashaddrjs"
+            },
+            "devDependencies": {
+                "@istanbuljs/nyc-config-typescript": "^1.0.2",
+                "@types/chai": "^4.3.14",
+                "@types/mocha": "^10.0.6",
+                "@types/node": "^20.12.7",
+                "chai": "^5.1.0",
+                "chronik-client": "file:../chronik-client",
+                "eslint-plugin-header": "^3.1.1",
+                "mocha": "^10.4.0",
+                "mocha-junit-reporter": "^2.2.1",
+                "nyc": "^15.1.0",
+                "source-map-support": "^0.5.21",
+                "ts-node": "^10.9.2",
+                "tsx": "^4.7.2",
+                "typescript": "^5.4.3",
+                "typescript-eslint": "^7.6.0"
+            }
+        },
         "../../modules/ecash-script": {
             "version": "2.1.3",
             "license": "MIT",
@@ -1779,6 +1831,14 @@
                 "node": ">=6.0.0"
             }
         },
+        "node_modules/ecash-agora": {
+            "resolved": "../../modules/ecash-agora",
+            "link": true
+        },
+        "node_modules/ecash-lib": {
+            "resolved": "../../modules/ecash-lib",
+            "link": true
+        },
         "node_modules/ecash-script": {
             "resolved": "../../modules/ecash-script",
             "link": true
@@ -6483,6 +6543,50 @@
                 "esutils": "^2.0.2"
             }
         },
+        "ecash-agora": {
+            "version": "file:../../modules/ecash-agora",
+            "requires": {
+                "@istanbuljs/nyc-config-typescript": "^1.0.2",
+                "@types/chai": "^4.3.14",
+                "@types/chai-as-promised": "^7.1.8",
+                "@types/mocha": "^10.0.6",
+                "@types/node": "^20.12.7",
+                "chai": "^4.4.1",
+                "chai-as-promised": "^7.1.1",
+                "chronik-client": "file:../chronik-client",
+                "ecash-lib": "file:../ecash-lib",
+                "eslint-plugin-header": "^3.1.1",
+                "mocha": "^10.4.0",
+                "mocha-junit-reporter": "^2.2.1",
+                "nyc": "^15.1.0",
+                "source-map-support": "^0.5.21",
+                "ts-node": "^10.9.2",
+                "tsx": "^4.7.2",
+                "typescript": "^5.4.3",
+                "typescript-eslint": "^7.6.0"
+            }
+        },
+        "ecash-lib": {
+            "version": "file:../../modules/ecash-lib",
+            "requires": {
+                "@istanbuljs/nyc-config-typescript": "^1.0.2",
+                "@types/chai": "^4.3.14",
+                "@types/mocha": "^10.0.6",
+                "@types/node": "^20.12.7",
+                "chai": "^5.1.0",
+                "chronik-client": "file:../chronik-client",
+                "ecashaddrjs": "file:../ecashaddrjs",
+                "eslint-plugin-header": "^3.1.1",
+                "mocha": "^10.4.0",
+                "mocha-junit-reporter": "^2.2.1",
+                "nyc": "^15.1.0",
+                "source-map-support": "^0.5.21",
+                "ts-node": "^10.9.2",
+                "tsx": "^4.7.2",
+                "typescript": "^5.4.3",
+                "typescript-eslint": "^7.6.0"
+            }
+        },
         "ecash-script": {
             "version": "file:../../modules/ecash-script",
             "requires": {
diff --git a/apps/ecash-herald/package.json b/apps/ecash-herald/package.json
--- a/apps/ecash-herald/package.json
+++ b/apps/ecash-herald/package.json
@@ -28,6 +28,8 @@
         "cache-manager": "^5.5.2",
         "chronik-client": "file:../../modules/chronik-client",
         "cron": "^3.1.7",
+        "ecash-agora": "file:../../modules/ecash-agora",
+        "ecash-lib": "file:../../modules/ecash-lib",
         "ecash-script": "file:../../modules/ecash-script",
         "ecashaddrjs": "file:../../modules/ecashaddrjs",
         "node-telegram-bot-api": "^0.66.0"
diff --git a/apps/ecash-herald/src/parse.js b/apps/ecash-herald/src/parse.js
--- a/apps/ecash-herald/src/parse.js
+++ b/apps/ecash-herald/src/parse.js
@@ -27,6 +27,8 @@
     containsOnlyPrintableAscii,
 } = require('./utils');
 const lokadMap = require('../constants/lokad');
+const { scriptOps } = require('ecash-agora');
+const { Script, fromHex, OP_0 } = require('ecash-lib');
 
 // Constants for SLP 1 token types as returned by chronik-client
 const SLP_1_PROTOCOL_NUMBER = 1;
@@ -2192,13 +2194,17 @@
         let appTxs = 0;
         let unknownLokadTxs = 0;
 
-        // tokenId => {info, listings, adPreps, sends, burns, mints, genesis: {genesisQty: <>, hasBaton: <>}}
+        // tokenId => {info, list, cancel, buy, adPrep, send, burn, mint, genesis: {genesisQty: <>, hasBaton: <>}}
         const tokenActions = new Map();
         let invalidTokenEntries = 0;
         let nftTokenEntries = 0;
         let mintVaultTokenEntries = 0;
         let alpTokenEntries = 0;
 
+        // Agora vars
+        let agoraTxs = 0;
+        const agoraActions = new Map();
+
         for (const tx of txs) {
             const { inputs, outputs, block, tokenEntries, isCoinbase } = tx;
 
@@ -2409,33 +2415,262 @@
                             }
                             case 'SEND': {
                                 // SEND may be Agora or Burn
-                                const existingActions =
+                                const existingTokenActions =
                                     tokenActions.get(tokenId);
+                                const existingAgoraActions =
+                                    agoraActions.get(tokenId);
+
+                                // For now, we assume that any p2sh token input is agora buy/cancel
+                                // and any p2sh token output is an ad setup tx
+                                // No other known cases of p2sh for token txs on ecash today
+                                // tho multisig is possible, no supporting wallets
+
+                                // mb parse for ad setup first, which is p2sh output?
+
+                                let isAgoraBuySellList = false;
+                                for (const input of inputs) {
+                                    if (typeof input.token !== 'undefined') {
+                                        const { outputScript, inputScript } =
+                                            input;
+                                        // A token input that is p2sh may be
+                                        // a listing, an ad setup, a buy, or a cancel
+                                        try {
+                                            const { type } =
+                                                cashaddr.getTypeAndHashFromOutputScript(
+                                                    outputScript,
+                                                );
+                                            if (type === 'p2sh') {
+                                                // We are only parsing SLP agora txs here
+                                                // Such txs will have
+                                                // A listing will have AGR0 lokad in input script
+                                                const AGORA_LOKAD_STARTSWITH =
+                                                    '0441475230';
+
+                                                if (
+                                                    inputScript.startsWith(
+                                                        AGORA_LOKAD_STARTSWITH,
+                                                    )
+                                                ) {
+                                                    // Agora tx
+
+                                                    // For now, we know all listing txs only have a single p2sh input
+
+                                                    if (inputs.length === 1) {
+                                                        // Agora listing
+                                                        agoraActions.set(
+                                                            tokenId,
+                                                            typeof existingAgoraActions ===
+                                                                'undefined'
+                                                                ? {
+                                                                      list: {
+                                                                          count: 1,
+                                                                      },
+                                                                      actionCount: 1,
+                                                                  }
+                                                                : {
+                                                                      ...existingAgoraActions,
+                                                                      list: {
+                                                                          count:
+                                                                              'list' in
+                                                                              existingAgoraActions
+                                                                                  ? existingAgoraActions
+                                                                                        .list
+                                                                                        .count +
+                                                                                    1
+                                                                                  : 1,
+                                                                      },
+                                                                      actionCount:
+                                                                          existingAgoraActions.actionCount +
+                                                                          1,
+                                                                  },
+                                                        );
+                                                        isAgoraBuySellList = true;
+                                                        // Stop processing inputs for this tx
+                                                        break;
+                                                    }
+                                                    // Check if this is a cancellation
+                                                    // See agora.ts from ecash-agora lib
+                                                    // For now, I don't think it makes sense to have an 'isCanceled' method from ecash-agora
+                                                    // This is a pretty specific application
+                                                    const ops = scriptOps(
+                                                        new Script(
+                                                            fromHex(
+                                                                inputScript,
+                                                            ),
+                                                        ),
+                                                    );
+                                                    // isCanceled is always the last pushop (before redeemScript)
+                                                    const opIsCanceled =
+                                                        ops[ops.length - 2];
+
+                                                    const isCanceled =
+                                                        opIsCanceled === OP_0;
+
+                                                    if (isCanceled) {
+                                                        // Agora cancel
+                                                        agoraActions.set(
+                                                            tokenId,
+                                                            typeof existingAgoraActions ===
+                                                                'undefined'
+                                                                ? {
+                                                                      cancel: {
+                                                                          count: 1,
+                                                                      },
+                                                                      actionCount: 1,
+                                                                  }
+                                                                : {
+                                                                      ...existingAgoraActions,
+                                                                      cancel: {
+                                                                          count:
+                                                                              'cancel' in
+                                                                              existingAgoraActions
+                                                                                  ? existingAgoraActions
+                                                                                        .cancel
+                                                                                        .count +
+                                                                                    1
+                                                                                  : 1,
+                                                                      },
+                                                                      actionCount:
+                                                                          existingAgoraActions.actionCount +
+                                                                          1,
+                                                                  },
+                                                        );
+                                                        isAgoraBuySellList = true;
+                                                        // Stop processing inputs for this tx
+                                                        break;
+                                                    } else {
+                                                        // Agora purchase
+                                                        agoraActions.set(
+                                                            tokenId,
+                                                            typeof existingAgoraActions ===
+                                                                'undefined'
+                                                                ? {
+                                                                      buy: {
+                                                                          count: 1,
+                                                                      },
+                                                                      actionCount: 1,
+                                                                  }
+                                                                : {
+                                                                      ...existingAgoraActions,
+                                                                      buy: {
+                                                                          count:
+                                                                              'buy' in
+                                                                              existingAgoraActions
+                                                                                  ? existingAgoraActions
+                                                                                        .buy
+                                                                                        .count +
+                                                                                    1
+                                                                                  : 1,
+                                                                      },
+                                                                      actionCount:
+                                                                          existingAgoraActions.actionCount +
+                                                                          1,
+                                                                  },
+                                                        );
+                                                        isAgoraBuySellList = true;
+                                                        // Stop processing inputs for this tx
+                                                        break;
+                                                    }
+                                                }
+                                            }
+                                        } catch (err) {
+                                            console.error(
+                                                `Error in cashaddr.getTypeAndHashFromOutputScript(${outputScript}) from txid ${tx.txid}`,
+                                            );
+                                            // Do not parse it as an agora tx
+                                        }
+                                        // We don't need to find any other inputs for this case
+                                        break;
+                                    }
+                                }
+                                if (isAgoraBuySellList) {
+                                    agoraTxs += 1;
+                                    // We have already processed this token tx
+                                    continue;
+                                }
 
-                                // TODO parse agora
+                                // Check for ad prep tx
+                                let isAdPrep = false;
+                                for (const output of outputs) {
+                                    if (typeof output.token !== 'undefined') {
+                                        const { outputScript } = output;
+                                        // We assume a p2sh token output is an ad setup tx
+                                        // No other known use cases at the moment
+                                        try {
+                                            const { type } =
+                                                cashaddr.getTypeAndHashFromOutputScript(
+                                                    outputScript,
+                                                );
+                                            if (type === 'p2sh') {
+                                                // Agora ad setup tx for SLP1
+                                                agoraActions.set(
+                                                    tokenId,
+                                                    typeof existingAgoraActions ===
+                                                        'undefined'
+                                                        ? {
+                                                              adPrep: {
+                                                                  count: 1,
+                                                              },
+                                                              actionCount: 1,
+                                                          }
+                                                        : {
+                                                              ...existingAgoraActions,
+                                                              adPrep: {
+                                                                  count:
+                                                                      'adPrep' in
+                                                                      existingAgoraActions
+                                                                          ? existingAgoraActions
+                                                                                .adPrep
+                                                                                .count +
+                                                                            1
+                                                                          : 1,
+                                                              },
+                                                              actionCount:
+                                                                  existingAgoraActions.actionCount +
+                                                                  1,
+                                                          },
+                                                );
+                                                break;
+                                                // Stop iterating over outputs
+                                            }
+                                        } catch (err) {
+                                            console.error(
+                                                `Error in cashaddr.getTypeAndHashFromOutputScript(${outputScript}) for output from txid ${tx.txid}`,
+                                            );
+                                            // Do not parse it as an agora tx
+                                        }
+                                    }
+                                }
+                                if (isAdPrep) {
+                                    agoraTxs += 1;
+                                    // We have processed this tx as an Agora Ad setup tx
+                                    // No further processing
+                                    continue;
+                                }
 
                                 // Parse as burn
                                 if (actualBurnAmount !== '0') {
                                     tokenActions.set(
                                         tokenId,
-                                        typeof existingActions === 'undefined'
+                                        typeof existingTokenActions ===
+                                            'undefined'
                                             ? {
                                                   burn: { count: 1 },
                                                   actionCount: 1,
                                               }
                                             : {
-                                                  ...existingActions,
+                                                  ...existingTokenActions,
                                                   burn: {
                                                       count:
                                                           'burn' in
-                                                          existingActions
-                                                              ? existingActions
+                                                          existingTokenActions
+                                                              ? existingTokenActions
                                                                     .burn
                                                                     .count + 1
                                                               : 1,
                                                   },
                                                   actionCount:
-                                                      existingActions.actionCount +
+                                                      existingTokenActions.actionCount +
                                                       1,
                                               },
                                     );
@@ -2446,19 +2681,20 @@
                                 // Parse as send
                                 tokenActions.set(
                                     tokenId,
-                                    typeof existingActions === 'undefined'
+                                    typeof existingTokenActions === 'undefined'
                                         ? { send: { count: 1 }, actionCount: 1 }
                                         : {
-                                              ...existingActions,
+                                              ...existingTokenActions,
                                               send: {
                                                   count:
-                                                      'send' in existingActions
-                                                          ? existingActions.send
-                                                                .count + 1
+                                                      'send' in
+                                                      existingTokenActions
+                                                          ? existingTokenActions
+                                                                .send.count + 1
                                                           : 1,
                                               },
                                               actionCount:
-                                                  existingActions.actionCount +
+                                                  existingTokenActions.actionCount +
                                                   1,
                                           },
                                 );
@@ -2732,6 +2968,111 @@
             tgMsg.push('');
         }
 
+        // Agora summary
+        if (agoraTxs > 0) {
+            // Zero out counters for sorting purposes
+            agoraActions.forEach((agoraActionInfo, tokenId) => {
+                // Note we do not check adPrep as any token with adPrep has listing
+                const { buy, list, cancel } = agoraActionInfo;
+
+                if (typeof buy === 'undefined') {
+                    agoraActionInfo.buy = { count: 0 };
+                }
+                if (typeof list === 'undefined') {
+                    agoraActionInfo.list = { count: 0 };
+                }
+                if (typeof cancel === 'undefined') {
+                    agoraActionInfo.cancel = { count: 0 };
+                }
+                agoraActions.set(tokenId, agoraActionInfo);
+            });
+
+            // Sort agoraActions by buys
+            const sortedAgoraActions = new Map(
+                [...agoraActions.entries()].sort(
+                    (keyValueArrayA, keyValueArrayB) =>
+                        keyValueArrayB[1].buy.count -
+                        keyValueArrayA[1].buy.count,
+                ),
+            );
+
+            const agoraTokens = Array.from(sortedAgoraActions.keys());
+            const agoraTokenCount = agoraTokens.length;
+
+            tgMsg.push(
+                `${config.emojis.agora} <b><i>${agoraTxs.toLocaleString(
+                    'en-US',
+                )} Agora tx${
+                    agoraTxs > 1 ? 's' : ''
+                } from ${agoraTokenCount} token${
+                    agoraTokenCount > 1 ? 's' : ''
+                }</i></b>`,
+            );
+
+            const AGORA_TOKENS_TO_SHOW = 10;
+
+            // Handle case where we do not see as many agora tokens as our max
+            const agoraTokensToShow =
+                agoraTokenCount < AGORA_TOKENS_TO_SHOW
+                    ? agoraTokenCount
+                    : AGORA_TOKENS_TO_SHOW;
+            const newsworthyAgoraTokens = agoraTokens.slice(
+                0,
+                agoraTokensToShow,
+            );
+
+            if (agoraTokenCount > AGORA_TOKENS_TO_SHOW) {
+                tgMsg.push(`<u>Top ${AGORA_TOKENS_TO_SHOW}</u>`);
+            }
+
+            // Emoji key
+            tgMsg.push(
+                `${config.emojis.agoraBuy}Buy, ${config.emojis.agoraList}List, ${config.emojis.agoraCancel}Cancel`,
+            );
+
+            for (let i = 0; i < newsworthyAgoraTokens.length; i += 1) {
+                const tokenId = newsworthyAgoraTokens[i];
+                const tokenActionInfo = sortedAgoraActions.get(tokenId);
+                const genesisInfo = tokenInfoMap.get(tokenId);
+
+                const { buy, list, cancel } = tokenActionInfo;
+
+                tgMsg.push(
+                    `<a href="${config.blockExplorer}/tx/${tokenId}">${
+                        typeof genesisInfo === 'undefined'
+                            ? `${tokenId.slice(0, 3)}...${tokenId.slice(-3)}`
+                            : genesisInfo.tokenName
+                    }</a>${
+                        typeof genesisInfo === 'undefined'
+                            ? ''
+                            : genesisInfo.tokenTicker !== ''
+                            ? ` (${genesisInfo.tokenTicker})`
+                            : ''
+                    }: ${
+                        buy.count > 0
+                            ? `${config.emojis.agoraBuy}${
+                                  buy.count > 1 ? `x${buy.count}` : ''
+                              }`
+                            : ''
+                    }${
+                        list.count > 0
+                            ? `${config.emojis.agoraList}${
+                                  list.count > 1 ? `x${list.count}` : ''
+                              }`
+                            : ''
+                    }${
+                        cancel.count > 0
+                            ? `${config.emojis.agoraCancel}${
+                                  cancel.count > 1 ? `x${cancel.count}` : ''
+                              }`
+                            : ''
+                    }`,
+                );
+            }
+            // Newline after agora section
+            tgMsg.push('');
+        }
+
         // Token summary
         if (tokenTxs > 0) {
             // Sort tokenActions map by number of token actions
@@ -2742,12 +3083,39 @@
                         keyValueArrayA[1].actionCount,
                 ),
             );
+
+            // Note we may have some agora tokens here, which is fine
+            // If agora tokens are also the top for other actions, want to demonstrate that
+            const nonAgoraTokens = Array.from(sortedTokenActions.keys());
+
+            const nonAgoraTokenCount = nonAgoraTokens.length;
             tgMsg.push(
                 `${config.emojis.token} <b><i>${tokenTxs.toLocaleString(
                     'en-US',
-                )} token tx${tokenTxs > 1 ? 's' : ''}</i></b>`,
+                )} token tx${
+                    tokenTxs > 1 ? 's' : ''
+                } from ${nonAgoraTokenCount} token${
+                    nonAgoraTokenCount > 1 ? 's' : ''
+                }</i></b>`,
+            );
+
+            const NON_AGORA_TOKENS_TO_SHOW = 5;
+            const nonAgoraTokensToShow =
+                nonAgoraTokenCount < NON_AGORA_TOKENS_TO_SHOW
+                    ? nonAgoraTokenCount
+                    : NON_AGORA_TOKENS_TO_SHOW;
+            const newsworthyTokens = nonAgoraTokens.slice(
+                0,
+                nonAgoraTokensToShow,
             );
-            sortedTokenActions.forEach((tokenActionInfo, tokenId) => {
+
+            if (nonAgoraTokenCount > NON_AGORA_TOKENS_TO_SHOW) {
+                tgMsg.push(`<u>Top ${NON_AGORA_TOKENS_TO_SHOW}</u>`);
+            }
+
+            for (let i = 0; i < newsworthyTokens.length; i += 1) {
+                const tokenId = newsworthyTokens[i];
+                const tokenActionInfo = sortedTokenActions.get(tokenId);
                 const genesisInfo = tokenInfoMap.get(tokenId);
 
                 const { send, genesis, burn, mint } = tokenActionInfo;
@@ -2787,7 +3155,7 @@
                             : ''
                     }`,
                 );
-            });
+            }
             if (alpTokenEntries > 0) {
                 tgMsg.push(
                     `${config.emojis.alp} <b>${alpTokenEntries.toLocaleString(
diff --git a/apps/ecash-herald/test/mocks/dailyTxs.js b/apps/ecash-herald/test/mocks/dailyTxs.js
--- a/apps/ecash-herald/test/mocks/dailyTxs.js
+++ b/apps/ecash-herald/test/mocks/dailyTxs.js
@@ -326,6 +326,313 @@
             timestamp: 1728988376,
         },
     },
+    // Agora txs
+    // SLP1 partial list
+    // 20469a4316506e0fea99ad0673d6663f2f546c0aad84b741e08c4d0f9248b18c
+    {
+        txid: '20469a4316506e0fea99ad0673d6663f2f546c0aad84b741e08c4d0f9248b18c',
+        version: 2,
+        inputs: [
+            {
+                prevOut: {
+                    txid: 'd84e49d7396553f8931e7ecebf1717ced0ba962d3f4be7ce672d418a9f3a107d',
+                    outIdx: 1,
+                },
+                inputScript:
+                    '0441475230075041525449414c4180f1bfdb06735c27ffe75627fa1fecd46844334f6686dd5e64b01d4e68de28d98b6ed87ac5bf86ce27af78b06944beae25e2f0084433d563d6bd19c6616a5a62414c8c4c766a04534c500001010453454e442001d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f89608000000000000000000013b62100000000000298f0000000000006de4ff1700000000f3282c4e03771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba601557f77ad075041525449414c88044147523087',
+                value: 914,
+                sequenceNo: 4294967295,
+                token: {
+                    tokenId:
+                        '01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896',
+                    tokenType: {
+                        protocol: 'SLP',
+                        type: 'SLP_TOKEN_TYPE_FUNGIBLE',
+                        number: 1,
+                    },
+                    amount: '2000',
+                    isMintBaton: false,
+                    entryIdx: 0,
+                },
+                outputScript: 'a9142ee1060eafaafbad92d2d5420120d40c0394a84e87',
+            },
+        ],
+        outputs: [
+            {
+                value: 0,
+                outputScript:
+                    '6a04534c500001010453454e442001d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f8960800000000000007d0',
+            },
+            {
+                value: 546,
+                outputScript: 'a914563178ea073228709397a2c98baf10677e683e6687',
+                token: {
+                    tokenId:
+                        '01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896',
+                    tokenType: {
+                        protocol: 'SLP',
+                        type: 'SLP_TOKEN_TYPE_FUNGIBLE',
+                        number: 1,
+                    },
+                    amount: '2000',
+                    isMintBaton: false,
+                    entryIdx: 0,
+                },
+            },
+        ],
+        lockTime: 0,
+        timeFirstSeen: 1729929435,
+        size: 368,
+        isCoinbase: false,
+        tokenEntries: [
+            {
+                tokenId:
+                    '01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896',
+                tokenType: {
+                    protocol: 'SLP',
+                    type: 'SLP_TOKEN_TYPE_FUNGIBLE',
+                    number: 1,
+                },
+                txType: 'SEND',
+                isInvalid: false,
+                burnSummary: '',
+                failedColorings: [],
+                actualBurnAmount: '0',
+                intentionalBurn: '0',
+                burnsMintBatons: false,
+            },
+        ],
+        tokenFailedParsings: [],
+        tokenStatus: 'TOKEN_STATUS_NORMAL',
+        block: {
+            height: 868211,
+            hash: '0000000000000000297b2e66c61c87a7b77c9f49c1033e0db146272ea150d2cc',
+            timestamp: 1729931891,
+        },
+    },
+    // SLP1 partial buy
+    // f4b2bd7fc77975103223f41f588751eedf16cfb4ee8dd44ebcb44191fd0d2eff
+    {
+        txid: 'f4b2bd7fc77975103223f41f588751eedf16cfb4ee8dd44ebcb44191fd0d2eff',
+        version: 2,
+        inputs: [
+            {
+                prevOut: {
+                    txid: '4822a0ccc510ddb0ce3ba423e3de49c12993a607c4c108345be9f6eef84767f5',
+                    outIdx: 2,
+                },
+                inputScript:
+                    '0441475230075041525449414c21023c72addb4fdf09af94f0c94d7fe92a386a7e70cf8a1d85916386bb2535c7b1b1407996448a8c0b89e341453ba9726eb40a2e8c07401808b82dc3623a2ab2c353c9115cbdbcd738b01d01a718c9c10336823231f7f16cdcc3ac43001c4c0c11e3764422020000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac89680000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac4d2c01f56747f8eef6e95b3408c1c407a69329c149dee323a43bceb0dd10c5cca0224802000000d37b63817b6ea2697604d0aa4701a2697602e2539700887d94527901377f75789263587e7802e253965880bc007e7e68587e527902e253965880bc007e7e825980bc7c7e007e7b02e1539302e2539658807e041976a914707501557f77a97e0288ac7e7e6b7d02220258800317a9147e024c7672587d807e7e7e01ab7e537901257f7702d3007f5c7f7701207f547f750440aef137886b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c677501557f7768ad075041525449414c880441475230872202000000000000ffffffffdc591bfcfc4bbdd22709d6be93a5c9f25c9be52771a079ed51a1bd8767c4fa5d40aef137c100000004d0aa4701514d55014c766a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb10800000000000000000000e253000000000000e253000000000000d0aa47010000000040aef13703771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba60860b8507800000000ab7b63817b6ea2697604d0aa4701a2697602e2539700887d94527901377f75789263587e7802e253965880bc007e7e68587e527902e253965880bc007e7e825980bc7c7e007e7b02e1539302e2539658807e041976a914707501557f77a97e0288ac7e7e6b7d02220258800317a9147e024c7672587d807e7e7e01ab7e537901257f7702d3007f5c7f7701207f547f750440aef137886b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c677501557f7768ad075041525449414c88044147523087',
+                value: 546,
+                sequenceNo: 4294967295,
+                token: {
+                    tokenId:
+                        'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1',
+                    tokenType: {
+                        protocol: 'SLP',
+                        type: 'SLP_TOKEN_TYPE_FUNGIBLE',
+                        number: 1,
+                    },
+                    amount: '94000',
+                    isMintBaton: false,
+                    entryIdx: 0,
+                },
+                outputScript: 'a914bdda81ba1d3bf0598d24d77b461bafbed0ba7af987',
+            },
+            {
+                prevOut: {
+                    txid: '4822a0ccc510ddb0ce3ba423e3de49c12993a607c4c108345be9f6eef84767f5',
+                    outIdx: 4,
+                },
+                inputScript:
+                    '4171aa357cf2a1e41d819440432b05557bd24da15319e714e6084330496e63f4cce9d8b3e77c86b9ae4a1ebd078a6ed8cbef34347b2d5a99643443c44d350e2ff0412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795',
+                value: 30808,
+                sequenceNo: 4294967295,
+                outputScript:
+                    '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac',
+            },
+        ],
+        outputs: [
+            {
+                value: 0,
+                outputScript:
+                    '6a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1080000000000000000080000000000016b480800000000000003e8',
+            },
+            {
+                value: 1000,
+                outputScript:
+                    '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac',
+            },
+            {
+                value: 546,
+                outputScript: 'a914366be7e1eee2040519012d19fbfc3002456aede487',
+                token: {
+                    tokenId:
+                        'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1',
+                    tokenType: {
+                        protocol: 'SLP',
+                        type: 'SLP_TOKEN_TYPE_FUNGIBLE',
+                        number: 1,
+                    },
+                    amount: '93000',
+                    isMintBaton: false,
+                    entryIdx: 0,
+                },
+            },
+            {
+                value: 546,
+                outputScript:
+                    '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac',
+                token: {
+                    tokenId:
+                        'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1',
+                    tokenType: {
+                        protocol: 'SLP',
+                        type: 'SLP_TOKEN_TYPE_FUNGIBLE',
+                        number: 1,
+                    },
+                    amount: '1000',
+                    isMintBaton: false,
+                    entryIdx: 0,
+                },
+            },
+            {
+                value: 26761,
+                outputScript:
+                    '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac',
+            },
+        ],
+        lockTime: 938585664,
+        timeFirstSeen: 1729985270,
+        size: 1244,
+        isCoinbase: false,
+        tokenEntries: [
+            {
+                tokenId:
+                    'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1',
+                tokenType: {
+                    protocol: 'SLP',
+                    type: 'SLP_TOKEN_TYPE_FUNGIBLE',
+                    number: 1,
+                },
+                txType: 'SEND',
+                isInvalid: false,
+                burnSummary: '',
+                failedColorings: [],
+                actualBurnAmount: '0',
+                intentionalBurn: '0',
+                burnsMintBatons: false,
+            },
+        ],
+        tokenFailedParsings: [],
+        tokenStatus: 'TOKEN_STATUS_NORMAL',
+        block: {
+            height: 868307,
+            hash: '000000000000000010c9f9bdb9446aac244e19ad0ae3936d73507964ed544e36',
+            timestamp: 1729986430,
+        },
+    },
+    // SLP1 partial cancel
+    // e9d594e054bf9a7cead11cdc31953f0e45782c97c6298513f41b70eb408aa1a8
+    {
+        txid: 'e9d594e054bf9a7cead11cdc31953f0e45782c97c6298513f41b70eb408aa1a8',
+        version: 2,
+        inputs: [
+            {
+                prevOut: {
+                    txid: '58ec58688cef1d0abe2ee30c15f84af51833e61e998841fac3ecbcadafc31233',
+                    outIdx: 2,
+                },
+                inputScript:
+                    '41fd18138ab17386e9599e54d9d5f1994d1c4add3af860b1ece44b71d04bc7e7cd799e1234e2959236cd38558713d7fdb797a894c527906b0235a38519ad63fbea4121024f624d04900c2e3b7ea6014cb257f525b6d229db274bceeadbb1f06c07776e82',
+                value: 975251,
+                sequenceNo: 4294967295,
+                outputScript:
+                    '76a9147847fe7070bec8567b3e810f543f2f80cc3e03be88ac',
+            },
+            {
+                prevOut: {
+                    txid: '0c580a7dbfb7f160f0e4623faa24eb0475b2220704c8c46f279a479a477433f8',
+                    outIdx: 1,
+                },
+                inputScript:
+                    '0441475230075041525449414c4113bb98283dc7a2f69957940bb3a45f4ec6050b61bcc1b1134d786727e379c8793107bf0d0b0e051665ab3eed2cca34901646cf564a1ab52cb32668da229eef0b41004d5f014c766a04534c500001010453454e442020a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8080000000000000000030276a4000000000000e815000000000000a24a2600000000004b4a343a024f624d04900c2e3b7ea6014cb257f525b6d229db274bceeadbb1f06c07776e8208948eff7f00000000ab7b63817b6ea2697603a24a26a269760376a4009700887d94527901377f75789263587e780376a400965580bc030000007e7e68587e52790376a400965580bc030000007e7e825980bc7c7e0200007e7b02e7159302e8159656807e041976a914707501557f77a97e0288ac7e7e6b7d02220258800317a9147e024c7672587d807e7e7e01ab7e537901257f7702dd007f5c7f7701207f547f75044b4a343a886b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c677501557f7768ad075041525449414c88044147523087',
+                value: 546,
+                sequenceNo: 4294967295,
+                token: {
+                    tokenId:
+                        '20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8',
+                    tokenType: {
+                        protocol: 'SLP',
+                        type: 'SLP_TOKEN_TYPE_FUNGIBLE',
+                        number: 1,
+                    },
+                    amount: '855738679296',
+                    isMintBaton: false,
+                    entryIdx: 0,
+                },
+                outputScript: 'a914cb61d733f8e99b1b40d40a53a59aca8a08368a6f87',
+            },
+        ],
+        outputs: [
+            {
+                value: 0,
+                outputScript:
+                    '6a04534c500001010453454e442020a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f808000000c73e000000',
+            },
+            {
+                value: 546,
+                outputScript:
+                    '76a9147847fe7070bec8567b3e810f543f2f80cc3e03be88ac',
+                token: {
+                    tokenId:
+                        '20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8',
+                    tokenType: {
+                        protocol: 'SLP',
+                        type: 'SLP_TOKEN_TYPE_FUNGIBLE',
+                        number: 1,
+                    },
+                    amount: '855738679296',
+                    isMintBaton: false,
+                    entryIdx: 0,
+                },
+            },
+            {
+                value: 973723,
+                outputScript:
+                    '76a9147847fe7070bec8567b3e810f543f2f80cc3e03be88ac',
+            },
+        ],
+        lockTime: 0,
+        timeFirstSeen: 1729789538,
+        size: 760,
+        isCoinbase: false,
+        tokenEntries: [
+            {
+                tokenId:
+                    '20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8',
+                tokenType: {
+                    protocol: 'SLP',
+                    type: 'SLP_TOKEN_TYPE_FUNGIBLE',
+                    number: 1,
+                },
+                txType: 'SEND',
+                isInvalid: false,
+                burnSummary: '',
+                failedColorings: [],
+                actualBurnAmount: '0',
+                intentionalBurn: '0',
+                burnsMintBatons: false,
+            },
+        ],
+        tokenFailedParsings: [],
+        tokenStatus: 'TOKEN_STATUS_NORMAL',
+        block: {
+            height: 867971,
+            hash: '000000000000000013f3d459ae121dc1494e7e9fe57c2e60cf393184d7ab6dc9',
+            timestamp: 1729793460,
+        },
+    },
     // Token txs
     // SLP1 fungible txs
     // Genesis tx
@@ -3090,6 +3397,16 @@
             hash: '',
         },
     ],
+    [
+        '01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896',
+        {
+            tokenTicker: 'BULL',
+            tokenName: 'Bull',
+            url: 'https://cashtab.com/',
+            decimals: 0,
+            hash: '',
+        },
+    ],
 ]);
 
 module.exports = { dailyTxs, tokenInfoMap };
diff --git a/apps/ecash-herald/test/parse.test.js b/apps/ecash-herald/test/parse.test.js
--- a/apps/ecash-herald/test/parse.test.js
+++ b/apps/ecash-herald/test/parse.test.js
@@ -365,8 +365,8 @@
             ),
             [
                 '<b>15 Oct 2024</b>\n' +
-                    'πŸ“¦56,311 blocks\n' +
-                    '➑️23 txs\n' +
+                    'πŸ“¦56,900 blocks\n' +
+                    '➑️26 txs\n' +
                     '\n' +
                     'πŸ“‰<b>1 XEC = $0.00003487</b> <i>(-0.40%)</i>\n' +
                     'Trading volume: $5,957,333\n' +
@@ -388,7 +388,13 @@
                     '🎁 <b>1</b> new user received <b>42 XEC</b>\n' +
                     '🎟 <b>1</b> <a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">CACHET</a> reward\n' +
                     '\n' +
-                    'πŸͺ™ <b><i>9 token txs</i></b>\n' +
+                    'πŸ› <b><i>3 Agora txs from 3 tokens</i></b>\n' +
+                    'πŸ’°Buy, 🏷List, ❌Cancel\n' +
+                    '<a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">Cachet</a> (CACHET): πŸ’°\n' +
+                    '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">Vespene Gas</a> (VSP): ❌\n' +
+                    '<a href="https://explorer.e.cash/tx/01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896">Bull</a> (BULL): 🏷\n' +
+                    '\n' +
+                    'πŸͺ™ <b><i>12 token txs from 2 tokens</i></b>\n' +
                     '<a href="https://explorer.e.cash/tx/04009a8be347f21a1122964c3226b99c36a9bd755c5a450a53848471a2466103">Perpetua</a> (PRP): πŸ§ͺ➑️πŸ”₯πŸ”¨\n' +
                     '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">Vespene Gas</a> (VSP): ➑️\n' +
                     'πŸ—» <b>1</b> ALP tx\n' +
@@ -428,8 +434,8 @@
             ),
             [
                 '<b>15 Oct 2024</b>\n' +
-                    'πŸ“¦56,311 blocks\n' +
-                    '➑️23 txs\n' +
+                    'πŸ“¦56,900 blocks\n' +
+                    '➑️26 txs\n' +
                     '\n' +
                     'πŸ“‰<b>1 XEC = $0.00003487</b> <i>(-0.40%)</i>\n' +
                     'Trading volume: $5,957,333\n' +
@@ -451,7 +457,13 @@
                     '🎁 <b>1</b> new user received <b>42 XEC</b>\n' +
                     '🎟 <b>1</b> <a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">CACHET</a> reward\n' +
                     '\n' +
-                    'πŸͺ™ <b><i>9 token txs</i></b>\n' +
+                    'πŸ› <b><i>3 Agora txs from 3 tokens</i></b>\n' +
+                    'πŸ’°Buy, 🏷List, ❌Cancel\n' +
+                    '<a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">aed...cb1</a>: πŸ’°\n' +
+                    '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">20a...6f8</a>: ❌\n' +
+                    '<a href="https://explorer.e.cash/tx/01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896">01d...896</a>: 🏷\n' +
+                    '\n' +
+                    'πŸͺ™ <b><i>12 token txs from 2 tokens</i></b>\n' +
                     '<a href="https://explorer.e.cash/tx/04009a8be347f21a1122964c3226b99c36a9bd755c5a450a53848471a2466103">040...103</a>: πŸ§ͺ➑️πŸ”₯πŸ”¨\n' +
                     '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">20a...6f8</a>: ➑️\n' +
                     'πŸ—» <b>1</b> ALP tx\n' +
@@ -484,8 +496,8 @@
             ),
             [
                 '<b>15 Oct 2024</b>\n' +
-                    'πŸ“¦56,311 blocks\n' +
-                    '➑️23 txs\n' +
+                    'πŸ“¦56,900 blocks\n' +
+                    '➑️26 txs\n' +
                     '\n' +
                     '<b><i>⛏️3 miners found blocks</i></b>\n' +
                     '<u>Top 3</u>\n' +
@@ -503,7 +515,13 @@
                     '🎁 <b>1</b> new user received <b>42 XEC</b>\n' +
                     '🎟 <b>1</b> <a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">CACHET</a> reward\n' +
                     '\n' +
-                    'πŸͺ™ <b><i>9 token txs</i></b>\n' +
+                    'πŸ› <b><i>3 Agora txs from 3 tokens</i></b>\n' +
+                    'πŸ’°Buy, 🏷List, ❌Cancel\n' +
+                    '<a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">Cachet</a> (CACHET): πŸ’°\n' +
+                    '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">Vespene Gas</a> (VSP): ❌\n' +
+                    '<a href="https://explorer.e.cash/tx/01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896">Bull</a> (BULL): 🏷\n' +
+                    '\n' +
+                    'πŸͺ™ <b><i>12 token txs from 2 tokens</i></b>\n' +
                     '<a href="https://explorer.e.cash/tx/04009a8be347f21a1122964c3226b99c36a9bd755c5a450a53848471a2466103">Perpetua</a> (PRP): πŸ§ͺ➑️πŸ”₯πŸ”¨\n' +
                     '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">Vespene Gas</a> (VSP): ➑️\n' +
                     'πŸ—» <b>1</b> ALP tx\n' +
diff --git a/contrib/teamcity/build-configurations.yml b/contrib/teamcity/build-configurations.yml
--- a/contrib/teamcity/build-configurations.yml
+++ b/contrib/teamcity/build-configurations.yml
@@ -60,6 +60,8 @@
   #    to be built, otherwise unset
   #  - DEPENDS_ECASH_LIB: "true" if these tests require ecash-lib to be
   #    built, otherwise unset
+  #  - DEPENDS_ECASH_AGORA: "true" if these tests require ecash-agora to be built,
+  #    otherwise unset
   #  - DEPENDS_CHRONIK_CLIENT: "true" if these tests require chronik-client
   #    to be build, otherwise unset
   js-mocha:
@@ -121,6 +123,15 @@
         npm run build
       fi
 
+      if [ -z "${DEPENDS_ECASH_AGORA+x}" ] ; then
+        echo "Test does not depend on ecash-agora"
+      else
+        echo "Test depends on ecash-agora. Building TypeScript..."
+        pushd "${TOPLEVEL}/modules/ecash-agora"
+        npm ci
+        npm run build
+      fi
+
       # Install ecash-script dependencies if this test uses them
       if [ -z "${DEPENDS_ECASH_SCRIPT+x}" ] ; then
         echo "Test does not depend on ecash-script, skipping ecash-script dependencies..."
@@ -836,6 +847,9 @@
       DEPENDS_CHRONIK_CLIENT: "true"
       DEPENDS_ECASH_SCRIPT: "true"
       DEPENDS_ECASHADDRJS: "true"
+      DEPENDS_ECASH_LIB_WASM: "true"
+      DEPENDS_ECASH_LIB: "true"
+      DEPENDS_ECASH_AGORA: "true"
     templates:
       - js-mocha
 
diff --git a/ecash-herald.Dockerfile b/ecash-herald.Dockerfile
--- a/ecash-herald.Dockerfile
+++ b/ecash-herald.Dockerfile
@@ -2,10 +2,52 @@
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
+# Multi-stage
+# 1) rust image for ecash-lib
+# 2) Node image for prod deployment of ecash-lib
+
+# 1) rust image for ecash-lib
+FROM rust:1.76.0 AS wasmbuilder
+
+RUN apt-get update \
+  && apt-get install clang binaryen -y \
+  && rustup target add wasm32-unknown-unknown \
+  && cargo install -f wasm-bindgen-cli@0.2.92
+
+# Copy Cargo.toml
+WORKDIR /app/
+COPY Cargo.toml .
+
+# Copy chronik to same directory structure as monorepo
+# This needs to be in place to run ./build-wasm
+WORKDIR /app/chronik/
+COPY chronik/ .
+
+# Copy secp256k1 to same directory structure as monorepo
+WORKDIR /app/src/secp256k1
+COPY src/secp256k1/ .
+
+# Copy ecash-secp256k1, ecash-lib and ecash-lib-wasm files to same directory structure as monorepo
+WORKDIR /app/modules/ecash-secp256k1
+COPY modules/ecash-secp256k1 .
+WORKDIR /app/modules/ecash-lib
+COPY modules/ecash-lib .
+WORKDIR /app/modules/ecash-lib-wasm
+COPY modules/ecash-lib-wasm .
+
+# Build web assembly for ecash-lib
+RUN ./build-wasm.sh
+
+# 2) Node image for prod deployment of token-server
+
 # Node image for prod deployment of ecash-herald
 
 FROM node:20-bookworm-slim
 
+# Copy static assets from WasmBuilder stage (ecash-lib-wasm and ecash-lib, with wasm built in place)
+WORKDIR /app/modules
+COPY --from=WasmBuilder /app/modules .
+
 # Build all local ecash-herald dependencies
 
 # ecashaddrjs
@@ -25,6 +67,17 @@
 COPY modules/ecash-script/ .
 RUN npm ci
 
+# ecash-lib
+WORKDIR /app/modules/ecash-lib
+RUN npm ci
+RUN npm run build
+
+# ecash-agora
+WORKDIR /app/modules/ecash-agora
+COPY modules/ecash-agora/ .
+RUN npm ci
+RUN npm run build
+
 # Now that local dependencies are ready, build ecash-herald
 WORKDIR /app/apps/ecash-herald
 
diff --git a/modules/ecash-agora/README.md b/modules/ecash-agora/README.md
--- a/modules/ecash-agora/README.md
+++ b/modules/ecash-agora/README.md
@@ -159,3 +159,4 @@
 -   Add validation to acceptTx method of AgoraPartial to prevent creation of unspendable offers [D16944](https://reviews.bitcoinabc.org/D16944)
 -   Export `scriptOps` helper function [D16972](https://reviews.bitcoinabc.org/D16972)
 -   Improve approximation for USD-esque tokens [D16995](https://reviews.bitcoinabc.org/D16995)
+-   Update tsconfig to support use in nodejs [D17019](https://reviews.bitcoinabc.org/D17019)
diff --git a/modules/ecash-agora/package.json b/modules/ecash-agora/package.json
--- a/modules/ecash-agora/package.json
+++ b/modules/ecash-agora/package.json
@@ -3,7 +3,6 @@
     "version": "0.1.1",
     "description": "Library for interacting with the eCash Agora protocol",
     "main": "./dist/index.js",
-    "type": "module",
     "scripts": {
         "build": "tsc && tsc -p ./tsconfig.build.json",
         "test": "mocha --import=tsx ./src/*.test.ts ./src/**/*.test.ts",
diff --git a/modules/ecash-agora/tsconfig.json b/modules/ecash-agora/tsconfig.json
--- a/modules/ecash-agora/tsconfig.json
+++ b/modules/ecash-agora/tsconfig.json
@@ -5,8 +5,7 @@
         /* ES2020 for bigint, DOM for WebAssemby */
         "lib": ["ES2020", "DOM"],
         /* Modules */
-        "module": "NodeNext",
-        "moduleResolution": "NodeNext",
+        "module": "commonjs",
         /* Emit */
         "noEmit": true,
         /* Interop Constraints */