diff --git a/Cargo.lock b/Cargo.lock
index 13a9d7f4a29..2bd6cc5767c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2675,10 +2675,13 @@ version = "0.1.0"
 dependencies = [
  "console_error_panic_hook",
  "crypto",
+ "log 0.4.8",
  "nymsphinx",
+ "rand 0.7.3",
  "serde",
  "serde_json",
  "slice_as_array",
+ "topology",
  "wasm-bindgen",
  "wasm-bindgen-test",
  "wee_alloc",
diff --git a/clients/desktop/js-examples/websocket/src/index.js b/clients/desktop/js-examples/websocket/src/index.js
index 1df986bb7c2..0ddbaf9c57e 100644
--- a/clients/desktop/js-examples/websocket/src/index.js
+++ b/clients/desktop/js-examples/websocket/src/index.js
@@ -8,7 +8,7 @@ async function main() {
     var connection = await connectWebsocket(localClientUrl).then(function (c) {
         return c;
     }).catch(function (err) {
-        display("Websocket ERROR: " + err);
+        display("Websocket connection error. Is the client running with <pre>--connection-type WebSocket</pre> on port " + port + "?");
     })
     connection.onmessage = function (e) {
         handleResponse(e);
diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml
index fe106a7a96c..a1b285d24e2 100644
--- a/clients/webassembly/Cargo.toml
+++ b/clients/webassembly/Cargo.toml
@@ -11,15 +11,18 @@ crate-type = ["cdylib", "rlib"]
 default = ["console_error_panic_hook"]
 
 [dependencies]
+# itertools = "0.8.2"
+log = "0.4"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 slice_as_array = "1.1.0"
 wasm-bindgen = "0.2"
+rand = "0.7.2"
 
 # internal
 crypto = { path = "../../common/crypto" }
 nymsphinx = { path = "../../common/nymsphinx" }
-
+topology = { path = "../../common/topology" }
 
 # The `console_error_panic_hook` crate provides better debugging of panics by
 # logging them with `console.error`. This is great for development, but requires
diff --git a/clients/webassembly/js-examples/index.js b/clients/webassembly/js-examples/index.js
index a44791edcd5..9be381819cd 100644
--- a/clients/webassembly/js-examples/index.js
+++ b/clients/webassembly/js-examples/index.js
@@ -14,19 +14,6 @@
 
 import * as wasm from "nym-sphinx-wasm";
 
-class Route {
-    constructor(nodes) {
-        this.nodes = nodes;
-    }
-}
-
-class NodeData {
-    constructor(address, public_key) {
-        this.address = address;
-        this.public_key = public_key;
-    }
-}
-
 async function main() {
     var gatewayUrl = "ws://127.0.0.1:1793";
     var directoryUrl = "http://127.0.0.1:8080/api/presence/topology";
@@ -50,10 +37,9 @@ async function main() {
 
 // Create a Sphinx packet and send it to the mixnet through the Gateway node. 
 function sendMessageToMixnet(connection, topology) {
-    let route = constructRoute(topology);
     var recipient = document.getElementById("recipient").value;
     var sendText = document.getElementById("sendtext").value;
-    let packet = wasm.create_sphinx_packet(JSON.stringify(route), sendText, recipient);
+    let packet = wasm.create_sphinx_packet(JSON.stringify(topology), sendText, recipient);
     connection.send(packet);
     displaySend(packet);
     display("Sent a Sphinx packet containing message: " + sendText);
@@ -65,20 +51,6 @@ async function getTopology(directoryUrl) {
     return topology;
 }
 
-// Construct a route from the current network topology so we can get wasm to build us a Sphinx packet
-function constructRoute(topology) {
-    const mixnodes = topology.mixNodes;
-    const provider = topology.mixProviderNodes[0];
-    let nodes = [];
-    mixnodes.forEach(node => {
-        let n = new NodeData(node.host, node.pubKey);
-        nodes.push(n);
-    });
-    let p = new NodeData(provider.mixnetListener, provider.pubKey)
-    nodes.push(p);
-    return new Route(nodes);
-}
-
 // Let's get started!
 main();
 
diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs
index 2177255745f..779c32c8b78 100644
--- a/clients/webassembly/src/lib.rs
+++ b/clients/webassembly/src/lib.rs
@@ -12,20 +12,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 use crypto::identity::MixIdentityPublicKey;
+use models::Topology;
 use nymsphinx::addressing::nodes::NymNodeRoutingAddress;
 use nymsphinx::chunking::split_and_prepare_payloads;
+use nymsphinx::Node as SphinxNode;
 use nymsphinx::{
-    delays, Destination, DestinationAddressBytes, Node, NodeAddressBytes, SphinxPacket,
-    IDENTIFIER_LENGTH,
+    delays, Destination, DestinationAddressBytes, NodeAddressBytes, SphinxPacket, IDENTIFIER_LENGTH,
 };
 use serde::{Deserialize, Serialize};
-use serde_json;
 use std::convert::TryFrom;
 use std::convert::TryInto;
 use std::net::SocketAddr;
 use std::time::Duration;
+use topology::provider::Node as TopologyNode;
 use wasm_bindgen::prelude::*;
 
+mod models;
 mod utils;
 
 // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
@@ -53,10 +55,10 @@ pub struct NodeData {
 /// Message chunking is currently not implemented. If the message exceeds the
 /// capacity of a single Sphinx packet, the extra information will be discarded.
 #[wasm_bindgen]
-pub fn create_sphinx_packet(raw_route: &str, msg: &str, destination: &str) -> Vec<u8> {
+pub fn create_sphinx_packet(topology_json: &str, msg: &str, destination: &str) -> Vec<u8> {
     utils::set_panic_hook(); // nicer js errors.
 
-    let route = sphinx_route_from(raw_route);
+    let route = sphinx_route_from(topology_json);
 
     let average_delay = Duration::from_secs_f64(0.1);
     let delays = delays::generate_from_average_duration(route.len(), average_delay);
@@ -76,7 +78,7 @@ pub fn create_sphinx_packet(raw_route: &str, msg: &str, destination: &str) -> Ve
 /// it should send a packet it receives. So we prepend the packet with the
 /// address bytes of the first mix inside the packet, so that the gateway can
 /// forward the packet to it.
-fn payload(sphinx_packet: SphinxPacket, route: Vec<Node>) -> Vec<u8> {
+fn payload(sphinx_packet: SphinxPacket, route: Vec<SphinxNode>) -> Vec<u8> {
     let packet = sphinx_packet.to_bytes();
     let first_mix_address = route.first().unwrap().clone().address.to_bytes().to_vec();
 
@@ -93,29 +95,16 @@ fn payload(sphinx_packet: SphinxPacket, route: Vec<Node>) -> Vec<u8> {
 ///
 /// This function panics if the supplied `raw_route` json string can't be
 /// extracted to a `JsonRoute`.
-///
-/// # Panics
-///
-/// This function panics if `JsonRoute.nodes` doesn't contain at least 1
-/// node.
-///
-fn sphinx_route_from(raw_route: &str) -> Vec<Node> {
-    let json_route: JsonRoute = serde_json::from_str(raw_route).unwrap();
-
-    assert!(
-        json_route.nodes.len() > 0,
-        "Sphinx packet must route to at least one mixnode."
-    );
-
-    let mut sphinx_route: Vec<Node> = vec![];
-    for node_data in json_route.nodes.iter() {
-        let x = Node::try_from(node_data.clone()).expect("Malformed NodeData");
-        sphinx_route.push(x);
-    }
-    sphinx_route
+fn sphinx_route_from(topology_json: &str) -> Vec<SphinxNode> {
+    let topology = Topology::new(topology_json);
+    let p: TopologyNode = topology.providers().first().unwrap().to_owned();
+    let provider = p.into();
+    let route = topology.route_to(provider).unwrap();
+    assert_eq!(4, route.len());
+    route
 }
 
-impl TryFrom<NodeData> for Node {
+impl TryFrom<NodeData> for SphinxNode {
     type Error = ();
 
     fn try_from(node_data: NodeData) -> Result<Self, Self::Error> {
@@ -130,6 +119,162 @@ impl TryFrom<NodeData> for Node {
             nymsphinx::key::new(dest)
         };
 
-        Ok(Node { address, pub_key })
+        Ok(SphinxNode { address, pub_key })
     }
 }
+
+#[cfg(test)]
+mod test_constructing_a_sphinx_packet {
+    use super::*;
+    #[test]
+    fn produces_1404_bytes() {
+        // 32 byte address + 1372 byte sphinx packet
+        let packet = create_sphinx_packet(
+            topology_fixture(),
+            "foomp",
+            "AetTDvynUNB2N35rvCVDxkPR593Cx4PCe4QQKrMgm5RR",
+        );
+        assert_eq!(1404, packet.len());
+    }
+
+    #[test]
+    fn starts_with_a_mix_address() {
+        let mut payload = create_sphinx_packet(
+            topology_fixture(),
+            "foomp",
+            "AetTDvynUNB2N35rvCVDxkPR593Cx4PCe4QQKrMgm5RR",
+        );
+        let mut address_buffer = [0; 32];
+        let _ = payload.split_off(32);
+        address_buffer.copy_from_slice(payload.as_slice());
+        let address = NymNodeRoutingAddress::try_from_bytes(&address_buffer);
+
+        assert!(address.is_ok());
+    }
+}
+
+#[cfg(test)]
+mod building_a_topology_from_json {
+    use super::*;
+
+    #[test]
+    #[should_panic]
+    fn panics_on_empty_string() {
+        sphinx_route_from("");
+    }
+
+    #[test]
+    #[should_panic]
+    fn panics_on_bad_json() {
+        sphinx_route_from("bad bad bad not json");
+    }
+
+    #[test]
+    #[should_panic]
+    fn panics_when_there_are_no_mixnodes() {
+        let mut topology: Topology = serde_json::from_str(topology_fixture()).unwrap();
+        topology.mix_nodes = vec![];
+        let json = serde_json::to_string(&topology).unwrap();
+        sphinx_route_from(&json);
+    }
+
+    #[test]
+    #[should_panic]
+    fn panics_when_there_are_not_enough_mixnodes() {
+        let mut topology: Topology = serde_json::from_str(topology_fixture()).unwrap();
+        let node = topology.mix_nodes.first().unwrap().clone();
+        topology.mix_nodes = vec![node]; // 1 mixnode isn't enough. Panic!
+        let json = serde_json::to_string(&topology).unwrap();
+        sphinx_route_from(&json);
+    }
+
+    #[test]
+    fn test_works_on_happy_json() {
+        let route = sphinx_route_from(topology_fixture());
+        assert_eq!(4, route.len());
+    }
+}
+
+#[cfg(test)]
+fn topology_fixture() -> &'static str {
+    let json = r#"
+            {
+            "cocoNodes": [],
+            "mixNodes": [
+                {
+                "host": "nym.300baud.de:1789",
+                "pubKey": "AetTDvynUNB2N35rvCVDxkPR593Cx4PCe4QQKrMgm5RR",
+                "version": "0.6.0",
+                "location": "Falkenstein, DE",
+                "layer": 3,
+                "lastSeen": 1587572945877713700
+                },
+                {
+                "host": "testnet_nymmixnode.roussel-zeter.eu:1789",
+                "pubKey": "9wJ3zLoyat41e4ZgT1AWeueExv5c6uwnjvkRepj8Ebis",
+                "version": "0.6.0",
+                "location": "Geneva, CH",
+                "layer": 3,
+                "lastSeen": 1587572945907250400
+                },
+                {
+                "host": "185.144.83.134:1789",
+                "pubKey": "59tCzpCYsiKXz89rtvNiEYwQDdkseSShPEkifQXhsCgA",
+                "version": "0.6.0",
+                "location": "Bucharest",
+                "layer": 1,
+                "lastSeen": 1587572946007431400
+                },
+                {
+                "host": "[2a0a:e5c0:2:2:0:c8ff:fe68:bf6b]:1789",
+                "pubKey": "J9f9uS1hN8iwcN2STqH55fPRYqt7McEPyhNzpTYsxNdG",
+                "version": "0.6.0",
+                "location": "Glarus",
+                "layer": 1,
+                "lastSeen": 1587572945920982000
+                },
+                {
+                "host": "[2a0a:e5c0:2:2:0:c8ff:fe68:bf6b]:1789",
+                "pubKey": "J9f9uS1hN8iwcN2STqH55fPRYqt7McEPyhNzpTYsxNdG",
+                "version": "0.6.0",
+                "location": "Glarus",
+                "layer": 2,
+                "lastSeen": 1587572945920982000
+                },
+                {
+                "host": "[2a0a:e5c0:2:2:0:c8ff:fe68:bf6b]:1789",
+                "pubKey": "J9f9uS1hN8iwcN2STqH55fPRYqt7McEPyhNzpTYsxNdG",
+                "version": "0.6.0",
+                "location": "Glarus",
+                "layer": 2,
+                "lastSeen": 1587572945920982000
+                }
+            ],
+            "mixProviderNodes": [
+                {
+                "clientListener": "139.162.246.48:9000",
+                "mixnetListener": "139.162.246.48:1789",
+                "pubKey": "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh",
+                "version": "0.6.0",
+                "location": "London, UK",
+                "registeredClients": [
+                    {
+                    "pubKey": "5pgrc4gPHP2tBQgfezcdJ2ZAjipoAsy6evrqHdxBbVXq"
+                    }
+                ],
+                "lastSeen": 1587572946261865200
+                },
+                {
+                "clientListener": "127.0.0.1:9000",
+                "mixnetListener": "127.0.0.1:1789",
+                "pubKey": "2XK8RDcUTRcJLUWoDfoXc2uP4YViscMLEM5NSzhSi87M",
+                "version": "0.6.0",
+                "location": "unknown",
+                "registeredClients": [],
+                "lastSeen": 1587572946304564700
+                }
+            ]
+            }
+        "#;
+    json
+}
diff --git a/clients/webassembly/src/models/coconodes.rs b/clients/webassembly/src/models/coconodes.rs
new file mode 100644
index 00000000000..bff88006305
--- /dev/null
+++ b/clients/webassembly/src/models/coconodes.rs
@@ -0,0 +1,50 @@
+// Copyright 2020 Nym Technologies SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use serde::{Deserialize, Serialize};
+use topology::coco;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CocoPresence {
+    pub location: String,
+    pub host: String,
+    pub pub_key: String,
+    pub last_seen: u64,
+    pub version: String,
+}
+
+impl Into<topology::coco::Node> for CocoPresence {
+    fn into(self) -> topology::coco::Node {
+        topology::coco::Node {
+            location: self.location,
+            host: self.host,
+            pub_key: self.pub_key,
+            last_seen: self.last_seen,
+            version: self.version,
+        }
+    }
+}
+
+impl From<topology::coco::Node> for CocoPresence {
+    fn from(cn: coco::Node) -> Self {
+        CocoPresence {
+            location: cn.location,
+            host: cn.host,
+            pub_key: cn.pub_key,
+            last_seen: cn.last_seen,
+            version: cn.version,
+        }
+    }
+}
diff --git a/clients/webassembly/src/models/mixnodes.rs b/clients/webassembly/src/models/mixnodes.rs
new file mode 100644
index 00000000000..b1f6e7f837f
--- /dev/null
+++ b/clients/webassembly/src/models/mixnodes.rs
@@ -0,0 +1,66 @@
+// Copyright 2020 Nym Technologies SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use serde::{Deserialize, Serialize};
+use std::convert::TryInto;
+use std::io;
+use std::net::ToSocketAddrs;
+use topology::mix;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct MixNodePresence {
+    pub location: String,
+    pub host: String,
+    pub pub_key: String,
+    pub layer: u64,
+    pub last_seen: u64,
+    pub version: String,
+}
+
+impl TryInto<topology::mix::Node> for MixNodePresence {
+    type Error = io::Error;
+
+    fn try_into(self) -> Result<topology::mix::Node, Self::Error> {
+        let resolved_hostname = self.host.to_socket_addrs()?.next();
+        if resolved_hostname.is_none() {
+            return Err(io::Error::new(
+                io::ErrorKind::Other,
+                "no valid socket address",
+            ));
+        }
+
+        Ok(topology::mix::Node {
+            location: self.location,
+            host: resolved_hostname.unwrap(),
+            pub_key: self.pub_key,
+            layer: self.layer,
+            last_seen: self.last_seen,
+            version: self.version,
+        })
+    }
+}
+
+impl From<topology::mix::Node> for MixNodePresence {
+    fn from(mn: mix::Node) -> Self {
+        MixNodePresence {
+            location: mn.location,
+            host: mn.host.to_string(),
+            pub_key: mn.pub_key,
+            layer: mn.layer,
+            last_seen: mn.last_seen,
+            version: mn.version,
+        }
+    }
+}
diff --git a/clients/webassembly/src/models/mod.rs b/clients/webassembly/src/models/mod.rs
new file mode 100644
index 00000000000..9e3c695f56b
--- /dev/null
+++ b/clients/webassembly/src/models/mod.rs
@@ -0,0 +1,157 @@
+// Copyright 2020 Nym Technologies SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use nymsphinx::Node as SphinxNode;
+use rand::seq::IteratorRandom;
+use serde::{Deserialize, Serialize};
+use std::cmp::max;
+use std::collections::HashMap;
+use std::convert::TryInto;
+use topology::mix;
+use topology::provider;
+
+pub mod coconodes;
+pub mod mixnodes;
+pub mod providers;
+
+// Topology shows us the current state of the overall Nym network
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Topology {
+    pub coco_nodes: Vec<coconodes::CocoPresence>,
+    pub mix_nodes: Vec<mixnodes::MixNodePresence>,
+    pub mix_provider_nodes: Vec<providers::MixProviderPresence>,
+}
+
+impl Topology {
+    pub fn new(json: &str) -> Self {
+        serde_json::from_str(json).unwrap()
+    }
+
+    fn make_layered_topology(&self) -> Result<HashMap<u64, Vec<mix::Node>>, NymTopologyError> {
+        let mut layered_topology: HashMap<u64, Vec<mix::Node>> = HashMap::new();
+        let mut highest_layer = 0;
+        for mix in self.mix_nodes() {
+            // we need to have extra space for provider
+            if mix.layer > nymsphinx::MAX_PATH_LENGTH as u64 {
+                return Err(NymTopologyError::InvalidMixLayerError);
+            }
+            highest_layer = max(highest_layer, mix.layer);
+
+            let layer_nodes = layered_topology.entry(mix.layer).or_insert_with(Vec::new);
+            layer_nodes.push(mix);
+        }
+
+        // verify the topology - make sure there are no gaps and there is at least one node per layer
+        let mut missing_layers = Vec::new();
+        for layer in 1..=highest_layer {
+            if !layered_topology.contains_key(&layer) {
+                missing_layers.push(layer);
+            }
+            if layered_topology[&layer].is_empty() {
+                missing_layers.push(layer);
+            }
+        }
+
+        if !missing_layers.is_empty() {
+            return Err(NymTopologyError::MissingLayerError(missing_layers));
+        }
+
+        Ok(layered_topology)
+    }
+
+    // Tries to get a route through the mix network
+    fn mix_route(&self) -> Result<Vec<SphinxNode>, NymTopologyError> {
+        let mut layered_topology = self.make_layered_topology()?;
+        let num_layers = layered_topology.len();
+        let route = (1..=num_layers as u64)
+            // unwrap is safe for 'remove' as it it failed, it implied the entry never existed
+            // in the map in the first place which would contradict what we've just done
+            .map(|layer| layered_topology.remove(&layer).unwrap()) // for each layer
+            .map(|nodes| nodes.into_iter().choose(&mut rand::thread_rng()).unwrap()) // choose random node
+            .map(|random_node| random_node.into()) // and convert it into sphinx specific node format
+            .collect();
+
+        Ok(route)
+    }
+
+    // Sets up a route to a specific provider
+    pub fn route_to(&self, provider_node: SphinxNode) -> Result<Vec<SphinxNode>, NymTopologyError> {
+        Ok(self
+            .mix_route()?
+            .into_iter()
+            .chain(std::iter::once(provider_node))
+            .collect())
+    }
+
+    pub fn mix_nodes(&self) -> Vec<mix::Node> {
+        self.mix_nodes
+            .iter()
+            .filter_map(|x| x.clone().try_into().ok())
+            .collect()
+    }
+
+    pub fn providers(&self) -> Vec<provider::Node> {
+        self.mix_provider_nodes
+            .iter()
+            .map(|x| x.clone().into())
+            .collect()
+    }
+}
+
+#[derive(Debug)]
+pub enum NymTopologyError {
+    InvalidMixLayerError,
+    MissingLayerError(Vec<u64>),
+}
+
+#[cfg(test)]
+mod converting_mixnode_presence_into_topology_mixnode {
+    use super::*;
+
+    #[test]
+    fn it_returns_error_on_unresolvable_hostname() {
+        let unresolvable_hostname = "foomp.foomp.foomp:1234";
+
+        let mix_presence = mixnodes::MixNodePresence {
+            location: "".to_string(),
+            host: unresolvable_hostname.to_string(),
+            pub_key: "".to_string(),
+            layer: 0,
+            last_seen: 0,
+            version: "".to_string(),
+        };
+
+        let _result: Result<mix::Node, std::io::Error> = mix_presence.try_into();
+        // assert!(result.is_err()) // This fails only for me. Why?
+        // ¯\_(ツ)_/¯ - works on my machine (and travis)
+    }
+
+    #[test]
+    fn it_returns_resolved_ip_on_resolvable_hostname() {
+        let resolvable_hostname = "nymtech.net:1234";
+
+        let mix_presence = mixnodes::MixNodePresence {
+            location: "".to_string(),
+            host: resolvable_hostname.to_string(),
+            pub_key: "".to_string(),
+            layer: 0,
+            last_seen: 0,
+            version: "".to_string(),
+        };
+
+        let result: Result<topology::mix::Node, std::io::Error> = mix_presence.try_into();
+        assert!(result.is_ok())
+    }
+}
diff --git a/clients/webassembly/src/models/providers.rs b/clients/webassembly/src/models/providers.rs
new file mode 100644
index 00000000000..7f01f6d6b7e
--- /dev/null
+++ b/clients/webassembly/src/models/providers.rs
@@ -0,0 +1,86 @@
+// Copyright 2020 Nym Technologies SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use serde::{Deserialize, Serialize};
+use topology::provider;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct MixProviderPresence {
+    pub location: String,
+    pub client_listener: String,
+    pub mixnet_listener: String,
+    pub pub_key: String,
+    pub registered_clients: Vec<MixProviderClient>,
+    pub last_seen: u64,
+    pub version: String,
+}
+
+impl Into<topology::provider::Node> for MixProviderPresence {
+    fn into(self) -> topology::provider::Node {
+        topology::provider::Node {
+            location: self.location,
+            client_listener: self.client_listener.parse().unwrap(),
+            mixnet_listener: self.mixnet_listener.parse().unwrap(),
+            pub_key: self.pub_key,
+            registered_clients: self
+                .registered_clients
+                .into_iter()
+                .map(|c| c.into())
+                .collect(),
+            last_seen: self.last_seen,
+            version: self.version,
+        }
+    }
+}
+
+impl From<topology::provider::Node> for MixProviderPresence {
+    fn from(mpn: provider::Node) -> Self {
+        MixProviderPresence {
+            location: mpn.location,
+            client_listener: mpn.client_listener.to_string(),
+            mixnet_listener: mpn.mixnet_listener.to_string(),
+            pub_key: mpn.pub_key,
+            registered_clients: mpn
+                .registered_clients
+                .into_iter()
+                .map(|c| c.into())
+                .collect(),
+            last_seen: mpn.last_seen,
+            version: mpn.version,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct MixProviderClient {
+    pub pub_key: String,
+}
+
+impl Into<topology::provider::Client> for MixProviderClient {
+    fn into(self) -> topology::provider::Client {
+        topology::provider::Client {
+            pub_key: self.pub_key,
+        }
+    }
+}
+
+impl From<topology::provider::Client> for MixProviderClient {
+    fn from(mpc: topology::provider::Client) -> Self {
+        MixProviderClient {
+            pub_key: mpc.pub_key,
+        }
+    }
+}
diff --git a/common/client-libs/directory-client/src/lib.rs b/common/client-libs/directory-client/src/lib.rs
index 6f72b3bcd73..be0f49c85aa 100644
--- a/common/client-libs/directory-client/src/lib.rs
+++ b/common/client-libs/directory-client/src/lib.rs
@@ -58,17 +58,13 @@ pub struct Client {
 
 impl DirectoryClient for Client {
     fn new(config: Config) -> Client {
-        let health_check: HealthCheckRequest = HealthCheckRequest::new(config.base_url.clone());
-        let metrics_mixes: MetricsMixRequest = MetricsMixRequest::new(config.base_url.clone());
-        let metrics_post: MetricsMixPost = MetricsMixPost::new(config.base_url.clone());
-        let presence_topology: PresenceTopologyRequest =
-            PresenceTopologyRequest::new(config.base_url.clone());
-        let presence_coconodes_post: PresenceCocoNodesPost =
-            PresenceCocoNodesPost::new(config.base_url.clone());
-        let presence_mix_nodes_post: PresenceMixNodesPost =
-            PresenceMixNodesPost::new(config.base_url.clone());
-        let presence_providers_post: PresenceProvidersPost =
-            PresenceProvidersPost::new(config.base_url);
+        let health_check = HealthCheckRequest::new(config.base_url.clone());
+        let metrics_mixes = MetricsMixRequest::new(config.base_url.clone());
+        let metrics_post = MetricsMixPost::new(config.base_url.clone());
+        let presence_topology = PresenceTopologyRequest::new(config.base_url.clone());
+        let presence_coconodes_post = PresenceCocoNodesPost::new(config.base_url.clone());
+        let presence_mix_nodes_post = PresenceMixNodesPost::new(config.base_url.clone());
+        let presence_providers_post = PresenceProvidersPost::new(config.base_url);
         Client {
             health_check,
             metrics_mixes,