diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt b/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt
index fe064249ca0cb45a700937fc6f4ddd7dfcd34b52..dc5137de2d4a0e87ad0bb337a396381078dc7e0e 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt
@@ -29,7 +29,7 @@ object Mocks {
         coEvery { publishPreKeys(any()) } returns Unit
         coEvery { publishPreKeys(any(), any()) } returns Unit
         coEvery { refreshPreKeysIfDepleted(any()) } returns Unit
-        coEvery { getUnidentifiedAccessPair(any(), any()) } returns mockk()
+        coEvery { getUnidentifiedAccessPair(any(), any()) } returns null
     }
     val accountStore: AccountStore.() -> Unit = {
         coEvery { save(any<NewAccount>()) } returns Unit
@@ -40,9 +40,13 @@ object Mocks {
         every { closeQuietly() } returns Unit
     }
     val contactStore: ContactStore.() -> Unit = {
+        coEvery { create(any(), any()) } returns 0
+        coEvery { create(any(), any(), any(), any()) } returns 0
         coEvery { createOwnContact(any()) } returns 0
-        coEvery { storeProfileKey(any(), any(), any())} returns Unit
+        coEvery { hasContact(any(), any()) } returns true
         coEvery { loadProfileKey(any(), any())} returns mockk()
+        coEvery { storeMissingIdentifier(any(), any(), any())} returns Unit
+        coEvery { storeProfileKey(any(), any(), any())} returns Unit
     }
     val protocolStore: ProtocolStore.() -> Unit = {
         every { of(any()) } returns mockk {
@@ -71,6 +75,12 @@ object Mocks {
         coEvery { send(any(), any(), any(), any(), any(), any()) } answers {
             mockkSuccessOf(secondArg())
         }
+        coEvery { sendProfileKey(any(), any(), any()) } answers {
+            mockkSuccessOf(secondArg())
+        }
+        coEvery { sendReceipt(any(), any(), any()) } answers {
+            mockkSuccessOf(secondArg())
+        }
         coEvery { setExpiration(any(), any(), any()) } answers {
             mockkSuccessOf(secondArg())
         }
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt
index f210d1ae169646633b58ad8a0c93319edfc6e38e..10a979ee7bfb04320c22739d058ea4cd13626cbb 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt
@@ -44,6 +44,7 @@ import kotlin.time.ExperimentalTime
 class SignalReceiver(private val app: Application) {
     companion object: Any(), KLoggable {
         override val logger = logger()
+        private val metrics = Metrics.SignalReceiver
         private const val TMP_FILE_PREFIX = "___"
         private const val TMP_FILE_SUFFIX = ".tmp"
         private const val MAX_ATTACHMENT_SIZE = 150L * 1024 * 1024 // 150MB
@@ -60,7 +61,6 @@ class SignalReceiver(private val app: Application) {
     // FIELDS/FACTORIES
 
     internal val messagesInFlight = AtomicInteger(0)
-
     private val subscriptions = ConcurrentHashMap<String,Job>()
     private val messageReceivers = ConcurrentHashMap<String,SignalServiceMessageReceiver>()
     private val messagePipes = ConcurrentHashMap<String,SignalServiceMessagePipe>()
@@ -166,20 +166,25 @@ class SignalReceiver(private val app: Application) {
     private suspend fun dispatch(account: VerifiedAccount, envelope: SignalServiceEnvelope): Job?  {
         envelope.type.asEnum().let {
             logger.debug { "Got ${it.asString} from ${envelope.sourceIdentifier ?: "SEALED"} to ${account.username}" }
-            Metrics.SignalReceiver.numberOfMessagesReceived.labels(it.asString).inc()
+            metrics.numberOfMessagesReceived.labels(it.asString).inc()
             return when (it) {
                 EnvelopeType.CIPHERTEXT,
-                EnvelopeType.UNIDENTIFIED_SENDER -> processMessage(envelope, account)
-
+                EnvelopeType.UNIDENTIFIED_SENDER -> {
+                    processMessage(envelope, account)
+                }
                 EnvelopeType.PREKEY_BUNDLE -> {
                     processPreKeyBundle(envelope, account)
                     processMessage(envelope, account)
                 }
-
-                EnvelopeType.RECEIPT -> processReceipt(envelope, account)
-
-                EnvelopeType.KEY_EXCHANGE, // TODO: handle this to process "reset secure session" events
-                EnvelopeType.UNKNOWN -> drop(envelope, account)
+                EnvelopeType.RECEIPT -> {
+                    processReceipt(envelope, account)
+                    null
+                }
+                EnvelopeType.KEY_EXCHANGE, // TODO: handle KEY_EXCHANGE to process "reset secure session" events
+                EnvelopeType.UNKNOWN ->  {
+                    drop(envelope, account)
+                    null
+                }
             }
         }
     }
@@ -193,21 +198,22 @@ class SignalReceiver(private val app: Application) {
             try {
                 messagesInFlight.getAndIncrement()
 
+                // decrypt data message (returning early if not data message -- eg: typing notification, etc.)
                 val contents = cipherOf(account).decrypt(envelope)
-                val dataMessage = contents.dataMessage.orNull()?: return@launch // drop non-data msgs (eg: typing, sync)
+                val dataMessage = contents.dataMessage.orNull()?: return@launch
                 contactAddress = contents.sender.asSignalcAddress()
 
-                val body = dataMessage.body?.orNull() ?: "" // expiry timer changes contain empty message bodies
-                val attachments = dataMessage.attachments.orNull() ?: emptyList()
+                // acknowledge receipt to sender and store their profile key if present
+                app.signalSender.sendReceipt(account, contactAddress, dataMessage.timestamp)
                 dataMessage.profileKey.orNull()?.let {
-                    // dataMessage.isProfileKeyUpdate is flaky on 1st message, so we store profile key on every message
                     app.contactStore.storeProfileKey(account.id, contactAddress.identifier, it)
+                } ?: run {
+                    metrics.numberOfMessagesWithoutProfileKey.labels(envelope.isUnidentifiedSender.toString()).inc()
                 }
 
-                launch {
-                    app.signalSender.sendReceipt(account, contactAddress, dataMessage.timestamp)
-                }
-
+                // extract contents of message and relay to client
+                val body = dataMessage.body?.orNull() ?: "" // expiry timer changes contain empty message bodies
+                val attachments = dataMessage.attachments.orNull() ?: emptyList()
                 app.socketSender.send(
                     SocketResponse.Cleartext.of(
                         contactAddress,
@@ -226,46 +232,38 @@ class SignalReceiver(private val app: Application) {
         }
     }
 
-    private suspend fun processPreKeyBundle(envelope: SignalServiceEnvelope, account: VerifiedAccount) =
-        withContext(app.coroutineScope.coroutineContext + Concurrency.Dispatcher) {
-            // This is our first (and only!) chance to store both the UUID and phone number that will be necessary
-            // to successfully decrypt subsequent messages in a session that may refer either to UUID or number.
-            // Store them here so we can resolve either to an int id in the protocol store!
-            if(!app.contactStore.hasContact(account.username, envelope.sourceIdentifier)) {
-                app.contactStore.create(
-                    accountId = account.username,
-                    phoneNumber = envelope.sourceE164.orNull(),
-                    uuid = UUID.fromString(envelope.sourceUuid.orNull()),
-                )
-                // We also want new contacts to be able to send us sealed sender messages as soon as possible, so
-                // send them our profile key (which enalbes sealed-sender message sending) now.
-                app.signalSender.sendProfileKey(account, envelope.asSignalcAddress())
-            }
-            // If we are receiving a prekey bundle, this is the beginning of a new session, the initiation
-            // of which might have depleted our prekey reserves below the level we want to keep on hand
-            // to start new sessions. So: launch a background job to check our prekey reserve and replenish it if needed!
-            app.accountManager.refreshPreKeysIfDepleted(account)
+    private suspend fun processPreKeyBundle(envelope: SignalServiceEnvelope, account: VerifiedAccount) {
+        // Prekey buncles are received (once per device) from new contacts before we initiate a sealed sender session
+        // with them. As such, this is our first (and only!) chance to store both the UUID and phone number
+        // that will be necessary to successfully decrypt subsequent messages in a session that may refer to the same
+        // user by *either* UUID or phone number (and will switch between the two during thes same session).
+        // We store both here so we can resolve either UUID or phone number to the same contact later.
+        // We also want new contacts to be able to send us sealed sender messages as soon as possible, so
+        // we send them our profile key, which enables sealed-sender message sending.
+        if(!app.contactStore.hasContact(account.id, envelope.sourceIdentifier)) {
+            app.contactStore.create(account, envelope)
+            app.signalSender.sendProfileKey(account, envelope.asSignalcAddress())
         }
-
-    private suspend fun processReceipt(envelope: SignalServiceEnvelope, account: VerifiedAccount): Job? {
-        // An unsealed receipt is the first contact we will have if we initiated a conversation with a contact, so use the
-        // opportunity to store their UUID and phone number!
-        if(!envelope.isUnidentifiedSender) {
-            app.contactStore.storeMissingIdentifier(
-                accountId = account.username,
-                contactPhoneNumber =  envelope.sourceE164.get(),
-                contactUuid =  UUID.fromString(envelope.sourceUuid.get()),
-            )
-        }
-        return null
+        // If we are receiving a prekey bundle, this is also the beginning of a new session, which consumes one of the
+        // recipient's one-time prekeys. Since this might have depleted the recipient's prekeys below the number needed
+        // to start new sessions, we launch a background job to check the prekey reserve and replenish it if needed!
+        app.accountManager.refreshPreKeysIfDepleted(account)
     }
 
-    private suspend fun drop(envelope: SignalServiceEnvelope, account: VerifiedAccount): Job? {
+    private suspend fun processReceipt(envelope: SignalServiceEnvelope, account: VerifiedAccount) =
+        // An unsealed receipt is the first contact we will have if we initiated a conversation with a contact, so
+        // use the opportunity to store their UUID and phone number! (Note: all envelopes of type RECEIPT are unsealed.)
+        app.contactStore.storeMissingIdentifier(
+            accountId = account.id,
+            contactPhoneNumber =  envelope.sourceE164.get(),
+            contactUuid =  UUID.fromString(envelope.sourceUuid.get()),
+        )
+
+
+    private suspend fun drop(envelope: SignalServiceEnvelope, account: VerifiedAccount) =
         app.socketSender.send(
             SocketResponse.Dropped(envelope.asSignalcAddress(), account.address, envelope)
         )
-        return null
-    }
 
     private suspend fun handleRelayError(err: Throwable, account: SignalcAddress, contact: SignalcAddress?) {
         when (err) {
@@ -274,13 +272,7 @@ class SignalReceiver(private val app: Application) {
                 // it to send a garbage message that will force a session reset and raise another identity exception.
                 // That exception (unlike this one) will include the fingerprint corresponding to the new session,
                 // and can be handled (and trusted) from the send path.
-                app.socketSender.send(
-                    SocketResponse.InboundIdentityFailure.of(
-                        account,
-                        contact,
-                        null,
-                    )
-                )
+                app.socketSender.send(SocketResponse.InboundIdentityFailure.of(account, contact,null))
             }
             else -> {
                 app.socketSender.send(SocketResponse.DecryptionError(account, contact, err))
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt
index 73330bd976f396732ff7a5cd6c4d26deeae8b05a..007a5b67a7da394c91b415b64bae00088365a09b 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt
@@ -84,7 +84,7 @@ class SignalSender(private val app: Application) {
                 Optional.absent(), // preview (we don't support those!)
                 sendAttachment.width,
                 sendAttachment.height,
-                TimeUtil.nowInMillis(), //uploadTimestamp
+                nowInMillis(), //uploadTimestamp
                 Optional.fromNullable(sendAttachment.caption),
                 Optional.fromNullable(sendAttachment.blurHash),
                 null, // progressListener
@@ -103,7 +103,7 @@ class SignalSender(private val app: Application) {
         body: String,
         expiration: Int,
         attachments: List<SocketRequest.Send.Attachment> = emptyList(),
-        timestamp: Long = TimeUtil.nowInMillis(),
+        timestamp: Long = nowInMillis(),
     ): SignalcSendResult =
         // TODO: handle `signalservice.api.push.exceptions.NotFoundException` here
         sendDataMessage(
@@ -124,7 +124,7 @@ class SignalSender(private val app: Application) {
     suspend fun sendProfileKey(
         sender: VerifiedAccount,
         recipient: SignalcAddress,
-        timestamp: Long = TimeUtil.nowInMillis(),
+        timestamp: Long = nowInMillis(),
     ): SignalcSendResult =
         sendDataMessage(
             sender,
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/metrics/Metrics.kt b/signalc/src/main/kotlin/info/signalboost/signalc/metrics/Metrics.kt
index 0353ef7d902590a67eb65cc30816209654b84ab6..a9eec56d5a2c4a4329df6555d288c3e3f461148a 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/metrics/Metrics.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/metrics/Metrics.kt
@@ -64,10 +64,20 @@ object Metrics {
     object SignalReceiver {
         val numberOfMessagesReceived: Counter = counterOf(
             "signal_receiver__number_of_messages_received",
-            "Counts number of inbound PREKEY_BUNDLE messages we receive from signal server when users try to establish new sessions." +
-                    "If we often receive a high number of these in quick succession, consider throttling prekey replenish jobs.",
+            "Counts number of inbound messages received from signal server and categorizes them by type",
             "envelope_type",
         )
+
+        val numberOfMessagesWithoutProfileKey = counterOf(
+            "signal_receiver__number_of_messages_without_profile_key",
+            "Counts number of messages received without profile key. " +
+                    "The 'is_sealed_sender' label allow us to track unsealed messages without profile keys " +
+                    "which are worrisome because this is the signature of sessions initiated from a desktop client " +
+                    "but not confirmed on a phone by tapping 'continue'. Without such confirmation, we will " +
+                    "never be able to initiate sealed sender sessions, and thus risk getting blocked " +
+                    "if the number of such messages gets too high.",
+            "is_sealed_sender"
+        )
     }
 
     object SignalSender {
diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt
index af46742d8ab8840b19278b609b93bb42d2d0eaa3..569bbcb085b6002268d7acdbfe9e9a351244e222 100644
--- a/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt
+++ b/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt
@@ -14,6 +14,7 @@ import org.jetbrains.exposed.sql.*
 import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
 import org.jetbrains.exposed.sql.transactions.transaction
 import org.signal.zkgroup.profiles.ProfileKey
+import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
 import java.lang.IllegalStateException
 import java.util.UUID
 import kotlin.io.path.ExperimentalPathApi
@@ -45,6 +46,9 @@ class ContactStore(app: Application) {
             }
         }.resultedValues!!.single()[Contacts.contactId]
 
+    suspend fun create(account: VerifiedAccount, envelope: SignalServiceEnvelope): Int =
+        create(account.id, envelope.sourceE164.orNull(), envelope.sourceUuid.orNull()?.let { UUID.fromString(it) } )
+
     suspend fun createOwnContact(account: VerifiedAccount) = with(account) {
         create(accountId = username, phoneNumber = username, uuid = uuid, profileKey = profileKeyBytes)
     }
diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt b/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt
index 6ca55166a29b1e1b61004704408612657aad9933..6c7153c547e0b290d94ed0a65ddfa351b8bebcad 100644
--- a/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt
+++ b/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt
@@ -2,6 +2,7 @@ package info.signalboost.signalc.logic
 
 import info.signalboost.signalc.Application
 import info.signalboost.signalc.Config
+import info.signalboost.signalc.model.SignalcAddress.Companion.asSignalcAddress
 import info.signalboost.signalc.testSupport.coroutines.CoroutineUtil.teardown
 import info.signalboost.signalc.testSupport.dataGenerators.AccountGen.genVerifiedAccount
 import info.signalboost.signalc.testSupport.dataGenerators.AddressGen.genDeviceId
@@ -38,6 +39,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
 import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
 import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type.*
 import java.io.IOException
+import java.util.*
 import java.util.concurrent.TimeoutException
 import kotlin.io.path.ExperimentalPathApi
 import kotlin.io.path.Path
@@ -118,6 +120,7 @@ class SignalReceiverTest : FreeSpec({
             every {
                 anyConstructed<SignalServiceCipher>().decrypt(any())
             } returns mockk {
+                every { sender } returns senderAddress.asSignalServiceAddress()
                 every { dataMessage.orNull() } returns mockDataMessage.apply {
                     every { body.orNull() } returnsMany cleartexts
                 }
@@ -541,6 +544,47 @@ class SignalReceiverTest : FreeSpec({
                         }
                     }
                 }
+
+                "when the receiving account does has not stored a contact for the sender" - {
+                    coEvery {
+                        app.contactStore.hasContact(recipientAccount.id, envelope.sourceIdentifier)
+                    } returns false
+
+                    "creates a contact for the sender" {
+                        messageReceiver.subscribe(recipientAccount)
+                        eventually(timeout, pollInterval) {
+                            coVerify {
+                                app.contactStore.create(recipientAccount, envelope)
+                            }
+                        }
+                    }
+
+                    "sends the contact a profile key" {
+                        messageReceiver.subscribe(recipientAccount)
+                        eventually(timeout * 4, pollInterval) {
+                            coVerify {
+                                app.signalSender.sendProfileKey(recipientAccount, any())
+                            }
+                        }
+                    }
+                }
+            }
+
+            "when signal sends a RECEIPT" - {
+                val (envelope) = signalSendsEnvelopeOf(RECEIPT_VALUE)
+
+                "stores an missing identifiers contained in the receipt" {
+                    messageReceiver.subscribe(recipientAccount)
+                    eventually(timeout, pollInterval) {
+                        coVerify {
+                            app.contactStore.storeMissingIdentifier(
+                                recipientAccount.id,
+                                envelope.sourceE164.get(),
+                                UUID.fromString(envelope.sourceUuid.get()),
+                            )
+                        }
+                    }
+                }
             }
 
             "when issued for an account that is already subscribed" - {
diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalSenderTest.kt b/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalSenderTest.kt
index 865baa77aec2789ff81fa00d6ef48c0de48b989e..1deaeaaf5e481b1a628179371bb3fa8ec844d0a9 100644
--- a/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalSenderTest.kt
+++ b/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalSenderTest.kt
@@ -14,8 +14,10 @@ import info.signalboost.signalc.testSupport.dataGenerators.FileGen.genJpegFile
 import info.signalboost.signalc.testSupport.dataGenerators.SocketRequestGen.genSendAttachment
 import info.signalboost.signalc.testSupport.matchers.SignalMessageMatchers.signalDataMessage
 import info.signalboost.signalc.testSupport.matchers.SignalMessageMatchers.signalExpirationUpdate
+import info.signalboost.signalc.testSupport.matchers.SignalMessageMatchers.signalReceiptMessage
 import info.signalboost.signalc.util.KeyUtil.genUuidStr
 import info.signalboost.signalc.util.TimeUtil
+import info.signalboost.signalc.util.TimeUtil.nowInMillis
 import io.kotest.assertions.timing.eventually
 import io.kotest.core.spec.style.FreeSpec
 import io.kotest.matchers.should
@@ -33,6 +35,7 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
 import org.whispersystems.signalservice.api.messages.SendMessageResult
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
+import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage
 import org.whispersystems.signalservice.api.push.SignalServiceAddress
 import java.io.File
 import java.io.InputStream
@@ -60,8 +63,24 @@ class SignalSenderTest : FreeSpec({
 
         val app = Application(config).run(testScope)
         val verifiedAccount = genVerifiedAccount()
+        val contactPhoneNumber = genPhoneNumber()
         val timeout = Duration.milliseconds(5)
 
+        val mockSuccess = mockk<SendMessageResult.Success> {
+            every { isNeedsSync } returns true
+            every { isUnidentified } returns false
+            every { duration } returns 0L
+        }
+
+        fun sendingSucceeds() {
+            every {
+                anyConstructed<SignalServiceMessageSender>().sendMessage(any(), any(), any())
+            } returns mockk {
+                every { address } returns contactPhoneNumber.asSignalServiceAddress()
+                every { success } returns mockSuccess
+            }
+        }
+
         beforeSpec {
             mockkObject(TimeUtil)
             mockkConstructor(SignalServiceMessageSender::class)
@@ -114,14 +133,7 @@ class SignalSenderTest : FreeSpec({
             }
         }
 
-        val mockSuccess = mockk<SendMessageResult.Success> {
-            every { isNeedsSync } returns true
-            every { isUnidentified } returns false
-            every { duration } returns 0L
-        }
-
         "#send" - {
-            val recipientPhone = genPhoneNumber()
             val dataMessageSlot = slot<SignalServiceDataMessage>()
             every {
                 anyConstructed<SignalServiceMessageSender>().sendMessage(
@@ -130,14 +142,14 @@ class SignalSenderTest : FreeSpec({
                     capture(dataMessageSlot),
                 )
             } returns mockk {
-                every { address } returns recipientPhone.asSignalServiceAddress()
+                every { address } returns contactPhoneNumber.asSignalServiceAddress()
                 every { success } returns mockSuccess
             }
 
             val now = TimeUtil.nowInMillis()
             suspend fun sendHello(): SignalcSendResult = app.signalSender.send(
                 sender = verifiedAccount,
-                recipient = recipientPhone.asSignalcAddress(),
+                recipient = contactPhoneNumber.asSignalcAddress(),
                 body = "hello!",
                 expiration = 5000,
                 attachments = emptyList(),
@@ -148,8 +160,8 @@ class SignalSenderTest : FreeSpec({
                 val result = sendHello()
                     verify {
                         anyConstructed<SignalServiceMessageSender>().sendMessage(
-                            SignalServiceAddress(null, recipientPhone),
-                            any(), // TODO: we actually pass something here!
+                            SignalServiceAddress(null, contactPhoneNumber),
+                            any(),
                             signalDataMessage(
                                 body = "hello!",
                                 timestamp = now,
@@ -164,7 +176,7 @@ class SignalSenderTest : FreeSpec({
                 every { TimeUtil.nowInMillis() } returns 1000L
                 app.signalSender.send(
                     verifiedAccount,
-                    recipientPhone.asSignalcAddress(),
+                    contactPhoneNumber.asSignalcAddress(),
                     "hello!",
                     DEFAULT_EXPIRY_TIME,
                     emptyList()
@@ -186,7 +198,7 @@ class SignalSenderTest : FreeSpec({
                 }
                 app.signalSender.send(
                     verifiedAccount,
-                    recipientPhone.asSignalcAddress(),
+                    contactPhoneNumber.asSignalcAddress(),
                     "",
                     DEFAULT_EXPIRY_TIME,
                     emptyList()
@@ -204,7 +216,7 @@ class SignalSenderTest : FreeSpec({
                 "when sealed sender tokens can be derived from a recipient's profile key" - {
                     val mockkUnidentifiedAccessPair = mockk<UnidentifiedAccessPair>()
                     coEvery {
-                        app.accountManager.getUnidentifiedAccessPair(verifiedAccount.identifier, recipientPhone)
+                        app.accountManager.getUnidentifiedAccessPair(verifiedAccount.identifier, contactPhoneNumber)
                     } returns mockkUnidentifiedAccessPair
 
                     "sends a sealed sender message with access tokens derived from key" {
@@ -222,7 +234,7 @@ class SignalSenderTest : FreeSpec({
                 "when sealed sender tokens cannot be derrived for a recipient (b/c missing profile key)" - {
                     "when unsealed messages are allowed" - {
                         coEvery {
-                            app.accountManager.getUnidentifiedAccessPair(verifiedAccount.identifier, recipientPhone)
+                            app.accountManager.getUnidentifiedAccessPair(verifiedAccount.identifier, contactPhoneNumber)
                         } returns null
 
                         "sends an unsealed-sender message (with no access tokens)" {
@@ -245,10 +257,10 @@ class SignalSenderTest : FreeSpec({
                         )
                         val app2 = Application(config2).run(testScope)
                         coEvery {
-                            app2.accountManager.getUnidentifiedAccessPair(verifiedAccount.identifier, recipientPhone)
+                            app2.accountManager.getUnidentifiedAccessPair(verifiedAccount.identifier, contactPhoneNumber)
                         } returns null
 
-                        val recipientAddress = recipientPhone.asSignalcAddress()
+                        val recipientAddress = contactPhoneNumber.asSignalcAddress()
 
                         afterTest {
                             app2.stop()
@@ -297,7 +309,7 @@ class SignalSenderTest : FreeSpec({
 
                 app.signalSender.send(
                     sender = verifiedAccount,
-                    recipient = recipientPhone.asSignalcAddress(),
+                    recipient = contactPhoneNumber.asSignalcAddress(),
                     body = "hello!",
                     expiration = 5000,
                     attachments = listOf(sendAttachment),
@@ -334,6 +346,46 @@ class SignalSenderTest : FreeSpec({
             }
         }
 
+        "#sendProfileKey" - {
+            sendingSucceeds()
+
+            "sends a profile key to a contact" {
+                app.signalSender.sendProfileKey(verifiedAccount, contactPhoneNumber.asSignalcAddress())
+                verify {
+                    anyConstructed<SignalServiceMessageSender>().sendMessage(
+                        contactPhoneNumber.asSignalServiceAddress(),
+                        any(),
+                        signalDataMessage(
+                            isProfileKeyUpdate = true,
+                            profileKey = verifiedAccount.profileKeyBytes,
+                        )
+                    )
+                }
+            }
+        }
+
+        "#sendReceipt" - {
+            val timestamp = nowInMillis()
+            every {
+                anyConstructed<SignalServiceMessageSender>().sendReceipt(any(), any(), any())
+            } returns Unit
+
+            "sends a READ receipt to a contact" {
+                app.signalSender.sendReceipt(verifiedAccount, contactPhoneNumber.asSignalcAddress(), timestamp)
+                verify {
+                    anyConstructed<SignalServiceMessageSender>().sendReceipt(
+                        contactPhoneNumber.asSignalServiceAddress(),
+                        Optional.absent(),
+                        signalReceiptMessage(
+                            type = SignalServiceReceiptMessage.Type.READ,
+                            timestamps = listOf(timestamp),
+                            id = timestamp,
+                        )
+                    )
+                }
+            }
+        }
+
         "#setExpiration" - {
             val recipientPhone = genPhoneNumber()
             every {
@@ -352,7 +404,7 @@ class SignalSenderTest : FreeSpec({
                 verify {
                     anyConstructed<SignalServiceMessageSender>().sendMessage(
                         SignalServiceAddress(null, recipientPhone),
-                        any(), // TODO: assert that we passed correct access pair here!
+                        Optional.absent(),
                         signalExpirationUpdate(60)
                     )
                 }
diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/store/ContactStoreTest.kt b/signalc/src/test/kotlin/info/signalboost/signalc/store/ContactStoreTest.kt
index beb2d01bef3f00f5a98560b02014eb421a321b35..1333f268121d75f8626d57fc05e1996799e092d2 100644
--- a/signalc/src/test/kotlin/info/signalboost/signalc/store/ContactStoreTest.kt
+++ b/signalc/src/test/kotlin/info/signalboost/signalc/store/ContactStoreTest.kt
@@ -5,8 +5,10 @@ import com.zaxxer.hikari.HikariDataSource
 import info.signalboost.signalc.Application
 import info.signalboost.signalc.Config
 import info.signalboost.signalc.testSupport.coroutines.CoroutineUtil.teardown
+import info.signalboost.signalc.testSupport.dataGenerators.AccountGen.genVerifiedAccount
 import info.signalboost.signalc.testSupport.dataGenerators.AddressGen.genPhoneNumber
 import info.signalboost.signalc.testSupport.dataGenerators.AddressGen.genUuid
+import info.signalboost.signalc.testSupport.dataGenerators.EnvelopeGen.genEnvelope
 import info.signalboost.signalc.util.KeyUtil.genProfileKeyBytes
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.core.spec.style.FreeSpec
@@ -33,7 +35,8 @@ class ContactStoreTest: FreeSpec({
         val config = Config.mockAllExcept(ContactStore::class, HikariDataSource::class)
         val app = Application(config).run(testScope)
 
-        val accountId = genPhoneNumber()
+        val account = genVerifiedAccount()
+        val accountId = account.id
         val contactProfileKey = genRandomBytes(32)
         val contactUpdatedProfileKey = genRandomBytes(32)
 
@@ -92,6 +95,18 @@ class ContactStoreTest: FreeSpec({
                         }
                     }
                 }
+
+                "given an account and an envelope" - {
+                    val envelope = genEnvelope()
+                    val id = app.contactStore.create(account, envelope)
+
+                    "creates a contact and returns its id" {
+                        id shouldBeGreaterThan 0
+                        app.contactStore.count() shouldBeGreaterThan initCount
+                        app.contactStore.hasContact(accountId, envelope.sourceE164.get()) shouldBe true
+                        app.contactStore.hasContact(accountId, envelope.sourceUuid.get()) shouldBe true
+                    }
+                }
             }
 
             "#hasContact" - {
diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SignalMessageMatchers.kt b/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SignalMessageMatchers.kt
index 2431b04c2f51c46b2d92174ae117bf2bb94ddab8..fcdf41dd611c0f9353ef0c673c7cd2bc68bede81 100644
--- a/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SignalMessageMatchers.kt
+++ b/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SignalMessageMatchers.kt
@@ -4,6 +4,7 @@ import io.mockk.MockKMatcherScope
 import org.whispersystems.libsignal.util.guava.Optional
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
+import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage
 
 object SignalMessageMatchers {
 
@@ -12,14 +13,30 @@ object SignalMessageMatchers {
         timestamp: Long? = null,
         expiresInSeconds: Int? = null,
         attachments: Optional<List<SignalServiceAttachment>>? = null,
+        isProfileKeyUpdate: Boolean? = null,
+        profileKey: ByteArray? = null,
     ): SignalServiceDataMessage = match {
         // check for equality of each provided param. if param not provided, don't check it!
         body?.let{ _ -> it.body.or("") == body } ?: true &&
-            timestamp?.let { _ -> it.timestamp == timestamp } ?: true &&
-            expiresInSeconds?.let { _ -> it.expiresInSeconds == expiresInSeconds } ?: true &&
-            attachments?.let { _ -> it.attachments == attachments } ?: true
+                timestamp?.let { _ -> it.timestamp == timestamp } ?: true &&
+                expiresInSeconds?.let { _ -> it.expiresInSeconds == expiresInSeconds } ?: true &&
+                attachments?.let { _ -> it.attachments == attachments } ?: true &&
+                isProfileKeyUpdate?.let { _ -> it.isProfileKeyUpdate == isProfileKeyUpdate} ?: true &&
+                profileKey?.let { _ -> it.profileKey.or(ByteArray(0)).contentEquals(profileKey) } ?: true
     }
 
+    fun MockKMatcherScope.signalReceiptMessage(
+        type: SignalServiceReceiptMessage.Type? = null,
+        timestamps: List<Long>? = null,
+        id: Long? = null,
+    ): SignalServiceReceiptMessage = match {
+        // check for equality of each provided param. if param not provided, don't check it!
+        type?.let{ _ -> it.type == type } ?: true &&
+                timestamps?.let { _ -> it.timestamps == timestamps } ?: true &&
+                id?.let { _ -> it.`when` == id } ?: true
+    }
+
+
     fun MockKMatcherScope.signalExpirationUpdate(
         expiresInSeconds: Int,
     ): SignalServiceDataMessage = match {