diff --git a/services/commtest/tests/identity_integration_tests.rs b/services/commtest/tests/identity_integration_tests.rs
--- a/services/commtest/tests/identity_integration_tests.rs
+++ b/services/commtest/tests/identity_integration_tests.rs
@@ -4,7 +4,7 @@
 use commtest::service_addr;
 use grpc_clients::identity::{
   get_auth_client, get_unauthenticated_client,
-  protos::auth::{Identity, UserIdentityRequest},
+  protos::auth::{Identity, UserIdentitiesRequest},
   protos::unauthenticated::{
     find_user_id_request::Identifier, FindUserIdRequest,
   },
@@ -54,11 +54,11 @@
   .await
   .expect("Couldn't connect to identity service");
 
-  let request = UserIdentityRequest {
-    user_id: device_info.user_id,
+  let request = UserIdentitiesRequest {
+    user_ids: vec![device_info.user_id.clone()],
   };
   let response = client
-    .find_user_identity(request)
+    .find_user_identities(request)
     .await
     .expect("request failed")
     .into_inner();
@@ -66,10 +66,10 @@
   let expected_username = device_info.username;
   assert!(
     matches!(
-      response.identity,
+      response.identities.get(&device_info.user_id),
       Some(Identity {
         username, ..
-      }) if username == expected_username
+      }) if *username == expected_username
     ),
     "username doesn't match"
   );
diff --git a/services/identity/src/database.rs b/services/identity/src/database.rs
--- a/services/identity/src/database.rs
+++ b/services/identity/src/database.rs
@@ -720,6 +720,54 @@
       .map_err(|e| Error::AwsSdk(e.into()))
   }
 
+  pub async fn find_db_user_identities(
+    &self,
+    user_ids: impl IntoIterator<Item = String>,
+  ) -> Result<HashMap<String, DBIdentity>, Error> {
+    use comm_lib::database::batch_operations::{
+      batch_get, ExponentialBackoffConfig,
+    };
+    let primary_keys = user_ids.into_iter().map(|user_id| {
+      create_simple_primary_key((
+        USERS_TABLE_PARTITION_KEY.to_string(),
+        user_id,
+      ))
+    });
+    let projection_expression = [
+      USERS_TABLE_PARTITION_KEY,
+      USERS_TABLE_USERNAME_ATTRIBUTE,
+      USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE,
+      USERS_TABLE_SOCIAL_PROOF_ATTRIBUTE_NAME,
+      USERS_TABLE_FARCASTER_ID_ATTRIBUTE_NAME,
+    ]
+    .join(", ");
+    debug!(
+      num_requests = primary_keys.size_hint().0,
+      "Attempting to batch get user identifiers"
+    );
+
+    let responses = batch_get(
+      &self.client,
+      USERS_TABLE,
+      primary_keys,
+      Some(projection_expression),
+      ExponentialBackoffConfig::default(),
+    )
+    .await
+    .map_err(Error::from)?;
+    debug!("Found {} matching user identifiers in DDB", responses.len());
+
+    let mut results = HashMap::with_capacity(responses.len());
+    for response in responses {
+      let user_id = response.get_attr(USERS_TABLE_PARTITION_KEY)?;
+      // if this fails, it means that projection expression didnt have all attrs it needed
+      let identity = DBIdentity::try_from(response)?;
+      results.insert(user_id, identity);
+    }
+
+    Ok(results)
+  }
+
   /// Retrieves username for password users or wallet address for wallet users
   /// Returns `None` if user not found
   #[tracing::instrument(skip_all)]
diff --git a/services/identity/src/grpc_services/authenticated.rs b/services/identity/src/grpc_services/authenticated.rs
--- a/services/identity/src/grpc_services/authenticated.rs
+++ b/services/identity/src/grpc_services/authenticated.rs
@@ -25,7 +25,7 @@
   PeersDeviceListsRequest, PeersDeviceListsResponse, RefreshUserPrekeysRequest,
   UpdateDeviceListRequest, UpdateUserPasswordFinishRequest,
   UpdateUserPasswordStartRequest, UpdateUserPasswordStartResponse,
-  UploadOneTimeKeysRequest, UserIdentityRequest, UserIdentityResponse,
+  UploadOneTimeKeysRequest, UserIdentitiesRequest, UserIdentitiesResponse,
 };
 use super::protos::unauth::Empty;
 
@@ -613,20 +613,25 @@
   }
 
   #[tracing::instrument(skip_all)]
