diff --git a/services/identity/src/client_service.rs b/services/identity/src/client_service.rs
--- a/services/identity/src/client_service.rs
+++ b/services/identity/src/client_service.rs
@@ -18,8 +18,9 @@
   error_types
 };
 use crate::database::{
-  DBDeviceTypeInt, DatabaseClient, DeviceType, KeyPayload,
+  DBDeviceTypeInt, DatabaseClient, DeviceType, KeyPayload
 };
+use crate::device_list::SignedDeviceList;
 use crate::error::{DeviceListError, Error as DBError};
 use crate::grpc_services::authenticated::DeletePasswordUserInfo;
 use crate::grpc_services::protos::unauth::{
@@ -35,7 +36,7 @@
 };
 use crate::grpc_services::shared::get_value;
 use crate::grpc_utils::{
-  SignedNonce, DeviceKeyUploadActions,
+  DeviceKeyUploadActions, RegistrationActions, SignedNonce
 };
 use crate::nonce::generate_nonce_data;
 use crate::reserved_users::{
@@ -66,6 +67,7 @@
   pub flattened_device_key_upload: FlattenedDeviceKeyUpload,
   pub user_id: Option<String>,
   pub farcaster_id: Option<String>,
+  pub initial_device_list: Option<SignedDeviceList>,
 }
 
 #[derive(Clone, Serialize, Deserialize)]
@@ -439,6 +441,13 @@
     let code_version = get_code_version(&request);
     let message = request.into_inner();
 
+    // WalletAuthRequest is used for both log_in_wallet_user and register_wallet_user
+    if !message.initial_device_list.is_empty() {
+      return Err(tonic::Status::invalid_argument(
+        "unexpected initial device list",
+      ));
+    }
+
     let parsed_message = parse_and_verify_siwe_message(
       &message.siwe_message,
       &message.siwe_signature,
@@ -1129,7 +1138,7 @@
 }
 
 fn construct_user_registration_info(
-  message: &impl DeviceKeyUploadActions,
+  message: &(impl DeviceKeyUploadActions + RegistrationActions),
   user_id: Option<String>,
   username: String,
   farcaster_id: Option<String>,
@@ -1141,6 +1150,7 @@
     )?,
     user_id,
     farcaster_id,
+    initial_device_list: message.get_and_verify_initial_device_list()?,
   })
 }
 
diff --git a/services/identity/src/device_list.rs b/services/identity/src/device_list.rs
--- a/services/identity/src/device_list.rs
+++ b/services/identity/src/device_list.rs
@@ -1,5 +1,5 @@
 use chrono::{DateTime, Duration, Utc};
-use std::collections::HashSet;
+use std::{collections::HashSet, str::FromStr};
 use tracing::{debug, error, warn};
 
 use crate::{
@@ -20,7 +20,7 @@
 
 /// Signed device list payload that is serializable to JSON.
 /// For the DDB payload, see [`DeviceListUpdate`]
-#[derive(serde::Serialize, serde::Deserialize)]
+#[derive(Clone, serde::Serialize, serde::Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct SignedDeviceList {
   /// JSON-stringified [`RawDeviceList`]
@@ -88,13 +88,20 @@
 impl TryFrom<UpdateDeviceListRequest> for SignedDeviceList {
   type Error = tonic::Status;
   fn try_from(request: UpdateDeviceListRequest) -> Result<Self, Self::Error> {
-    serde_json::from_str(&request.new_device_list).map_err(|err| {
+    request.new_device_list.parse().map_err(|err| {
       warn!("Failed to deserialize device list update: {}", err);
       tonic::Status::invalid_argument("invalid device list payload")
     })
   }
 }
 
+impl FromStr for SignedDeviceList {
+  type Err = serde_json::Error;
+  fn from_str(s: &str) -> Result<Self, Self::Err> {
+    serde_json::from_str(s)
+  }
+}
+
 impl TryFrom<SignedDeviceList> for DeviceListUpdate {
   type Error = tonic::Status;
   fn try_from(signed_list: SignedDeviceList) -> Result<Self, Self::Error> {
diff --git a/services/identity/src/grpc_utils.rs b/services/identity/src/grpc_utils.rs
--- a/services/identity/src/grpc_utils.rs
+++ b/services/identity/src/grpc_utils.rs
@@ -2,11 +2,12 @@
 use ed25519_dalek::{PublicKey, Signature, Verifier};
 use serde::Deserialize;
 use tonic::Status;
+use tracing::warn;
 
 use crate::{
-  database::DeviceRow,
-  ddb_utils::DBIdentity,
-  ddb_utils::Identifier as DBIdentifier,
+  database::{DeviceListUpdate, DeviceRow, KeyPayload},
+  ddb_utils::{DBIdentity, Identifier as DBIdentifier},
+  device_list::SignedDeviceList,
   grpc_services::protos::{
     auth::{EthereumIdentity, Identity, InboundKeyInfo, OutboundKeyInfo},
     unauth::{
@@ -249,6 +250,68 @@
   }
 }
 
+/// Common functionality for registration request messages
+trait RegistrationData {
+  fn initial_device_list(&self) -> &str;
+}
+
+impl RegistrationData for RegistrationStartRequest {
+  fn initial_device_list(&self) -> &str {
+    &self.initial_device_list
+  }
+}
+impl RegistrationData for ReservedRegistrationStartRequest {
+  fn initial_device_list(&self) -> &str {
+    &self.initial_device_list
+  }
+}
+impl RegistrationData for WalletAuthRequest {
+  fn initial_device_list(&self) -> &str {
+    &self.initial_device_list
+  }
+}
+impl RegistrationData for ReservedWalletRegistrationRequest {
+  fn initial_device_list(&self) -> &str {
+    &self.initial_device_list
+  }
+}
+
+/// Similar to `[DeviceKeyUploadActions]` but only for registration requests
+pub trait RegistrationActions {
+  fn get_and_verify_initial_device_list(
+    &self,
+  ) -> Result<Option<SignedDeviceList>, tonic::Status>;
+}
+
+impl<T: RegistrationData + DeviceKeyUploadActions> RegistrationActions for T {
+  fn get_and_verify_initial_device_list(
+    &self,
+  ) -> Result<Option<SignedDeviceList>, tonic::Status> {
+    let payload = self.initial_device_list();
+    if payload.is_empty() {
+      return Ok(None);
+    }
+    let signed_list: SignedDeviceList = payload.parse().map_err(|err| {
+      warn!("Failed to deserialize initial device list: {}", err);
+      tonic::Status::invalid_argument("invalid device list payload")
+    })?;
+
+    let key_info = self
+      .payload()?
+      .parse::<KeyPayload>()
+      .map_err(|_| tonic::Status::invalid_argument("malformed payload"))?;
+    let primary_device_id = key_info.primary_identity_public_keys.ed25519;
+
+    let update_payload = DeviceListUpdate::try_from(signed_list.clone())?;
+    crate::device_list::verify_initial_device_list(
+      &update_payload,
+      &primary_device_id,
+    )?;
+
+    Ok(Some(signed_list))
+  }
+}
+
 impl From<DBIdentity> for Identity {
   fn from(value: DBIdentity) -> Self {
     match value.identifier {