diff --git a/modules/chronik-client/README.md b/modules/chronik-client/README.md
--- a/modules/chronik-client/README.md
+++ b/modules/chronik-client/README.md
@@ -101,3 +101,4 @@
 0.22.1 - Return `script` key for utxos fetched from `tokenId` endpoint
 0.23.0 - Add support for returning `TokenInfo` from `chronik.token(tokenId)` calls to `ChronikClientNode`
 0.24.0 - Support `subscribeToAddress` and `unsubscribeFromAddress` methods in the `ChronikClientNode` websocket
+0.25.0 - Add support for `validateRawTx` endpoint to `ChronikClientNode`
diff --git a/modules/chronik-client/package-lock.json b/modules/chronik-client/package-lock.json
--- a/modules/chronik-client/package-lock.json
+++ b/modules/chronik-client/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "chronik-client",
-    "version": "0.24.0",
+    "version": "0.25.0",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "chronik-client",
-            "version": "0.24.0",
+            "version": "0.25.0",
             "license": "MIT",
             "dependencies": {
                 "@types/ws": "^8.2.1",
diff --git a/modules/chronik-client/package.json b/modules/chronik-client/package.json
--- a/modules/chronik-client/package.json
+++ b/modules/chronik-client/package.json
@@ -1,6 +1,6 @@
 {
     "name": "chronik-client",
-    "version": "0.24.0",
+    "version": "0.25.0",
     "description": "A client for accessing the Chronik Indexer API",
     "main": "dist/index.js",
     "types": "dist/index.d.ts",
diff --git a/modules/chronik-client/src/ChronikClientNode.ts b/modules/chronik-client/src/ChronikClientNode.ts
--- a/modules/chronik-client/src/ChronikClientNode.ts
+++ b/modules/chronik-client/src/ChronikClientNode.ts
@@ -80,6 +80,16 @@
         };
     }
 