-  async fn find_user_identity(
+  async fn find_user_identities(
     &self,
-    request: tonic::Request<UserIdentityRequest>,
-  ) -> Result<Response<UserIdentityResponse>, tonic::Status> {
+    request: tonic::Request<UserIdentitiesRequest>,
+  ) -> Result<Response<UserIdentitiesResponse>, tonic::Status> {
     let message = request.into_inner();
-    let identifier = self
+
+    let results = self
       .db_client
-      .get_user_identity(&message.user_id)
+      .find_db_user_identities(message.user_ids)
       .await
-      .map_err(handle_db_error)?
-      .ok_or_else(|| tonic::Status::not_found("user not found"))?;
+      .map_err(handle_db_error)?;
 
-    let response = UserIdentityResponse {
-      identity: Some(identifier.into()),
+    let mapped_results = results
+      .into_iter()
+      .map(|(user_id, identifier)| (user_id, identifier.into()))
+      .collect();
+
+    let response = UserIdentitiesResponse {
+      identities: mapped_results,
     };
     return Ok(Response::new(response));
   }
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
@@ -255,6 +255,7 @@
       DBIdentifier::Username(username) => Identity {
         username,
         eth_identity: None,
+        farcaster_id: value.farcaster_id,
       },
       DBIdentifier::WalletAddress(eth_identity) => Identity {
         username: eth_identity.wallet_address.clone(),
@@ -263,6 +264,7 @@
           siwe_message: eth_identity.social_proof.message,
           siwe_signature: eth_identity.social_proof.signature,
         }),
+        farcaster_id: value.farcaster_id,
       },
     }
   }
diff --git a/shared/protos/identity_auth.proto b/shared/protos/identity_auth.proto
--- a/shared/protos/identity_auth.proto
+++ b/shared/protos/identity_auth.proto
@@ -80,7 +80,7 @@
 
   /* Miscellaneous actions */
 
-  rpc FindUserIdentity(UserIdentityRequest) returns (UserIdentityResponse) {}
+  rpc FindUserIdentities(UserIdentitiesRequest) returns (UserIdentitiesResponse) {}
 }
 
 // Helper types
@@ -95,6 +95,7 @@
   // this is wallet address for Ethereum users
   string username = 1;
   optional EthereumIdentity eth_identity = 2;
+  optional string farcaster_id = 3;
 }
 
 // UploadOneTimeKeys
@@ -258,13 +259,13 @@
   string farcaster_id = 1;
 }
 
-// FindUserIdentity
+// FindUserIdentities
 
