diff --git a/test/functional/chronik_electrum.py b/test/functional/chronik_electrum.py
new file mode 100644
--- /dev/null
+++ b/test/functional/chronik_electrum.py
@@ -0,0 +1,65 @@
+# Copyright (c) 2024-present The Bitcoin developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""
+Test Chronik's electrum interface
+"""
+from test_framework.jsonrpctools import synchronous_jsonrpc_get
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import assert_equal, chronikelectrum_port
+
+
+class ChronikElectrum(BitcoinTestFramework):
+    def set_test_params(self):
+        self.setup_clean_chain = True
+        self.num_nodes = 1
+        self.port = chronikelectrum_port(0)
+        self.extra_args = [["-chronik"]]
+
+    def skip_test_if_missing_module(self):
+        self.skip_if_no_chronik()
+
+    def run_test(self):
+        self.unique_id = -1
+        self.test_errors()
+        self.test_success()
+
+    def check_method(self, method, params, expected_result, expected_error):
+        self.unique_id += 1
+        result = synchronous_jsonrpc_get(
+            "127.0.0.1", self.port, self.unique_id, method, params
+        )
+        assert_equal(result.result, expected_result)
+        assert_equal(result.error, expected_error)
+        if not expected_error:
+            assert_equal(result.id, self.unique_id)
+
+    def test_errors(self):
+        """Test adherence to the JSON RPC spec (error codes...)
+        See https://www.jsonrpc.org/specification
+        """
+        # This should return a 32601 code as per spec
+        self.check_method(
+            method="foobar",
+            params=None,
+            expected_result=None,
+            expected_error={"code": -32600, "message": "Invalid request"},
+        )
+
+        # This should return a -32600 code as per the example in the spec
+        self.check_method(
+            method=-1,
+            params=None,
+            expected_result=None,
+            expected_error={"code": -32700, "message": "Failed to parse"},
+        )
+
+    def test_success(self):
+        # This method return {... "result":null} which JSON-decodes to None
+        self.check_method(
+            "server.ping", params=None, expected_result=None, expected_error=None
+        )
+
+
+if __name__ == "__main__":
+    ChronikElectrum().main()
diff --git a/test/functional/test_framework/jsonrpctools.py b/test/functional/test_framework/jsonrpctools.py
new file mode 100644
--- /dev/null
+++ b/test/functional/test_framework/jsonrpctools.py
@@ -0,0 +1,54 @@
+# Copyright (c) 2024-present The Bitcoin developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+from __future__ import annotations
+
+import json
+import socket
+from typing import Any, Optional
+
+
+class JsonRpcResponse:
+    def __init__(
+        self,
+        id_: Optional[int],
+        result: Optional[Any] = None,
+        error: Optional[dict] = None,
+    ):
+        self.id = id_
+        self.result = result
+        self.error = error
+
+
+def synchronous_jsonrpc_get(
+    host: str,
+    port: int,
+    id_: Optional[int],
+    method: str,
+    params: Optional[list | dict] = None,
+) -> JsonRpcResponse:
+    """Send a JSON-RPC query over a raw tcp socket.
+    :param method: Name of the method to be invoked
+    :param params: A JSON serializable structured value that holds the parameter values
+        to be used during the invocation of the method.
+    :raises: May raise any error related to socket connections or JSON parsing
+    """
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    s.connect((host, port))
+    request: dict[str, Any] = {"jsonrpc": "2.0", "method": method}
+    if id_ is not None:
+        request["id"] = id_
+    if params is not None:
+        request["params"] = params
+    s.send(json.dumps(request).encode("utf-8") + b"\n")
+
+    data = b""
+    while b"\n" not in data:
+        data += s.recv(1024)
+
+    json_reply = json.loads(data.decode("utf-8"))
+    # As per the JSONRPC spec, we cannot have both an error and a result
+    assert "error" not in json_reply or "result" not in json_reply
+    return JsonRpcResponse(
+        json_reply.get("id"), json_reply.get("result"), json_reply.get("error")
+    )
diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py
--- a/test/functional/test_framework/util.py
+++ b/test/functional/test_framework/util.py
@@ -311,6 +311,7 @@
     P2P = 0
     RPC = 1
     CHRONIK = 2
+    CHRONIKELECTRUM = 3
 
 
 # The maximum number of nodes a single test can spawn
@@ -327,6 +328,7 @@
     PortName.P2P: 0,
     PortName.RPC: PORT_RANGE,
     PortName.CHRONIK: PORT_RANGE * 2,
+    PortName.CHRONIKELECTRUM: PORT_RANGE * 3,
 }
 
 # Globals used for incrementing ports. Initially uninitialized because they
@@ -422,6 +424,10 @@
     return unique_port(PortName.CHRONIK, n)
 
 
+def chronikelectrum_port(n: int) -> int:
+    return unique_port(PortName.CHRONIKELECTRUM, n)
+
+
 def rpc_url(datadir, chain, host, port):
     rpc_u, rpc_p = get_auth_cookie(datadir, chain)
     if host is None:
@@ -464,6 +470,7 @@
         f.write(f"port={str(p2p_port(n))}\n")
         f.write(f"rpcport={str(rpc_port(n))}\n")
         f.write(f"chronikbind=127.0.0.1:{str(chronik_port(n))}\n")
+        f.write(f"chronikelectrumbind=127.0.0.1:{chronikelectrum_port(n)}\n")
         # Disable server-side timeouts to avoid intermittent issues
         f.write("rpcservertimeout=99000\n")
         # Chronik by default is tuned for initial sync, tune it down for regtest