diff --git a/Frameworks/Bindings.txt b/Frameworks/Bindings.txt
index 83a8da383de3a57509257d2b3b05af2c65d64fe1..4b34da083d45ceb5ab713f175f5c86b44a7cb92d 100644
--- a/Frameworks/Bindings.txt
+++ b/Frameworks/Bindings.txt
@@ -1,4 +1,4 @@
-https://git.xx.network/elixxir/client/-/commit/57b7e3b88e740c36935ab2da04d94fd5a6a85262
+https://git.xx.network/elixxir/client/-/commit/2bf3c3bf5b2fdeb01120cb47fd1d698e80d64e26
 go version go1.17.13 darwin/arm64
 Xcode 13.4.1 Build version 13F100
 gomobile bind target: ios,iossimulator,macos
diff --git a/Frameworks/Bindings.xcframework/Info.plist b/Frameworks/Bindings.xcframework/Info.plist
index 43fe32dad94767ae305b61db56f5604f8612617e..3d81013ba0ba606da4b42b6f3119f688c6d9b97d 100644
--- a/Frameworks/Bindings.xcframework/Info.plist
+++ b/Frameworks/Bindings.xcframework/Info.plist
@@ -6,7 +6,7 @@
 	<array>
 		<dict>
 			<key>LibraryIdentifier</key>
-			<string>macos-arm64_x86_64</string>
+			<string>ios-arm64_x86_64-simulator</string>
 			<key>LibraryPath</key>
 			<string>Bindings.framework</string>
 			<key>SupportedArchitectures</key>
@@ -15,11 +15,13 @@
 				<string>x86_64</string>
 			</array>
 			<key>SupportedPlatform</key>
-			<string>macos</string>
+			<string>ios</string>
+			<key>SupportedPlatformVariant</key>
+			<string>simulator</string>
 		</dict>
 		<dict>
 			<key>LibraryIdentifier</key>
-			<string>ios-arm64_x86_64-simulator</string>
+			<string>macos-arm64_x86_64</string>
 			<key>LibraryPath</key>
 			<string>Bindings.framework</string>
 			<key>SupportedArchitectures</key>
@@ -28,9 +30,7 @@
 				<string>x86_64</string>
 			</array>
 			<key>SupportedPlatform</key>
-			<string>ios</string>
-			<key>SupportedPlatformVariant</key>
-			<string>simulator</string>
+			<string>macos</string>
 		</dict>
 		<dict>
 			<key>LibraryIdentifier</key>
diff --git a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings
index 381c2ba28c87178ac5f80950ba6439f8b4bfa8f1..28ce31cafb362d9d42caec0122fdc8bdb733cc0e 100644
Binary files a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings and b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings differ
diff --git a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
index 313101ebfb224eb77a7b3e3b99459fab55b79c3c..5d794b630d29654c2d7f48ca38792a9745cb0ddc 100644
--- a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
+++ b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
@@ -525,29 +525,28 @@ These threads may become a significant drain on battery when offline, ensure
 they are stopped if there is no internet access.
 
 Threads Started:
-  - Network Follower (/network/follow.go)
-    tracks the network events and hands them off to workers for handling.
-  - Historical Round Retrieval (/network/rounds/historical.go)
-    retrieves data about rounds that are too old to be stored by the client.
-	 - Message Retrieval Worker Group (/network/rounds/retrieve.go)
-	   requests all messages in a given round from the gateway of the last
-	   nodes.
-	 - Message Handling Worker Group (/network/message/handle.go)
-	   decrypts and partitions messages when signals via the Switchboard.
-	 - Health Tracker (/network/health),
-	   via the network instance, tracks the state of the network.
-	 - Garbled Messages (/network/message/garbled.go)
-	   can be signaled to check all recent messages that could be decoded. It
-	   uses a message store on disk for persistence.
-	 - Critical Messages (/network/message/critical.go)
-	   ensures all protocol layer mandatory messages are sent. It uses a message
-	   store on disk for persistence.
-	 - KeyExchange Trigger (/keyExchange/trigger.go)
-	   responds to sent rekeys and executes them.
-  - KeyExchange Confirm (/keyExchange/confirm.go)
-	   responds to confirmations of successful rekey operations.
-  - Auth Callback (/auth/callback.go)
-    handles both auth confirm and requests.
+ - Network Follower (/network/follow.go)
+   tracks the network events and hands them off to workers for handling.
+ - Historical Round Retrieval (/network/rounds/historical.go)
+   retrieves data about rounds that are too old to be stored by the client.
+ - Message Retrieval Worker Group (/network/rounds/retrieve.go)
+	  requests all messages in a given round from the gateway of the last nodes.
+ - Message Handling Worker Group (/network/message/handle.go)
+	  decrypts and partitions messages when signals via the Switchboard.
+	- Health Tracker (/network/health),
+	  via the network instance, tracks the state of the network.
+ - Garbled Messages (/network/message/garbled.go)
+	  can be signaled to check all recent messages that could be decoded. It
+	  uses a message store on disk for persistence.
+	- Critical Messages (/network/message/critical.go)
+	  ensures all protocol layer mandatory messages are sent. It uses a message
+	  store on disk for persistence.
+	- KeyExchange Trigger (/keyExchange/trigger.go)
+	  responds to sent rekeys and executes them.
+ - KeyExchange Confirm (/keyExchange/confirm.go)
+	  responds to confirmations of successful rekey operations.
+ - Auth Callback (/auth/callback.go)
+   handles both auth confirm and requests.
  */
 - (BOOL)startNetworkFollower:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -566,8 +565,8 @@ backend keeps track of, which is formally referred to as a
 may need context on the available services for this client.
 
 Parameters:
-  - cb - A TrackServicesCallback, which will be passed the marshalled
-    message.ServiceList.
+ - cb - A TrackServicesCallback, which will be passed the marshalled
+   message.ServiceList.
  */
 - (void)trackServices:(id<BindingsTrackServicesCallback> _Nullable)cb;
 /**
@@ -1429,30 +1428,27 @@ Cmix.GetNodeRegistrationStatus returns JSON marshalled.
 this user.
 
 Example NotificationReport JSON:
-
-{
- "ForMe": true,
- "Type": "e2e",
- "Source": "dGVzdGVyMTIzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
-}
+ {
+   "ForMe": true,
+   "Type": "e2e",
+   "Source": "dGVzdGVyMTIzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ }
 
 Given the Type, the Source value will have specific contextual meanings.
 Below is a table that will define the contextual meaning of the Source field
 given all possible Type fields.
 
-     TYPE     |     SOURCE         |    DESCRIPTION
-    ________________________________________________________________________________________
-    "default" |  recipient user ID |  A message with no association.
-	   "request" |  sender user ID    |  A channel request has been received, from Source.
-    "reset"   |  sender user ID    |  A channel reset has been received.
-    "confirm" |  sender user ID    |  A channel request has been accepted.
-    "silent"  |  sender user ID    |  A message where the user should not be notified.
-    "e2e"     |  sender user ID    |  A reception of an E2E message.
-    "group"   |  group ID          |  A reception of a group chat message.
-    "endFT"   |  sender user ID    |  The last message sent confirming end of file transfer.
-    "groupRQ" |  sender user ID    |  A request from Source to join a group chat.
- todo iterate over this docstring, ensure descriptions/sources are
-   still accurate (they are from the old implementation
+  TYPE     |     SOURCE         |    DESCRIPTION
+ ----------+--------------------+--------------------------------------------------------
+ "default" |  recipient user ID |  A message with no association.
+ "request" |  sender user ID    |  A channel request has been received, from Source.
+ "reset"   |  sender user ID    |  A channel reset has been received.
+ "confirm" |  sender user ID    |  A channel request has been accepted.
+ "silent"  |  sender user ID    |  A message where the user should not be notified.
+ "e2e"     |  sender user ID    |  A reception of an E2E message.
+ "group"   |  group ID          |  A reception of a group chat message.
+ "endFT"   |  sender user ID    |  The last message sent confirming end of file transfer.
+ "groupRQ" |  sender user ID    |  A request from Source to join a group chat.
  */
 @interface BindingsNotificationReport : NSObject <goSeqRefInterface> {
 }