-message UserIdentityRequest {
-  // user ID for which we want to get the identity
-  string user_id = 1;
+message UserIdentitiesRequest {
+  // user IDs for which we want to get the identity
+  repeated string user_ids = 1;
 }
 
-message UserIdentityResponse {
-  Identity identity = 1;
+message UserIdentitiesResponse {
+  map<string, Identity> identities = 1;
 }
diff --git a/web/protobufs/identity-auth-client.cjs b/web/protobufs/identity-auth-client.cjs
--- a/web/protobufs/identity-auth-client.cjs
+++ b/web/protobufs/identity-auth-client.cjs
@@ -1057,61 +1057,61 @@
 /**
  * @const
  * @type {!grpc.web.MethodDescriptor<
- *   !proto.identity.auth.UserIdentityRequest,
- *   !proto.identity.auth.UserIdentityResponse>}
+ *   !proto.identity.auth.UserIdentitiesRequest,
+ *   !proto.identity.auth.UserIdentitiesResponse>}
  */
-const methodDescriptor_IdentityClientService_FindUserIdentity = new grpc.web.MethodDescriptor(
-  '/identity.auth.IdentityClientService/FindUserIdentity',
+const methodDescriptor_IdentityClientService_FindUserIdentities = new grpc.web.MethodDescriptor(
+  '/identity.auth.IdentityClientService/FindUserIdentities',
   grpc.web.MethodType.UNARY,
-  proto.identity.auth.UserIdentityRequest,
-  proto.identity.auth.UserIdentityResponse,
+  proto.identity.auth.UserIdentitiesRequest,
+  proto.identity.auth.UserIdentitiesResponse,
   /**
-   * @param {!proto.identity.auth.UserIdentityRequest} request
+   * @param {!proto.identity.auth.UserIdentitiesRequest} request
    * @return {!Uint8Array}
    */
   function(request) {
     return request.serializeBinary();
   },
-  proto.identity.auth.UserIdentityResponse.deserializeBinary
+  proto.identity.auth.UserIdentitiesResponse.deserializeBinary
 );
 
 
 /**
- * @param {!proto.identity.auth.UserIdentityRequest} request The
+ * @param {!proto.identity.auth.UserIdentitiesRequest} request The
  *     request proto
  * @param {?Object<string, string>} metadata User defined
  *     call metadata
- * @param {function(?grpc.web.RpcError, ?proto.identity.auth.UserIdentityResponse)}
+ * @param {function(?grpc.web.RpcError, ?proto.identity.auth.UserIdentitiesResponse)}
  *     callback The callback function(error, response)
- * @return {!grpc.web.ClientReadableStream<!proto.identity.auth.UserIdentityResponse>|undefined}
+ * @return {!grpc.web.ClientReadableStream<!proto.identity.auth.UserIdentitiesResponse>|undefined}
  *     The XHR Node Readable Stream
  */
-proto.identity.auth.IdentityClientServiceClient.prototype.findUserIdentity =
+proto.identity.auth.IdentityClientServiceClient.prototype.findUserIdentities =
     function(request, metadata, callback) {
   return this.client_.rpcCall(this.hostname_ +
-      '/identity.auth.IdentityClientService/FindUserIdentity',
+      '/identity.auth.IdentityClientService/FindUserIdentities',
       request,
       metadata || {},
-      methodDescriptor_IdentityClientService_FindUserIdentity,
+      methodDescriptor_IdentityClientService_FindUserIdentities,
       callback);
 };
 
 
 /**
- * @param {!proto.identity.auth.UserIdentityRequest} request The
+ * @param {!proto.identity.auth.UserIdentitiesRequest} request The
  *     request proto
  * @param {?Object<string, string>=} metadata User defined
  *     call metadata
- * @return {!Promise<!proto.identity.auth.UserIdentityResponse>}
+ * @return {!Promise<!proto.identity.auth.UserIdentitiesResponse>}
  *     Promise that resolves to the response
  */
-proto.identity.auth.IdentityClientServicePromiseClient.prototype.findUserIdentity =
+proto.identity.auth.IdentityClientServicePromiseClient.prototype.findUserIdentities =
     function(request, metadata) {
   return this.client_.unaryCall(this.hostname_ +
-      '/identity.auth.IdentityClientService/FindUserIdentity',
+      '/identity.auth.IdentityClientService/FindUserIdentities',
       request,
       metadata || {},
-      methodDescriptor_IdentityClientService_FindUserIdentity);
+      methodDescriptor_IdentityClientService_FindUserIdentities);
 };
 
 
diff --git a/web/protobufs/identity-auth-client.cjs.flow b/web/protobufs/identity-auth-client.cjs.flow
--- a/web/protobufs/identity-auth-client.cjs.flow
+++ b/web/protobufs/identity-auth-client.cjs.flow
@@ -123,12 +123,12 @@
                response: identityStructs.Empty) => void
   ): grpcWeb.ClientReadableStream<identityStructs.Empty>;
 
-  findUserIdentity(
-    request: identityAuthStructs.UserIdentityRequest,
+  findUserIdentities(
+    request: identityAuthStructs.UserIdentitiesRequest,
     metadata: grpcWeb.Metadata | void,
     callback: (err: grpcWeb.RpcError,
-               response: identityAuthStructs.UserIdentityResponse) => void
-  ): grpcWeb.ClientReadableStream<identityAuthStructs.UserIdentityResponse>;
+               response: identityAuthStructs.UserIdentitiesResponse) => void
+  ): grpcWeb.ClientReadableStream<identityAuthStructs.UserIdentitiesResponse>;
 }
 
 declare export class IdentityClientServicePromiseClient {
@@ -216,8 +216,9 @@
     metadata?: grpcWeb.Metadata
   ): Promise<identityStructs.Empty>;
 
-  findUserIdentity(
-    request: identityAuthStructs.UserIdentityRequest,
+  findUserIdentities(
+    request: identityAuthStructs.UserIdentitiesRequest,
     metadata?: grpcWeb.Metadata
-  ): Promise<identityAuthStructs.UserIdentityResponse>;
+  ): Promise<identityAuthStructs.UserIdentitiesResponse>;
 }
+
diff --git a/web/protobufs/identity-auth-structs.cjs b/web/protobufs/identity-auth-structs.cjs
--- a/web/protobufs/identity-auth-structs.cjs
+++ b/web/protobufs/identity-auth-structs.cjs
@@ -47,8 +47,8 @@
 goog.exportSymbol('proto.identity.auth.UpdateUserPasswordStartRequest', null, global);
 goog.exportSymbol('proto.identity.auth.UpdateUserPasswordStartResponse', null, global);
 goog.exportSymbol('proto.identity.auth.UploadOneTimeKeysRequest', null, global);
