diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 70066bd2241017e30bc4702ec1127cc25fed1688..57081f3ef7c5ae6abcf0cb130aa994d8a6460abc 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -57,6 +57,7 @@ let package = Package(
         .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
         .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
         .product(name: "XXDatabase", package: "client-ios-db"),
+        .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
         .product(name: "XXModels", package: "client-ios-db"),
       ],
       swiftSettings: swiftSettings
diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c6e03cbedb7f1d0020d11a94c6fc0172a5fc5247
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift
@@ -0,0 +1,49 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
+import XXModels
+
+public struct AuthCallbackHandler {
+  public typealias OnError = (Error) -> Void
+
+  public var run: (@escaping OnError) -> Cancellable
+
+  public func callAsFunction(onError: @escaping OnError) -> Cancellable {
+    run(onError)
+  }
+}
+
+extension AuthCallbackHandler {
+  public static func live(
+    messenger: Messenger,
+    handleRequest: AuthCallbackHandlerRequest,
+    handleConfirm: AuthCallbackHandlerConfirm,
+    handleReset: AuthCallbackHandlerReset
+  ) -> AuthCallbackHandler {
+    AuthCallbackHandler { onError in
+      messenger.registerAuthCallbacks(.init { callback in
+        do {
+          switch callback {
+          case .request(let contact, _, _, _):
+            try handleRequest(contact)
+
+          case .confirm(let contact, _, _, _):
+            try handleConfirm(contact)
+
+          case .reset(let contact, _, _, _):
+            try handleReset(contact)
+          }
+        } catch {
+          onError(error)
+        }
+      })
+    }
+  }
+}
+
+extension AuthCallbackHandler {
+  public static let unimplemented = AuthCallbackHandler(
+    run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {})
+  )
+}
diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift
new file mode 100644
index 0000000000000000000000000000000000000000..2aa6787fff741651d649fb8f88397669b448d92f
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift
@@ -0,0 +1,31 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+import XXModels
+
+public struct AuthCallbackHandlerConfirm {
+  public var run: (XXClient.Contact) throws -> Void
+
+  public func callAsFunction(_ contact: XXClient.Contact) throws {
+    try run(contact)
+  }
+}
+
+extension AuthCallbackHandlerConfirm {
+  public static func live(db: DBManagerGetDB) -> AuthCallbackHandlerConfirm {
+    AuthCallbackHandlerConfirm { xxContact in
+      let id = try xxContact.getId()
+      guard var dbContact = try db().fetchContacts(.init(id: [id])).first else {
+        return
+      }
+      dbContact.authStatus = .friend
+      dbContact = try db().saveContact(dbContact)
+    }
+  }
+}
+
+extension AuthCallbackHandlerConfirm {
+  public static let unimplemented = AuthCallbackHandlerConfirm(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift
new file mode 100644
index 0000000000000000000000000000000000000000..1ec239230874c79e3d670bf377c57ab45400895b
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift
@@ -0,0 +1,40 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+import XXModels
+
+public struct AuthCallbackHandlerRequest {
+  public var run: (XXClient.Contact) throws -> Void
+
+  public func callAsFunction(_ contact: XXClient.Contact) throws {
+    try run(contact)
+  }
+}
+
+extension AuthCallbackHandlerRequest {
+  public static func live(
+    db: DBManagerGetDB,
+    now: @escaping () -> Date
+  ) -> AuthCallbackHandlerRequest {
+    AuthCallbackHandlerRequest { xxContact in
+      let id = try xxContact.getId()
+      guard try db().fetchContacts(.init(id: [id])).isEmpty else {
+        return
+      }
+      var dbContact = XXModels.Contact(id: id)
+      dbContact.marshaled = xxContact.data
+      dbContact.username = try xxContact.getFact(.username)?.value
+      dbContact.email = try xxContact.getFact(.email)?.value
+      dbContact.phone = try xxContact.getFact(.phone)?.value
+      dbContact.authStatus = .verificationInProgress
+      dbContact.createdAt = now()
+      dbContact = try db().saveContact(dbContact)
+    }
+  }
+}
+
+extension AuthCallbackHandlerRequest {
+  public static let unimplemented = AuthCallbackHandlerRequest(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift
new file mode 100644
index 0000000000000000000000000000000000000000..729fe13eff8ee42ed1a12ba2ce03d082e5b2cfa8
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift
@@ -0,0 +1,31 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+import XXModels
+
+public struct AuthCallbackHandlerReset {
+  public var run: (XXClient.Contact) throws -> Void
+
+  public func callAsFunction(_ contact: XXClient.Contact) throws {
+    try run(contact)
+  }
+}
+
+extension AuthCallbackHandlerReset {
+  public static func live(db: DBManagerGetDB) -> AuthCallbackHandlerReset {
+    AuthCallbackHandlerReset { xxContact in
+      let id = try xxContact.getId()
+      guard var dbContact = try db().fetchContacts(.init(id: [id])).first else {
+        return
+      }
+      dbContact.authStatus = .stranger
+      dbContact = try db().saveContact(dbContact)
+    }
+  }
+}
+
+extension AuthCallbackHandlerReset {
+  public static let unimplemented = AuthCallbackHandlerReset(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..1a1b5f19573669403a069bd4e3cecd2e11347e73
--- /dev/null
+++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift
@@ -0,0 +1,54 @@
+import CustomDump
+import XCTest
+import XXModels
+import XXClient
+@testable import AppCore
+
+final class AuthCallbackHandlerConfirmTests: XCTestCase {
+  func testConfirm() throws {
+    var didFetchContacts: [XXModels.Contact.Query] = []
+    var didSaveContact: [XXModels.Contact] = []
+
+    let dbContact = XXModels.Contact(
+      id: "id".data(using: .utf8)!,
+      authStatus: .requested
+    )
+    let confirm = AuthCallbackHandlerConfirm.live(
+      db: .init {
+        var db: Database = .failing
+        db.fetchContacts.run = { query in
+          didFetchContacts.append(query)
+          return [dbContact]
+        }
+        db.saveContact.run = { contact in
+          didSaveContact.append(contact)
+          return contact
+        }
+        return db
+      }
+    )
+    var xxContact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!)
+    xxContact.getIdFromContact.run = { _ in "id".data(using: .utf8)! }
+
+    try confirm(xxContact)
+
+    XCTAssertNoDifference(didFetchContacts, [.init(id: ["id".data(using: .utf8)!])])
+    var expectedSavedContact = dbContact
+    expectedSavedContact.authStatus = .friend
+    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])
+  }
+
+  func testConfirmWhenContactNotInDatabase() throws {
+    let confirm = AuthCallbackHandlerConfirm.live(
+      db: .init {
+        var db: Database = .failing
+        db.fetchContacts.run = { _ in [] }
+        return db
+      }
+    )
+    var contact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!)
+    contact.getIdFromContact.run = { _ in "id".data(using: .utf8)! }
+
+    try confirm(contact)
+  }
+}
diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..fb92a5b8871258b61c88f5bfd43bdb53d64208ba
--- /dev/null
+++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift
@@ -0,0 +1,67 @@
+import CustomDump
+import XCTest
+import XXModels
+import XXClient
+import XCTestDynamicOverlay
+@testable import AppCore
+
+final class AuthCallbackHandlerRequestTests: XCTestCase {
+  func testRequestFromNewContact() throws {
+    let now = Date()
+    var didFetchContacts: [XXModels.Contact.Query] = []
+    var didSaveContact: [XXModels.Contact] = []
+
+    let request = AuthCallbackHandlerRequest.live(
+      db: .init {
+        var db: Database = .failing
+        db.fetchContacts.run = { query in
+          didFetchContacts.append(query)
+          return []
+        }
+        db.saveContact.run = { contact in
+          didSaveContact.append(contact)
+          return contact
+        }
+        return db
+      },
+      now: { now }
+    )
+    var xxContact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!)
+    xxContact.getIdFromContact.run = { _ in "id".data(using: .utf8)! }
+    xxContact.getFactsFromContact.run = { _ in
+      [
+        Fact(type: .username, value: "username"),
+        Fact(type: .email, value: "email"),
+        Fact(type: .phone, value: "phone"),
+      ]
+    }
+
+    try request(xxContact)
+
+    XCTAssertNoDifference(didFetchContacts, [.init(id: ["id".data(using: .utf8)!])])
+    XCTAssertNoDifference(didSaveContact, [.init(
+      id: "id".data(using: .utf8)!,
+      marshaled: "contact".data(using: .utf8)!,
+      username: "username",
+      email: "email",
+      phone: "phone",
+      authStatus: .verificationInProgress,
+      createdAt: now
+    )])
+  }
+
+  func testRequestWhenContactInDatabase() throws {
+    let request = AuthCallbackHandlerRequest.live(
+      db: .init {
+        var db: Database = .failing
+        db.fetchContacts.run = { _ in [.init(id: "id".data(using: .utf8)!)] }
+        return db
+      },
+      now: XCTUnimplemented("now", placeholder: Date())
+    )
+    var contact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!)
+    contact.getIdFromContact.run = { _ in "id".data(using: .utf8)! }
+
+    try request(contact)
+  }
+}
diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..9a4407bf758a0c0ba922c154d199b758dea701be
--- /dev/null
+++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift
@@ -0,0 +1,54 @@
+import CustomDump
+import XCTest
+import XXModels
+import XXClient
+@testable import AppCore
+
+final class AuthCallbackHandlerResetTests: XCTestCase {
+  func testReset() throws {
+    var didFetchContacts: [XXModels.Contact.Query] = []
+    var didSaveContact: [XXModels.Contact] = []
+
+    let dbContact = XXModels.Contact(
+      id: "id".data(using: .utf8)!,
+      authStatus: .friend
+    )
+    let reset = AuthCallbackHandlerReset.live(
+      db: .init {
+        var db: Database = .failing
+        db.fetchContacts.run = { query in
+          didFetchContacts.append(query)
+          return [dbContact]
+        }
+        db.saveContact.run = { contact in
+          didSaveContact.append(contact)
+          return contact
+        }
+        return db
+      }
+    )
+    var xxContact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!)
+    xxContact.getIdFromContact.run = { _ in "id".data(using: .utf8)! }
+
+    try reset(xxContact)
+
+    XCTAssertNoDifference(didFetchContacts, [.init(id: ["id".data(using: .utf8)!])])
+    var expectedSavedContact = dbContact
+    expectedSavedContact.authStatus = .stranger
+    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])
+  }
+
+  func testResetWhenContactNotInDatabase() throws {
+    let reset = AuthCallbackHandlerReset.live(
+      db: .init {
+        var db: Database = .failing
+        db.fetchContacts.run = { _ in [] }
+        return db
+      }
+    )
+    var contact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!)
+    contact.getIdFromContact.run = { _ in "id".data(using: .utf8)! }
+
+    try reset(contact)
+  }
+}
diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..2eddbbb9e3b9d7524e6ea0e1e65e481cbe921437
--- /dev/null
+++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerTests.swift
@@ -0,0 +1,114 @@
+import CustomDump
+import XCTest
+import XXClient
+import XXMessengerClient
+@testable import AppCore
+
+final class AuthCallbackHandlerTests: XCTestCase {
+  func testCallbackHandling() throws {
+    struct TestState: Equatable {
+      var didRegisterAuthCallbacks = 0
+      var didCancelAuthCallbacks = 0
+      var didHandleRequest: [Contact] = []
+      var didHandleConfirm: [Contact] = []
+      var didHandleReset: [Contact] = []
+    }
+    var registeredAuthCallbacks: [AuthCallbacks] = []
+    var state = TestState()
+    var expectedState = state
+
+    var messenger: Messenger = .unimplemented
+    messenger.registerAuthCallbacks.run = { callbacks in
+      state.didRegisterAuthCallbacks += 1
+      registeredAuthCallbacks.append(callbacks)
+      return Cancellable { state.didCancelAuthCallbacks += 1 }
+    }
+
+    let handle = AuthCallbackHandler.live(
+      messenger: messenger,
+      handleRequest: .init { state.didHandleRequest.append($0) },
+      handleConfirm: .init { state.didHandleConfirm.append($0) },
+      handleReset: .init { state.didHandleReset.append($0) }
+    )
+
+    var cancellable: Any? = handle(onError: { error in
+      XCTFail("Unexpected error: \(error)")
+    })
+
+    expectedState.didRegisterAuthCallbacks += 1
+    XCTAssertNoDifference(state, expectedState)
+
+    let contact1 = XXClient.Contact.unimplemented("1".data(using: .utf8)!)
+    registeredAuthCallbacks.first?.handle(
+      .request(contact: contact1, receptionId: Data(), ephemeralId: 0, roundId: 0)
+    )
+
+    expectedState.didHandleRequest.append(contact1)
+    XCTAssertNoDifference(state, expectedState)
+
+    let contact2 = XXClient.Contact.unimplemented("2".data(using: .utf8)!)
+    registeredAuthCallbacks.first?.handle(
+      .confirm(contact: contact2, receptionId: Data(), ephemeralId: 0, roundId: 0)
+    )
+
+    expectedState.didHandleConfirm.append(contact2)
+    XCTAssertNoDifference(state, expectedState)
+
+    let contact3 = XXClient.Contact.unimplemented("3".data(using: .utf8)!)
+    registeredAuthCallbacks.first?.handle(
+      .reset(contact: contact3, receptionId: Data(), ephemeralId: 0, roundId: 0)
+    )
+
+    expectedState.didHandleReset.append(contact3)
+    XCTAssertNoDifference(state, expectedState)
+
+    cancellable = nil
+
+    expectedState.didCancelAuthCallbacks += 1
+    XCTAssertNoDifference(state, expectedState)
+
+    _ = cancellable
+  }
+
+  func testCallbackHandlingFailure() {
+    enum Failure: Error, Equatable {
+      case request
+      case confirm
+      case reset
+    }
+    var registeredAuthCallbacks: [AuthCallbacks] = []
+    var errors: [Error] = []
+
+    var messenger: Messenger = .unimplemented
+    messenger.registerAuthCallbacks.run = { callbacks in
+      registeredAuthCallbacks.append(callbacks)
+      return Cancellable {}
+    }
+
+    let handle = AuthCallbackHandler.live(
+      messenger: messenger,
+      handleRequest: .init { _ in throw Failure.request },
+      handleConfirm: .init { _ in throw Failure.confirm },
+      handleReset: .init { _ in throw Failure.reset }
+    )
+
+    let cancellable = handle(onError: { errors.append($0) })
+
+    registeredAuthCallbacks.first?.handle(
+      .request(contact: .unimplemented(Data()), receptionId: Data(), ephemeralId: 0, roundId: 0)
+    )
+    registeredAuthCallbacks.first?.handle(
+      .confirm(contact: .unimplemented(Data()), receptionId: Data(), ephemeralId: 0, roundId: 0)
+    )
+    registeredAuthCallbacks.first?.handle(
+      .reset(contact: .unimplemented(Data()), receptionId: Data(), ephemeralId: 0, roundId: 0)
+    )
+
+    XCTAssertNoDifference(
+      errors.map { $0 as? Failure },
+      [.request, .confirm, .reset]
+    )
+
+    _ = cancellable
+  }
+}