@@ -2655,35 +2651,39 @@ registered listener.
 @end
 
 /**
- * TrackServicesCallback is the callback for Cmix.TrackServices.
+ * TrackServicesCallback is the callback for [Cmix.TrackServices].
 This will pass to the user a JSON-marshalled list of backend services.
 If there was an error retrieving or marshalling the service list,
 there is an error for the second parameter which will be non-null.
 
-Example JSON:
+Parameters:
+ - marshalData - JSON marshalled bytes of [message.ServiceList], which is an
+   array of [id.ID] and [message.Service].
+ - err - JSON unmarshalling error
 
-[
- {
-   "Id": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
-   "Services": [
-     {
-       "Identifier": null,
-       "Tag": "test",
-       "Metadata": null
-     }
-   ]
- },
- {
-   "Id": "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
-   "Services": [
-     {
-       "Identifier": null,
-       "Tag": "test",
-       "Metadata": null
-     }
-   ]
- },
-]
+Example JSON:
+ [
+   {
+     "Id": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD", // bytes of id.ID encoded as base64 string
+     "Services": [
+       {
+         "Identifier": "AQID",                             // bytes encoded as base64 string
+         "Tag": "TestTag 1",                               // string
+         "Metadata": "BAUG"                                // bytes encoded as base64 string
+       }
+     ]
+   },
+   {
+     "Id": "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+     "Services": [
+       {
+         "Identifier": "AQID",
+         "Tag": "TestTag 2",
+         "Metadata": "BAUG"
+       }
+     ]
+   },
+ ]
  */
 @interface BindingsTrackServicesCallback : NSObject <goSeqRefInterface, BindingsTrackServicesCallback> {
 }
diff --git a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings
index 1005ae67edb1b57deb38ed23b9639767d796f5ab..9f860b583c263a55e38bf9334bc0acf71597d748 100644
Binary files a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings and b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings differ
diff --git a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h
index 313101ebfb224eb77a7b3e3b99459fab55b79c3c..5d794b630d29654c2d7f48ca38792a9745cb0ddc 100644
--- a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h
+++ b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h
@@ -525,29 +525,28 @@ These threads may become a significant drain on battery when offline, ensure
 they are stopped if there is no internet access.
 
 Threads Started:
-  - Network Follower (/network/follow.go)
-    tracks the network events and hands them off to workers for handling.
-  - Historical Round Retrieval (/network/rounds/historical.go)
-    retrieves data about rounds that are too old to be stored by the client.
-	 - Message Retrieval Worker Group (/network/rounds/retrieve.go)
-	   requests all messages in a given round from the gateway of the last
-	   nodes.
-	 - Message Handling Worker Group (/network/message/handle.go)
-	   decrypts and partitions messages when signals via the Switchboard.
-	 - Health Tracker (/network/health),
-	   via the network instance, tracks the state of the network.
-	 - Garbled Messages (/network/message/garbled.go)
-	   can be signaled to check all recent messages that could be decoded. It
-	   uses a message store on disk for persistence.
-	 - Critical Messages (/network/message/critical.go)
-	   ensures all protocol layer mandatory messages are sent. It uses a message
-	   store on disk for persistence.
-	 - KeyExchange Trigger (/keyExchange/trigger.go)
-	   responds to sent rekeys and executes them.
-  - KeyExchange Confirm (/keyExchange/confirm.go)
-	   responds to confirmations of successful rekey operations.
-  - Auth Callback (/auth/callback.go)
-    handles both auth confirm and requests.
+ - Network Follower (/network/follow.go)
+   tracks the network events and hands them off to workers for handling.
+ - Historical Round Retrieval (/network/rounds/historical.go)
+   retrieves data about rounds that are too old to be stored by the client.
+ - Message Retrieval Worker Group (/network/rounds/retrieve.go)
+	  requests all messages in a given round from the gateway of the last nodes.
+ - Message Handling Worker Group (/network/message/handle.go)
+	  decrypts and partitions messages when signals via the Switchboard.
+	- Health Tracker (/network/health),
+	  via the network instance, tracks the state of the network.
+ - Garbled Messages (/network/message/garbled.go)
+	  can be signaled to check all recent messages that could be decoded. It
+	  uses a message store on disk for persistence.
+	- Critical Messages (/network/message/critical.go)
+	  ensures all protocol layer mandatory messages are sent. It uses a message
+	  store on disk for persistence.
+	- KeyExchange Trigger (/keyExchange/trigger.go)
+	  responds to sent rekeys and executes them.
+ - KeyExchange Confirm (/keyExchange/confirm.go)
+	  responds to confirmations of successful rekey operations.
+ - Auth Callback (/auth/callback.go)
+   handles both auth confirm and requests.
  */
 - (BOOL)startNetworkFollower:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -566,8 +565,8 @@ backend keeps track of, which is formally referred to as a
 may need context on the available services for this client.
 
 Parameters:
-  - cb - A TrackServicesCallback, which will be passed the marshalled
-    message.ServiceList.
+ - cb - A TrackServicesCallback, which will be passed the marshalled
+   message.ServiceList.
  */
 - (void)trackServices:(id<BindingsTrackServicesCallback> _Nullable)cb;
 /**
@@ -1429,30 +1428,27 @@ Cmix.GetNodeRegistrationStatus returns JSON marshalled.
 this user.
 
 Example NotificationReport JSON:
-
-{
- "ForMe": true,
- "Type": "e2e",
- "Source": "dGVzdGVyMTIzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
-}
+ {
+   "ForMe": true,
+   "Type": "e2e",
+   "Source": "dGVzdGVyMTIzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ }
 
 Given the Type, the Source value will have specific contextual meanings.
 Below is a table that will define the contextual meaning of the Source field
 given all possible Type fields.
 
-     TYPE     |     SOURCE         |    DESCRIPTION
-    ________________________________________________________________________________________
-    "default" |  recipient user ID |  A message with no association.
-	   "request" |  sender user ID    |  A channel request has been received, from Source.
-    "reset"   |  sender user ID    |  A channel reset has been received.
-    "confirm" |  sender user ID    |  A channel request has been accepted.
-    "silent"  |  sender user ID    |  A message where the user should not be notified.
-    "e2e"     |  sender user ID    |  A reception of an E2E message.
-    "group"   |  group ID          |  A reception of a group chat message.
-    "endFT"   |  sender user ID    |  The last message sent confirming end of file transfer.
-    "groupRQ" |  sender user ID    |  A request from Source to join a group chat.
- todo iterate over this docstring, ensure descriptions/sources are
-   still accurate (they are from the old implementation
+  TYPE     |     SOURCE         |    DESCRIPTION
+ ----------+--------------------+--------------------------------------------------------
+ "default" |  recipient user ID |  A message with no association.
+ "request" |  sender user ID    |  A channel request has been received, from Source.
+ "reset"   |  sender user ID    |  A channel reset has been received.
+ "confirm" |  sender user ID    |  A channel request has been accepted.
+ "silent"  |  sender user ID    |  A message where the user should not be notified.
+ "e2e"     |  sender user ID    |  A reception of an E2E message.
+ "group"   |  group ID          |  A reception of a group chat message.
+ "endFT"   |  sender user ID    |  The last message sent confirming end of file transfer.
+ "groupRQ" |  sender user ID    |  A request from Source to join a group chat.
  */
 @interface BindingsNotificationReport : NSObject <goSeqRefInterface> {
 }
@@ -2655,35 +2651,39 @@ registered listener.
 @end
 
 /**
- * TrackServicesCallback is the callback for Cmix.TrackServices.
+ * TrackServicesCallback is the callback for [Cmix.TrackServices].
 This will pass to the user a JSON-marshalled list of backend services.
 If there was an error retrieving or marshalling the service list,
 there is an error for the second parameter which will be non-null.
 
-Example JSON:
+Parameters:
+ - marshalData - JSON marshalled bytes of [message.ServiceList], which is an
+   array of [id.ID] and [message.Service].
+ - err - JSON unmarshalling error
 
-[
- {
-   "Id": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
-   "Services": [
-     {
-       "Identifier": null,
-       "Tag": "test",
-       "Metadata": null
-     }
-   ]
- },
- {
-   "Id": "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
-   "Services": [
-     {
-       "Identifier": null,
-       "Tag": "test",
-       "Metadata": null
-     }
-   ]
- },
-]
+Example JSON:
+ [
+   {
+     "Id": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD", // bytes of id.ID encoded as base64 string
+     "Services": [
+       {
+         "Identifier": "AQID",                             // bytes encoded as base64 string
+         "Tag": "TestTag 1",                               // string
+         "Metadata": "BAUG"                                // bytes encoded as base64 string
+       }
+     ]
+   },
+   {
+     "Id": "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+     "Services": [
+       {
+         "Identifier": "AQID",
+         "Tag": "TestTag 2",
+         "Metadata": "BAUG"
+       }
+     ]
+   },
+ ]
  */
 @interface BindingsTrackServicesCallback : NSObject <goSeqRefInterface, BindingsTrackServicesCallback> {
 }