-goog.exportSymbol('proto.identity.auth.UserIdentityRequest', null, global);
-goog.exportSymbol('proto.identity.auth.UserIdentityResponse', null, global);
+goog.exportSymbol('proto.identity.auth.UserIdentitiesRequest', null, global);
+goog.exportSymbol('proto.identity.auth.UserIdentitiesResponse', null, global);
 /**
  * Generated by JsPbCodeGenerator.
  * @param {Array=} opt_data Optional initial data array, typically from a
@@ -542,16 +542,16 @@
  * @extends {jspb.Message}
  * @constructor
  */
-proto.identity.auth.UserIdentityRequest = function(opt_data) {
-  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+proto.identity.auth.UserIdentitiesRequest = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, proto.identity.auth.UserIdentitiesRequest.repeatedFields_, null);
 };
-goog.inherits(proto.identity.auth.UserIdentityRequest, jspb.Message);
+goog.inherits(proto.identity.auth.UserIdentitiesRequest, jspb.Message);
 if (goog.DEBUG && !COMPILED) {
   /**
    * @public
    * @override
    */
-  proto.identity.auth.UserIdentityRequest.displayName = 'proto.identity.auth.UserIdentityRequest';
+  proto.identity.auth.UserIdentitiesRequest.displayName = 'proto.identity.auth.UserIdentitiesRequest';
 }
 /**
  * Generated by JsPbCodeGenerator.
@@ -563,16 +563,16 @@
  * @extends {jspb.Message}
  * @constructor
  */
-proto.identity.auth.UserIdentityResponse = function(opt_data) {
+proto.identity.auth.UserIdentitiesResponse = function(opt_data) {
   jspb.Message.initialize(this, opt_data, 0, -1, null, null);
 };
-goog.inherits(proto.identity.auth.UserIdentityResponse, jspb.Message);
+goog.inherits(proto.identity.auth.UserIdentitiesResponse, jspb.Message);
 if (goog.DEBUG && !COMPILED) {
   /**
    * @public
    * @override
    */
-  proto.identity.auth.UserIdentityResponse.displayName = 'proto.identity.auth.UserIdentityResponse';
+  proto.identity.auth.UserIdentitiesResponse.displayName = 'proto.identity.auth.UserIdentitiesResponse';
 }
 
 
@@ -797,7 +797,8 @@
 proto.identity.auth.Identity.toObject = function(includeInstance, msg) {
   var f, obj = {
     username: jspb.Message.getFieldWithDefault(msg, 1, ""),
-    ethIdentity: (f = msg.getEthIdentity()) && proto.identity.auth.EthereumIdentity.toObject(includeInstance, f)
+    ethIdentity: (f = msg.getEthIdentity()) && proto.identity.auth.EthereumIdentity.toObject(includeInstance, f),
+    farcasterId: jspb.Message.getFieldWithDefault(msg, 3, "")
   };
 
   if (includeInstance) {
@@ -843,6 +844,10 @@
       reader.readMessage(value,proto.identity.auth.EthereumIdentity.deserializeBinaryFromReader);
       msg.setEthIdentity(value);
       break;
+    case 3:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setFarcasterId(value);
+      break;
     default:
       reader.skipField();
       break;
@@ -887,6 +892,13 @@
       proto.identity.auth.EthereumIdentity.serializeBinaryToWriter
     );
   }
+  f = /** @type {string} */ (jspb.Message.getField(message, 3));
+  if (f != null) {
+    writer.writeString(
+      3,
+      f
+    );
+  }
 };
 
 
@@ -945,6 +957,42 @@
 };
 
 