+    /** Validate a tx by rawtx to check if it unintentionally burns tokens */
+    public async validateRawTx(rawTx: Uint8Array | string): Promise<Tx_InNode> {
+        const request = proto.RawTx.encode({
+            rawTx: typeof rawTx === 'string' ? fromHex(rawTx) : rawTx,
+        }).finish();
+        const data = await this._proxyInterface.post('/validate-tx', request);
+        const validateResponse = proto.Tx.decode(data);
+        return convertToTx(validateResponse);
+    }
+
     /** Fetch current info of the blockchain, such as tip hash and height. */
     public async blockchainInfo(): Promise<BlockchainInfo> {
         const data = await this._proxyInterface.get(`/blockchain-info`);
diff --git a/modules/chronik-client/test/integration/broadcast_txs.ts b/modules/chronik-client/test/integration/broadcast_txs_and_validate_rawtx.ts
rename from modules/chronik-client/test/integration/broadcast_txs.ts
rename to modules/chronik-client/test/integration/broadcast_txs_and_validate_rawtx.ts
--- a/modules/chronik-client/test/integration/broadcast_txs.ts
+++ b/modules/chronik-client/test/integration/broadcast_txs_and_validate_rawtx.ts
@@ -7,7 +7,7 @@
 import { ChildProcess } from 'node:child_process';
 import { EventEmitter, once } from 'node:events';
 import path from 'path';
-import { ChronikClientNode } from '../../index';
+import { ChronikClientNode, Tx_InNode } from '../../index';
 import initializeTestRunner, {
     cleanupMochaRegtest,
     setMochaTimeout,
@@ -33,6 +33,9 @@
     let get_test_info: Promise<TestInfo>;
     let chronikUrl: string[];
     let setupScriptTermination: ReturnType<typeof setTimeout>;
+    let alpGenesisRawTx: string;
+    let alpGenesisPreview: Tx_InNode;
+    let alpGenesisAfter: Tx_InNode;
 
     before(async function () {
         // Initialize testRunner before mocha tests
@@ -133,30 +136,71 @@
         testRunner.send('next');
     });
 
-    it('New regtest chain', async () => {
+    it('New regtest chain. Behavior of broadcastTx and validateRawTx.', async () => {
         // Initialize new ChronikClientNode
         const chronik = new ChronikClientNode(chronikUrl);
 
         // We can't broadcast an invalid tx
+        const BAD_VALUE_IN_SATS = 2500000000;
+        const BAD_VALUE_OUT_SATS = 4999999960000;
+        const SATOSHIS_PER_XEC = 100;
         const BAD_RAW_TX =
             '0100000001fa5b8f14f5b63ae42f7624a416214bdfffd1de438e9db843a4ddf4d392302e2100000000020151000000000800000000000000003c6a5039534c5032000747454e4553495300000000000006e80300000000d00700000000b80b00000000a00f0000000088130000000070170000000000102700000000000017a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b87102700000000000017a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b87102700000000000017a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b87102700000000000017a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b87102700000000000017a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b87102700000000000017a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b8760c937278c04000017a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b8700000000';
         await expect(chronik.broadcastTx(BAD_RAW_TX)).to.be.rejectedWith(
             Error,
-            `Failed getting /broadcast-tx (): 400: Broadcast failed: Transaction rejected by mempool: bad-txns-in-belowout, value in (25000000.00) < value out (49999999600.00)`,
+            `Failed getting /broadcast-tx (): 400: Broadcast failed: Transaction rejected by mempool: bad-txns-in-belowout, value in (${(
+                BAD_VALUE_IN_SATS / SATOSHIS_PER_XEC
+            ).toFixed(2)}) < value out (${(
+                BAD_VALUE_OUT_SATS / SATOSHIS_PER_XEC
+            ).toFixed(2)})`,
         );
 
-        // We can broadcast an ALP genesis tx
-        const alpGenesisRawTx = await get_alp_genesis_rawtx;
+        // We can, though, preview an invalid tx using chronik.validateRawTx
+        const invalidTx = await chronik.validateRawTx(BAD_RAW_TX);
+        expect(invalidTx.txid).to.eql(
+            '5c91ef5b654d21ad5db2c7af71f2924a0c31a5ef347498bbcba8ee1374d6c6a9',
+        );
+        const invalidTxSumInputs = invalidTx.inputs
+            .map(input => input.value)
+            .reduce((prev, curr) => prev + curr, 0);
+        const invalidTxSumOutputs = invalidTx.outputs
+            .map(output => output.value)
+            .reduce((prev, curr) => prev + curr, 0);
+
+        // Indeed, the outputs are greater than the inputs, and such that the tx is invalid
+        expect(invalidTxSumInputs).to.eql(BAD_VALUE_IN_SATS);
+        expect(invalidTxSumOutputs).to.eql(BAD_VALUE_OUT_SATS);
+
+        // We cannot call validateRawTx to get a tx from a rawtx of a normal token send tx if its inputs are not in the mempool or db
+        // txid in blockchain but not regtest, 423e24bf0715cfb80727e5e7a6ff7b9e37cb2f555c537ab06fdc7fd9b3a0ba3a
+        const NORMAL_TOKEN_SEND =
+            '020000000278e5886fb86174d9abd4af331a4b67a3baf37d052703c176009a92dba60181d9020000006b4830450221009149768d5e8b2bedf8259f91741db160ae389451ed11bb376f372c61c88d58ec02202492c21df1b21b99d7b021eb6eef78be99b45a64e9c7d9ce8f8880abfa28a5e4412103632f603f43ae61afece65288d7d92e55188783edb74e205be974b8cd1cd36a1effffffff78e5886fb86174d9abd4af331a4b67a3baf37d052703c176009a92dba60181d9030000006b483045022100f7311d000d3fbe672dd742e85f372cd6d52435210d0c92f21e73ca6588918a4702204b5a7a90a73e5fd48f90af24c02c4f15e8c40515af931dd44f8030691a2e5d8d412103632f603f43ae61afece65288d7d92e55188783edb74e205be974b8cd1cd36a1effffffff040000000000000000406a04534c500001010453454e4420fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa0800000002540be40008000000079d6e2ee722020000000000001976a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac22020000000000001976a9141c13ddb8dd422bbe02dc2ae8798b4549a67a3c1d88acf7190600000000001976a9141c13ddb8dd422bbe02dc2ae8798b4549a67a3c1d88ac00000000';
+
+        await expect(
+            chronik.validateRawTx(NORMAL_TOKEN_SEND),
+        ).to.be.rejectedWith(
+            Error,
+            'Failed getting /validate-tx (): 400: Failed indexing mempool token tx: Tx is spending d98101a6db929a0076c10327057df3baa3674b1a33afd4abd97461b86f88e578 which is found neither in the mempool nor DB',
+        );
+
+        alpGenesisRawTx = await get_alp_genesis_rawtx;
         const alpGenesisTxid = await get_alp_genesis_txid;
 
+        // We can validateRawTx an ALP genesis tx before it is broadcast
+        alpGenesisPreview = await chronik.validateRawTx(alpGenesisRawTx);
+
+        // We can broadcast an ALP genesis tx
         expect(await chronik.broadcastTx(alpGenesisRawTx)).to.deep.equal({
             txid: alpGenesisTxid,
         });
 
-        // We can't broadcast an ALP burn tx without setting skipTokenChecks
         const alpBurnRawTx = await get_alp_burn_rawtx;
         const alpBurnTxid = await get_alp_burn_txid;
 
+        // We can preview an ALP burn tx before it is broadcast
+        const alpBurnPreview = await chronik.validateRawTx(alpBurnRawTx);
+
+        // We can't broadcast an ALP burn tx without setting skipTokenChecks
         await expect(chronik.broadcastTx(alpBurnRawTx)).to.be.rejectedWith(
             Error,
             `Failed getting /broadcast-tx (): 400: Tx ${alpBurnTxid} failed token checks: Unexpected burn: Burns 1 base tokens.`,
@@ -183,6 +227,9 @@
             `Failed getting /broadcast-txs (): 400: Broadcast failed: Transaction rejected by mempool: txn-mempool-conflict`,
         );
 
+        // We can also preview okRawTx before it is broadcast
+        const okPreview = await chronik.validateRawTx(okRawTx);
+
         // We can broadcast multiple txs including a burn if we set skipTokenChecks
         expect(
             await chronik.broadcastTxs([okRawTx, alpBurnRawTx], true),
@@ -191,27 +238,57 @@
         // We can broadcast an ALP burn tx if we set skipTokenChecks
         const alpBurnTwoRawTx = await get_alp_burn_2_rawtx;
         const alpBurnTwoTxid = await get_alp_burn_2_txid;
+
+        // And we can preview it before it is broadcast
+        const alpBurnTwoPreview = await chronik.validateRawTx(alpBurnTwoRawTx);
+
         expect(await chronik.broadcastTx(alpBurnTwoRawTx, true)).to.deep.equal({
             txid: alpBurnTwoTxid,
         });
 
         // All of these txs are in the mempool, i.e. they have been broadcast
-        const broadcastTxids = [
-            alpGenesisTxid,
-            okTxid,
-            alpBurnTxid,
-            alpBurnTwoTxid,
+        const broadcastTxs = [
+            { txid: alpGenesisTxid, preview: alpGenesisPreview },
+            { txid: okTxid, preview: okPreview },
+            { txid: alpBurnTxid, preview: alpBurnPreview },
+            { txid: alpBurnTwoTxid, preview: alpBurnTwoPreview },
         ];
-        for (const txid of broadcastTxids) {
-            expect((await chronik.tx(txid)).txid).to.eql(txid);
+        for (const tx of broadcastTxs) {
+            const { txid, preview } = tx;
+            const txInMempool = await chronik.tx(txid);
+            // We get the same tx from the mempool
+            expect(txInMempool.txid).to.eql(txid);
+            // Previewing the tx gives us the same info as calling chronik.tx on the tx in the mempool
+            // Except for expected changes
+            // - spentBy key is present in mempool if it was spentBy
+            // - timeFirstSeen is 0 in the preview txs
+            expect({ ...preview }).to.deep.equal({
+                ...txInMempool,
+                // preview txs have timeFirstSeen of 0
+                timeFirstSeen: 0,
+                // preview txs have output.spentBy undefined
+                outputs: [...preview.outputs],
+            });
         }
 
-        // We can't broadcast an invalid rawtx
+        // If we use validateRawTx on a tx that has been broadcast, we still get timeFirstSeen of 0
+        // and outputs.spentBy of undefined, even if the outputs have been spent
+        // We can validateRawTx an ALP genesis tx after it is broadcast
+        alpGenesisAfter = await chronik.validateRawTx(alpGenesisRawTx);
+        expect(alpGenesisAfter).to.deep.equal(alpGenesisPreview);
+
+        // We can't broadcast an invalid hex rawtx
         await expect(chronik.broadcastTx('not a rawtx')).to.be.rejectedWith(
             Error,
             `Odd hex length: not a rawtx`,
         );
 
+        // We can't preview an invalid hex rawtx
+        await expect(chronik.validateRawTx('not a rawtx')).to.be.rejectedWith(
+            Error,
+            `Odd hex length: not a rawtx`,
+        );
+
         // We can't broadcast a rawtx that conflicts with the mempool
         await expect(chronik.broadcastTx(BAD_RAW_TX)).to.be.rejectedWith(
             Error,
@@ -241,11 +318,32 @@
         const chronik = new ChronikClientNode(chronikUrl);
 
         const alpGenesisRawTx = await get_alp_genesis_rawtx;
-        // If we broadcast a tx already in the mempool, we get a normal response
 
+        // We can't broadcast a rawtx if it is already in a block
         await expect(chronik.broadcastTx(alpGenesisRawTx)).to.be.rejectedWith(
             Error,
             `Failed getting /broadcast-tx (): 400: Broadcast failed: Transaction already in block chain`,
         );
+
+        // If we use validateRawTx on a tx that has been mined, we still get timeFirstSeen of 0
+        // and outputs.spentBy of undefined, even if the outputs have been spent
+        const alpGenesisAfterMined = await chronik.validateRawTx(
+            alpGenesisRawTx,
+        );
+
+        expect(alpGenesisPreview).to.deep.equal({
+            ...alpGenesisAfterMined,
+            // We no longer have outputScript in this input
+            // TODO is this a bug, or some expected quirk in this tx?
+            inputs: [
+                {
+                    ...alpGenesisAfterMined.inputs[0],
+                    // inputs[0].outputScript is undefined in alpGenesisAfterMined
+                    outputScript: alpGenesisPreview.inputs[0].outputScript,
+                    // inputs[0].value is 0 in alpGenesisAfterMined
+                    value: alpGenesisPreview.inputs[0].value,
+                },
+            ],
+        });
     });
 });
diff --git a/test/functional/setup_scripts/chronik-client_broadcast_txs.py b/test/functional/setup_scripts/chronik-client_broadcast_txs_and_validate_rawtx.py
rename from test/functional/setup_scripts/chronik-client_broadcast_txs.py
rename to test/functional/setup_scripts/chronik-client_broadcast_txs_and_validate_rawtx.py