diff --git a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings
index 2b925090d31ec9d29ff598919efa963e385b8603..216e196c2585dcb12fddaf726eced003f87f6460 100644
Binary files a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings and b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings differ
diff --git a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
index 313101ebfb224eb77a7b3e3b99459fab55b79c3c..5d794b630d29654c2d7f48ca38792a9745cb0ddc 100644
--- a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
+++ b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
@@ -525,29 +525,28 @@ These threads may become a significant drain on battery when offline, ensure
 they are stopped if there is no internet access.
 
 Threads Started:
-  - Network Follower (/network/follow.go)
-    tracks the network events and hands them off to workers for handling.
-  - Historical Round Retrieval (/network/rounds/historical.go)
-    retrieves data about rounds that are too old to be stored by the client.
-	 - Message Retrieval Worker Group (/network/rounds/retrieve.go)
-	   requests all messages in a given round from the gateway of the last
-	   nodes.
-	 - Message Handling Worker Group (/network/message/handle.go)
-	   decrypts and partitions messages when signals via the Switchboard.
-	 - Health Tracker (/network/health),
-	   via the network instance, tracks the state of the network.
-	 - Garbled Messages (/network/message/garbled.go)
-	   can be signaled to check all recent messages that could be decoded. It
-	   uses a message store on disk for persistence.
-	 - Critical Messages (/network/message/critical.go)
-	   ensures all protocol layer mandatory messages are sent. It uses a message
-	   store on disk for persistence.
-	 - KeyExchange Trigger (/keyExchange/trigger.go)
-	   responds to sent rekeys and executes them.
-  - KeyExchange Confirm (/keyExchange/confirm.go)
-	   responds to confirmations of successful rekey operations.
-  - Auth Callback (/auth/callback.go)
-    handles both auth confirm and requests.
+ - Network Follower (/network/follow.go)
+   tracks the network events and hands them off to workers for handling.
+ - Historical Round Retrieval (/network/rounds/historical.go)
+   retrieves data about rounds that are too old to be stored by the client.
+ - Message Retrieval Worker Group (/network/rounds/retrieve.go)
+	  requests all messages in a given round from the gateway of the last nodes.
+ - Message Handling Worker Group (/network/message/handle.go)
+	  decrypts and partitions messages when signals via the Switchboard.
+	- Health Tracker (/network/health),
+	  via the network instance, tracks the state of the network.
+ - Garbled Messages (/network/message/garbled.go)
+	  can be signaled to check all recent messages that could be decoded. It
+	  uses a message store on disk for persistence.
+	- Critical Messages (/network/message/critical.go)
+	  ensures all protocol layer mandatory messages are sent. It uses a message
+	  store on disk for persistence.
+	- KeyExchange Trigger (/keyExchange/trigger.go)
+	  responds to sent rekeys and executes them.
+ - KeyExchange Confirm (/keyExchange/confirm.go)
+	  responds to confirmations of successful rekey operations.
+ - Auth Callback (/auth/callback.go)
+   handles both auth confirm and requests.
  */
 - (BOOL)startNetworkFollower:(long)timeoutMS error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -566,8 +565,8 @@ backend keeps track of, which is formally referred to as a
 may need context on the available services for this client.
 
 Parameters:
-  - cb - A TrackServicesCallback, which will be passed the marshalled
-    message.ServiceList.
+ - cb - A TrackServicesCallback, which will be passed the marshalled
+   message.ServiceList.
  */
 - (void)trackServices:(id<BindingsTrackServicesCallback> _Nullable)cb;
 /**
@@ -1429,30 +1428,27 @@ Cmix.GetNodeRegistrationStatus returns JSON marshalled.
 this user.
 
 Example NotificationReport JSON:
-
-{
- "ForMe": true,
- "Type": "e2e",
- "Source": "dGVzdGVyMTIzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
-}
+ {
+   "ForMe": true,
+   "Type": "e2e",
+   "Source": "dGVzdGVyMTIzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ }
 
 Given the Type, the Source value will have specific contextual meanings.
 Below is a table that will define the contextual meaning of the Source field
 given all possible Type fields.
 
-     TYPE     |     SOURCE         |    DESCRIPTION
-    ________________________________________________________________________________________
-    "default" |  recipient user ID |  A message with no association.
-	   "request" |  sender user ID    |  A channel request has been received, from Source.
-    "reset"   |  sender user ID    |  A channel reset has been received.
-    "confirm" |  sender user ID    |  A channel request has been accepted.
-    "silent"  |  sender user ID    |  A message where the user should not be notified.
-    "e2e"     |  sender user ID    |  A reception of an E2E message.
-    "group"   |  group ID          |  A reception of a group chat message.
-    "endFT"   |  sender user ID    |  The last message sent confirming end of file transfer.
-    "groupRQ" |  sender user ID    |  A request from Source to join a group chat.
- todo iterate over this docstring, ensure descriptions/sources are
-   still accurate (they are from the old implementation
+  TYPE     |     SOURCE         |    DESCRIPTION
+ ----------+--------------------+--------------------------------------------------------
+ "default" |  recipient user ID |  A message with no association.
+ "request" |  sender user ID    |  A channel request has been received, from Source.
+ "reset"   |  sender user ID    |  A channel reset has been received.
+ "confirm" |  sender user ID    |  A channel request has been accepted.
+ "silent"  |  sender user ID    |  A message where the user should not be notified.
+ "e2e"     |  sender user ID    |  A reception of an E2E message.
+ "group"   |  group ID          |  A reception of a group chat message.
+ "endFT"   |  sender user ID    |  The last message sent confirming end of file transfer.
+ "groupRQ" |  sender user ID    |  A request from Source to join a group chat.
  */
 @interface BindingsNotificationReport : NSObject <goSeqRefInterface> {
 }
@@ -2655,35 +2651,39 @@ registered listener.
 @end
 
 /**
- * TrackServicesCallback is the callback for Cmix.TrackServices.
+ * TrackServicesCallback is the callback for [Cmix.TrackServices].
 This will pass to the user a JSON-marshalled list of backend services.
 If there was an error retrieving or marshalling the service list,
 there is an error for the second parameter which will be non-null.
 
-Example JSON:
+Parameters:
+ - marshalData - JSON marshalled bytes of [message.ServiceList], which is an
+   array of [id.ID] and [message.Service].
+ - err - JSON unmarshalling error
 
-[
- {
-   "Id": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
-   "Services": [
-     {
-       "Identifier": null,
-       "Tag": "test",
-       "Metadata": null
-     }
-   ]
- },
- {
-   "Id": "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
-   "Services": [
-     {
-       "Identifier": null,
-       "Tag": "test",
-       "Metadata": null
-     }
-   ]
- },
-]
+Example JSON:
+ [
+   {
+     "Id": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD", // bytes of id.ID encoded as base64 string
+     "Services": [
+       {
+         "Identifier": "AQID",                             // bytes encoded as base64 string
+         "Tag": "TestTag 1",                               // string
+         "Metadata": "BAUG"                                // bytes encoded as base64 string
+       }
+     ]
+   },
+   {
+     "Id": "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+     "Services": [
+       {
+         "Identifier": "AQID",
+         "Tag": "TestTag 2",
+         "Metadata": "BAUG"
+       }
+     ]
+   },
+ ]
  */
 @interface BindingsTrackServicesCallback : NSObject <goSeqRefInterface, BindingsTrackServicesCallback> {
 }