+/**
+ * optional string farcaster_id = 3;
+ * @return {string}
+ */
+proto.identity.auth.Identity.prototype.getFarcasterId = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.identity.auth.Identity} returns this
+ */
+proto.identity.auth.Identity.prototype.setFarcasterId = function(value) {
+  return jspb.Message.setField(this, 3, value);
+};
+
+
+/**
+ * Clears the field making it undefined.
+ * @return {!proto.identity.auth.Identity} returns this
+ */
+proto.identity.auth.Identity.prototype.clearFarcasterId = function() {
+  return jspb.Message.setField(this, 3, undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.identity.auth.Identity.prototype.hasFarcasterId = function() {
+  return jspb.Message.getField(this, 3) != null;
+};
+
+
 
 /**
  * List of repeated fields within this message type.
@@ -4715,6 +4763,13 @@
 
 
 
+/**
+ * List of repeated fields within this message type.
+ * @private {!Array<number>}
+ * @const
+ */
+proto.identity.auth.UserIdentitiesRequest.repeatedFields_ = [1];
+
 
 
 if (jspb.Message.GENERATE_TO_OBJECT) {
@@ -4730,8 +4785,8 @@
  *     http://goto/soy-param-migration
  * @return {!Object}
  */
-proto.identity.auth.UserIdentityRequest.prototype.toObject = function(opt_includeInstance) {
-  return proto.identity.auth.UserIdentityRequest.toObject(opt_includeInstance, this);
+proto.identity.auth.UserIdentitiesRequest.prototype.toObject = function(opt_includeInstance) {
+  return proto.identity.auth.UserIdentitiesRequest.toObject(opt_includeInstance, this);
 };
 
 
@@ -4740,13 +4795,13 @@
  * @param {boolean|undefined} includeInstance Deprecated. Whether to include
  *     the JSPB instance for transitional soy proto support:
  *     http://goto/soy-param-migration
- * @param {!proto.identity.auth.UserIdentityRequest} msg The msg instance to transform.
+ * @param {!proto.identity.auth.UserIdentitiesRequest} msg The msg instance to transform.
  * @return {!Object}
  * @suppress {unusedLocalVariables} f is only used for nested messages
  */
-proto.identity.auth.UserIdentityRequest.toObject = function(includeInstance, msg) {
+proto.identity.auth.UserIdentitiesRequest.toObject = function(includeInstance, msg) {
   var f, obj = {
-    userId: jspb.Message.getFieldWithDefault(msg, 1, "")
+    userIdsList: (f = jspb.Message.getRepeatedField(msg, 1)) == null ? undefined : f
   };
 
   if (includeInstance) {
@@ -4760,23 +4815,23 @@
 /**
  * Deserializes binary data (in protobuf wire format).
  * @param {jspb.ByteSource} bytes The bytes to deserialize.
- * @return {!proto.identity.auth.UserIdentityRequest}
+ * @return {!proto.identity.auth.UserIdentitiesRequest}
  */
-proto.identity.auth.UserIdentityRequest.deserializeBinary = function(bytes) {
+proto.identity.auth.UserIdentitiesRequest.deserializeBinary = function(bytes) {
   var reader = new jspb.BinaryReader(bytes);
-  var msg = new proto.identity.auth.UserIdentityRequest;
-  return proto.identity.auth.UserIdentityRequest.deserializeBinaryFromReader(msg, reader);
+  var msg = new proto.identity.auth.UserIdentitiesRequest;
+  return proto.identity.auth.UserIdentitiesRequest.deserializeBinaryFromReader(msg, reader);
 };
 
 
 /**
  * Deserializes binary data (in protobuf wire format) from the
  * given reader into the given message object.
- * @param {!proto.identity.auth.UserIdentityRequest} msg The message object to deserialize into.
+ * @param {!proto.identity.auth.UserIdentitiesRequest} msg The message object to deserialize into.
  * @param {!jspb.BinaryReader} reader The BinaryReader to use.
- * @return {!proto.identity.auth.UserIdentityRequest}
+ * @return {!proto.identity.auth.UserIdentitiesRequest}
  */
-proto.identity.auth.UserIdentityRequest.deserializeBinaryFromReader = function(msg, reader) {
+proto.identity.auth.UserIdentitiesRequest.deserializeBinaryFromReader = function(msg, reader) {
   while (reader.nextField()) {
     if (reader.isEndGroup()) {
       break;
@@ -4785,7 +4840,7 @@
     switch (field) {
     case 1:
       var value = /** @type {string} */ (reader.readString());
-      msg.setUserId(value);
+      msg.addUserIds(value);
       break;
     default:
       reader.skipField();
@@ -4800,9 +4855,9 @@
  * Serializes the message to binary data (in protobuf wire format).
  * @return {!Uint8Array}
  */
-proto.identity.auth.UserIdentityRequest.prototype.serializeBinary = function() {
+proto.identity.auth.UserIdentitiesRequest.prototype.serializeBinary = function() {
   var writer = new jspb.BinaryWriter();
-  proto.identity.auth.UserIdentityRequest.serializeBinaryToWriter(this, writer);
+  proto.identity.auth.UserIdentitiesRequest.serializeBinaryToWriter(this, writer);
   return writer.getResultBuffer();
 };
 
@@ -4810,15 +4865,15 @@
 /**
  * Serializes the given message to binary data (in protobuf wire
  * format), writing to the given BinaryWriter.
- * @param {!proto.identity.auth.UserIdentityRequest} message
+ * @param {!proto.identity.auth.UserIdentitiesRequest} message
  * @param {!jspb.BinaryWriter} writer
  * @suppress {unusedLocalVariables} f is only used for nested messages
  */
-proto.identity.auth.UserIdentityRequest.serializeBinaryToWriter = function(message, writer) {
+proto.identity.auth.UserIdentitiesRequest.serializeBinaryToWriter = function(message, writer) {
   var f = undefined;
-  f = message.getUserId();
+  f = message.getUserIdsList();
   if (f.length > 0) {
-    writer.writeString(
+    writer.writeRepeatedString(
       1,
       f
     );
@@ -4827,20 +4882,39 @@
 
 
 /**
- * optional string user_id = 1;
- * @return {string}
+ * repeated string user_ids = 1;
+ * @return {!Array<string>}
  */
-proto.identity.auth.UserIdentityRequest.prototype.getUserId = function() {
-  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
+proto.identity.auth.UserIdentitiesRequest.prototype.getUserIdsList = function() {
+  return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 1));
+};
+
+
+/**
+ * @param {!Array<string>} value
+ * @return {!proto.identity.auth.UserIdentitiesRequest} returns this
+ */
+proto.identity.auth.UserIdentitiesRequest.prototype.setUserIdsList = function(value) {
+  return jspb.Message.setField(this, 1, value || []);
 };
 
 
 /**
  * @param {string} value
- * @return {!proto.identity.auth.UserIdentityRequest} returns this
+ * @param {number=} opt_index
+ * @return {!proto.identity.auth.UserIdentitiesRequest} returns this
  */
-proto.identity.auth.UserIdentityRequest.prototype.setUserId = function(value) {
-  return jspb.Message.setProto3StringField(this, 1, value);
+proto.identity.auth.UserIdentitiesRequest.prototype.addUserIds = function(value, opt_index) {
+  return jspb.Message.addToRepeatedField(this, 1, value, opt_index);
+};
+
+
+/**
+ * Clears the list making it empty but non-null.
+ * @return {!proto.identity.auth.UserIdentitiesRequest} returns this
+ */
+proto.identity.auth.UserIdentitiesRequest.prototype.clearUserIdsList = function() {
+  return this.setUserIdsList([]);
 };
 
 
@@ -4860,8 +4934,8 @@
  *     http://goto/soy-param-migration
  * @return {!Object}
  */
-proto.identity.auth.UserIdentityResponse.prototype.toObject = function(opt_includeInstance) {
-  return proto.identity.auth.UserIdentityResponse.toObject(opt_includeInstance, this);
+proto.identity.auth.UserIdentitiesResponse.prototype.toObject = function(opt_includeInstance) {
+  return proto.identity.auth.UserIdentitiesResponse.toObject(opt_includeInstance, this);
 };
 
 
@@ -4870,13 +4944,13 @@
  * @param {boolean|undefined} includeInstance Deprecated. Whether to include
  *     the JSPB instance for transitional soy proto support:
  *     http://goto/soy-param-migration
- * @param {!proto.identity.auth.UserIdentityResponse} msg The msg instance to transform.
+ * @param {!proto.identity.auth.UserIdentitiesResponse} msg The msg instance to transform.
  * @return {!Object}
  * @suppress {unusedLocalVariables} f is only used for nested messages
  */
-proto.identity.auth.UserIdentityResponse.toObject = function(includeInstance, msg) {
+proto.identity.auth.UserIdentitiesResponse.toObject = function(includeInstance, msg) {
   var f, obj = {
-    identity: (f = msg.getIdentity()) && proto.identity.auth.Identity.toObject(includeInstance, f)
+    identitiesMap: (f = msg.getIdentitiesMap()) ? f.toObject(includeInstance, proto.identity.auth.Identity.toObject) : []
   };
 
   if (includeInstance) {
@@ -4890,23 +4964,23 @@
 /**
  * Deserializes binary data (in protobuf wire format).
  * @param {jspb.ByteSource} bytes The bytes to deserialize.
- * @return {!proto.identity.auth.UserIdentityResponse}
+ * @return {!proto.identity.auth.UserIdentitiesResponse}
  */
-proto.identity.auth.UserIdentityResponse.deserializeBinary = function(bytes) {
+proto.identity.auth.UserIdentitiesResponse.deserializeBinary = function(bytes) {
   var reader = new jspb.BinaryReader(bytes);
-  var msg = new proto.identity.auth.UserIdentityResponse;
-  return proto.identity.auth.UserIdentityResponse.deserializeBinaryFromReader(msg, reader);
+  var msg = new proto.identity.auth.UserIdentitiesResponse;
+  return proto.identity.auth.UserIdentitiesResponse.deserializeBinaryFromReader(msg, reader);
 };
 
 
 /**
  * Deserializes binary data (in protobuf wire format) from the
  * given reader into the given message object.
- * @param {!proto.identity.auth.UserIdentityResponse} msg The message object to deserialize into.
+ * @param {!proto.identity.auth.UserIdentitiesResponse} msg The message object to deserialize into.
  * @param {!jspb.BinaryReader} reader The BinaryReader to use.
- * @return {!proto.identity.auth.UserIdentityResponse}
+ * @return {!proto.identity.auth.UserIdentitiesResponse}
  */
-proto.identity.auth.UserIdentityResponse.deserializeBinaryFromReader = function(msg, reader) {
+proto.identity.auth.UserIdentitiesResponse.deserializeBinaryFromReader = function(msg, reader) {
   while (reader.nextField()) {
     if (reader.isEndGroup()) {
       break;
@@ -4914,9 +4988,10 @@
     var field = reader.getFieldNumber();
     switch (field) {
     case 1:
-      var value = new proto.identity.auth.Identity;
-      reader.readMessage(value,proto.identity.auth.Identity.deserializeBinaryFromReader);
-      msg.setIdentity(value);
+      var value = msg.getIdentitiesMap();
+      reader.readMessage(value, function(message, reader) {
+        jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readMessage, proto.identity.auth.Identity.deserializeBinaryFromReader, "", new proto.identity.auth.Identity());
+         });
       break;
     default:
       reader.skipField();
@@ -4931,9 +5006,9 @@
  * Serializes the message to binary data (in protobuf wire format).
  * @return {!Uint8Array}
  */
-proto.identity.auth.UserIdentityResponse.prototype.serializeBinary = function() {
+proto.identity.auth.UserIdentitiesResponse.prototype.serializeBinary = function() {
   var writer = new jspb.BinaryWriter();
-  proto.identity.auth.UserIdentityResponse.serializeBinaryToWriter(this, writer);
+  proto.identity.auth.UserIdentitiesResponse.serializeBinaryToWriter(this, writer);
   return writer.getResultBuffer();
 };
 
@@ -4941,57 +5016,39 @@
 /**
  * Serializes the given message to binary data (in protobuf wire
  * format), writing to the given BinaryWriter.
- * @param {!proto.identity.auth.UserIdentityResponse} message
+ * @param {!proto.identity.auth.UserIdentitiesResponse} message
  * @param {!jspb.BinaryWriter} writer
  * @suppress {unusedLocalVariables} f is only used for nested messages
  */
-proto.identity.auth.UserIdentityResponse.serializeBinaryToWriter = function(message, writer) {
+proto.identity.auth.UserIdentitiesResponse.serializeBinaryToWriter = function(message, writer) {
   var f = undefined;
-  f = message.getIdentity();
-  if (f != null) {
-    writer.writeMessage(
-      1,
-      f,
-      proto.identity.auth.Identity.serializeBinaryToWriter
-    );
+  f = message.getIdentitiesMap(true);
+  if (f && f.getLength() > 0) {
+    f.serializeBinary(1, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeMessage, proto.identity.auth.Identity.serializeBinaryToWriter);
   }
 };
 
 
 /**
- * optional Identity identity = 1;
- * @return {?proto.identity.auth.Identity}
- */
-proto.identity.auth.UserIdentityResponse.prototype.getIdentity = function() {
-  return /** @type{?proto.identity.auth.Identity} */ (
-    jspb.Message.getWrapperField(this, proto.identity.auth.Identity, 1));
-};
-
-
-/**
- * @param {?proto.identity.auth.Identity|undefined} value
- * @return {!proto.identity.auth.UserIdentityResponse} returns this
-*/
-proto.identity.auth.UserIdentityResponse.prototype.setIdentity = function(value) {
-  return jspb.Message.setWrapperField(this, 1, value);
-};
-
-
-/**
- * Clears the message field making it undefined.
- * @return {!proto.identity.auth.UserIdentityResponse} returns this
+ * map<string, Identity> identities = 1;
+ * @param {boolean=} opt_noLazyCreate Do not create the map if
+ * empty, instead returning `undefined`
+ * @return {!jspb.Map<string,!proto.identity.auth.Identity>}
  */
-proto.identity.auth.UserIdentityResponse.prototype.clearIdentity = function() {
-  return this.setIdentity(undefined);
+proto.identity.auth.UserIdentitiesResponse.prototype.getIdentitiesMap = function(opt_noLazyCreate) {
+  return /** @type {!jspb.Map<string,!proto.identity.auth.Identity>} */ (
+      jspb.Message.getMapField(this, 1, opt_noLazyCreate,
+      proto.identity.auth.Identity));
 };
 
 
 /**
- * Returns whether this field is set.
- * @return {boolean}
+ * Clears values from the map. The map will be non-null.
+ * @return {!proto.identity.auth.UserIdentitiesResponse} returns this
  */
-proto.identity.auth.UserIdentityResponse.prototype.hasIdentity = function() {
-  return jspb.Message.getField(this, 1) != null;
+proto.identity.auth.UserIdentitiesResponse.prototype.clearIdentitiesMap = function() {
+  this.getIdentitiesMap().clear();
+  return this;
 };
 
 
diff --git a/web/protobufs/identity-auth-structs.cjs.flow b/web/protobufs/identity-auth-structs.cjs.flow
--- a/web/protobufs/identity-auth-structs.cjs.flow
+++ b/web/protobufs/identity-auth-structs.cjs.flow
@@ -42,6 +42,11 @@
   hasEthIdentity(): boolean;
   clearEthIdentity(): Identity;
 
+  getFarcasterId(): string;
+  setFarcasterId(value: string): Identity;
+  hasFarcasterId(): boolean;
+  clearFarcasterId(): Identity;
+
   serializeBinary(): Uint8Array;
   toObject(includeInstance?: boolean): IdentityObject;
   static toObject(includeInstance: boolean, msg: Identity): IdentityObject;
@@ -53,6 +58,7 @@
 export type IdentityObject = {
   username: string,
   ethIdentity: ?EthereumIdentityObject,
+  farcasterId: ?string,
 }
 
 declare export class UploadOneTimeKeysRequest extends Message {
@@ -507,36 +513,36 @@
   farcasterId: string,
 }
 
-declare export class UserIdentityRequest extends Message {
-  getUserId(): string;
-  setUserId(value: string): UserIdentityRequest;
+declare export class UserIdentitiesRequest extends Message {
+  getUserIdsList(): Array<string>;
+  setUserIdsList(value: Array<string>): UserIdentitiesRequest;
+  clearUserIdsList(): UserIdentitiesRequest;
+  addUserIds(value: string, index?: number): UserIdentitiesRequest;
 
   serializeBinary(): Uint8Array;
-  toObject(includeInstance?: boolean): UserIdentityRequestObject;
-  static toObject(includeInstance: boolean, msg: UserIdentityRequest): UserIdentityRequestObject;
-  static serializeBinaryToWriter(message: UserIdentityRequest, writer: BinaryWriter): void;
-  static deserializeBinary(bytes: Uint8Array): UserIdentityRequest;
-  static deserializeBinaryFromReader(message: UserIdentityRequest, reader: BinaryReader): UserIdentityRequest;
+  toObject(includeInstance?: boolean): UserIdentitiesRequestObject;
+  static toObject(includeInstance: boolean, msg: UserIdentitiesRequest): UserIdentitiesRequestObject;
+  static serializeBinaryToWriter(message: UserIdentitiesRequest, writer: BinaryWriter): void;
+  static deserializeBinary(bytes: Uint8Array): UserIdentitiesRequest;
+  static deserializeBinaryFromReader(message: UserIdentitiesRequest, reader: BinaryReader): UserIdentitiesRequest;
 }
 
-export type UserIdentityRequestObject = {
-  userId: string,
+export type UserIdentitiesRequestObject = {
+  userIdsList: Array<string>,
 }
 
-declare export class UserIdentityResponse extends Message {
-  getIdentity(): Identity | void;
-  setIdentity(value?: Identity): UserIdentityResponse;
-  hasIdentity(): boolean;
-  clearIdentity(): UserIdentityResponse;
+declare export class UserIdentitiesResponse extends Message {
+  getIdentitiesMap(): Map<string, Identity>;
+  clearIdentitiesMap(): UserIdentitiesResponse;
 
   serializeBinary(): Uint8Array;
-  toObject(includeInstance?: boolean): UserIdentityResponseObject;
-  static toObject(includeInstance: boolean, msg: UserIdentityResponse): UserIdentityResponseObject;
-  static serializeBinaryToWriter(message: UserIdentityResponse, writer: BinaryWriter): void;
-  static deserializeBinary(bytes: Uint8Array): UserIdentityResponse;
-  static deserializeBinaryFromReader(message: UserIdentityResponse, reader: BinaryReader): UserIdentityResponse;
+  toObject(includeInstance?: boolean): UserIdentitiesResponseObject;
+  static toObject(includeInstance: boolean, msg: UserIdentitiesResponse): UserIdentitiesResponseObject;
+  static serializeBinaryToWriter(message: UserIdentitiesResponse, writer: BinaryWriter): void;
+  static deserializeBinary(bytes: Uint8Array): UserIdentitiesResponse;
+  static deserializeBinaryFromReader(message: UserIdentitiesResponse, reader: BinaryReader): UserIdentitiesResponse;
 }
 
-export type UserIdentityResponseObject = {
-  identity: ?IdentityObject,
+export type UserIdentitiesResponseObject = {
+  identitiesMap: Array<[string, IdentityObject]>,
 }