diff --git a/Sources/XXClient/Callbacks/TrackServicesCallback.swift b/Sources/XXClient/Callbacks/TrackServicesCallback.swift
index df3043bb2a90252e85dc8181fcd3e7f5d6c6265a..c84a996b0ce48c2b8eb045b386a209c1f9244c8e 100644
--- a/Sources/XXClient/Callbacks/TrackServicesCallback.swift
+++ b/Sources/XXClient/Callbacks/TrackServicesCallback.swift
@@ -2,11 +2,11 @@ import Bindings
 import XCTestDynamicOverlay
 
 public struct TrackServicesCallback {
-  public init(handle: @escaping (Result<Data, Error>) -> Void) {
+  public init(handle: @escaping (Result<MessageServiceList, Error>) -> Void) {
     self.handle = handle
   }
 
-  public var handle: (Result<Data, Error>) -> Void
+  public var handle: (Result<MessageServiceList, Error>) -> Void
 }
 
 extension TrackServicesCallback {
@@ -30,7 +30,11 @@ extension TrackServicesCallback {
           return
         }
         if let marshalData = marshalData {
-          callback.handle(.success(marshalData))
+          do {
+            callback.handle(.success(try MessageServiceList.decode(marshalData)))
+          } catch {
+            callback.handle(.failure(error))
+          }
           return
         }
         fatalError("BindingsTrackServicesCallback received nil marshalData and err")
diff --git a/Sources/XXClient/Models/MessageService.swift b/Sources/XXClient/Models/MessageService.swift
new file mode 100644
index 0000000000000000000000000000000000000000..583225e0cbcedd9a9aad3a75782521e65858a95b
--- /dev/null
+++ b/Sources/XXClient/Models/MessageService.swift
@@ -0,0 +1,43 @@
+import Foundation
+
+public struct MessageService: Equatable {
+  public init(
+    identifier: Data,
+    tag: String,
+    metadata: Data
+  ) {
+    self.identifier = identifier
+    self.tag = tag
+    self.metadata = metadata
+  }
+
+  public var identifier: Data
+  public var tag: String
+  public var metadata: Data
+}
+
+extension MessageService: Codable {
+  enum CodingKeys: String, CodingKey {
+    case identifier = "Identifier"
+    case tag = "Tag"
+    case metadata = "Metadata"
+  }
+
+  public static func decode(_ data: Data) throws -> Self {
+    try JSONDecoder().decode(Self.self, from: data)
+  }
+
+  public func encode() throws -> Data {
+    try JSONEncoder().encode(self)
+  }
+}
+
+extension Array where Element == MessageService {
+  public static func decode(_ data: Data) throws -> Self {
+    try JSONDecoder().decode(Self.self, from: data)
+  }
+
+  public func encode() throws -> Data {
+    try JSONEncoder().encode(self)
+  }
+}
diff --git a/Sources/XXClient/Models/MessageServiceList.swift b/Sources/XXClient/Models/MessageServiceList.swift
new file mode 100644
index 0000000000000000000000000000000000000000..47e83d56b2c7b00c77e65ebb5da22c3a20ac5401
--- /dev/null
+++ b/Sources/XXClient/Models/MessageServiceList.swift
@@ -0,0 +1,38 @@
+import Foundation
+
+public typealias MessageServiceList = Array<MessageServiceListElement>
+
+extension MessageServiceList {
+  public static func decode(_ data: Data) throws -> Self {
+    try JSONDecoder().decode(Self.self, from: data)
+  }
+
+  public func encode() throws -> Data {
+    try JSONEncoder().encode(self)
+  }
+}
+
+public struct MessageServiceListElement: Equatable {
+  public init(id: Data, services: [MessageService]) {
+    self.id = id
+    self.services = services
+  }
+
+  public var id: Data
+  public var services: [MessageService]
+}
+
+extension MessageServiceListElement: Codable {
+  enum CodingKeys: String, CodingKey {
+    case id = "Id"
+    case services = "Services"
+  }
+
+  public static func decode(_ data: Data) throws -> Self {
+    try JSONDecoder().decode(Self.self, from: data)
+  }
+
+  public func encode() throws -> Data {
+    try JSONEncoder().encode(self)
+  }
+}
diff --git a/Tests/XXClientTests/Models/MessageServiceListTests.swift b/Tests/XXClientTests/Models/MessageServiceListTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..027344343aa3f66137c6258f4e81e4a51d0dd3a0
--- /dev/null
+++ b/Tests/XXClientTests/Models/MessageServiceListTests.swift
@@ -0,0 +1,77 @@
+import CustomDump
+import XCTest
+@testable import XXClient
+
+final class MessageServiceListTests: XCTestCase {
+  func testCoding() throws {
+    let model: MessageServiceList = [
+      MessageServiceListElement(
+        id: "id1".data(using: .utf8)!,
+        services: [
+          MessageService(
+            identifier: "service1-id".data(using: .utf8)!,
+            tag: "service1-tag",
+            metadata: "service1-metadata".data(using: .utf8)!
+          ),
+        ]
+      ),
+      MessageServiceListElement(
+        id: "id2".data(using: .utf8)!,
+        services: [
+          MessageService(
+            identifier: "service2-id".data(using: .utf8)!,
+            tag: "service2-tag",
+            metadata: "service2-metadata".data(using: .utf8)!
+          ),
+          MessageService(
+            identifier: "service3-id".data(using: .utf8)!,
+            tag: "service3-tag",
+            metadata: "service3-metadata".data(using: .utf8)!
+          ),
+        ]
+      ),
+    ]
+
+    let encodedModels = try model.encode()
+    let decodedModels = try MessageServiceList.decode(encodedModels)
+
+    XCTAssertNoDifference(model, decodedModels)
+  }
+
+  func testElementCoding() throws {
+    let idB64 = "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD"
+    let serviceIdentifierB64 = "AQID"
+    let serviceTag = "TestTag 2"
+    let serviceMetadataB64 = "BAUG"
+    let jsonString = """
+    {
+      "Id": "\(idB64)",
+      "Services": [
+        {
+          "Identifier": "\(serviceIdentifierB64)",
+          "Tag": "\(serviceTag)",
+          "Metadata": "\(serviceMetadataB64)"
+        }
+      ]
+    }
+    """
+    let jsonData = jsonString.data(using: .utf8)!
+    let model = try MessageServiceListElement.decode(jsonData)
+
+    XCTAssertNoDifference(model, MessageServiceListElement(
+      id: Data(base64Encoded: idB64)!,
+      services: [
+        MessageService(
+          identifier: Data(base64Encoded: serviceIdentifierB64)!,
+          tag: serviceTag,
+          metadata: Data(base64Encoded: serviceMetadataB64)!
+        )
+      ]
+    ))
+
+    let encodedModel = try model.encode()
+    let decodedModel = try MessageServiceListElement.decode(encodedModel)
+
+    XCTAssertNoDifference(decodedModel, model)
+  }
+}
diff --git a/Tests/XXClientTests/Models/MessageServiceTests.swift b/Tests/XXClientTests/Models/MessageServiceTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..e0a0d9bad5bca5fbd1418886a4c5709c9b196f21
--- /dev/null
+++ b/Tests/XXClientTests/Models/MessageServiceTests.swift
@@ -0,0 +1,56 @@
+import CustomDump
+import XCTest
+@testable import XXClient
+
+final class MessageServiceTests: XCTestCase {
+  func testCoding() throws {
+    let identifierB64 = "AQID"
+    let tag = "TestTag 2"
+    let metadataB64 = "BAUG"
+    let jsonString = """
+    {
+     "Identifier": "\(identifierB64)",
+     "Tag": "\(tag)",
+     "Metadata": "\(metadataB64)"
+    }
+    """
+    let jsonData = jsonString.data(using: .utf8)!
+    let model = try MessageService.decode(jsonData)
+
+    XCTAssertNoDifference(model, MessageService(
+      identifier: Data(base64Encoded: identifierB64)!,
+      tag: tag,
+      metadata: Data(base64Encoded: metadataB64)!
+    ))
+
+    let encodedModel = try model.encode()
+    let decodedModel = try MessageService.decode(encodedModel)
+
+    XCTAssertNoDifference(decodedModel, model)
+  }
+
+  func testCodingArray() throws {
+    let models = [
+      MessageService(
+        identifier: "service1-id".data(using: .utf8)!,
+        tag: "service1-tag",
+        metadata: "service1-metadata".data(using: .utf8)!
+      ),
+      MessageService(
+        identifier: "service2-id".data(using: .utf8)!,
+        tag: "service2-tag",
+        metadata: "service2-metadata".data(using: .utf8)!
+      ),
+      MessageService(
+        identifier: "service3-id".data(using: .utf8)!,
+        tag: "service3-tag",
+        metadata: "service3-metadata".data(using: .utf8)!
+      ),
+    ]
+
+    let encodedModels = try models.encode()
+    let decodedModels = try [MessageService].decode(encodedModels)
+
+    XCTAssertNoDifference(models, decodedModels)
+  }
+}