diff --git a/desktop/src-tauri/src/commands/media.rs b/desktop/src-tauri/src/commands/media.rs
index bf8692ff70..ed3b340238 100644
--- a/desktop/src-tauri/src/commands/media.rs
+++ b/desktop/src-tauri/src/commands/media.rs
@@ -411,7 +411,7 @@ fn should_retry_legacy_upload(status: reqwest::StatusCode) -> bool {
 }
 
 async fn send_upload_attempt(
-    state: &State<'_, AppState>,
+    state: &AppState,
     url: String,
     auth_header: &str,
     mime: &str,
@@ -455,10 +455,22 @@ async fn send_upload_attempt(
     response.map_err(|error| classify_request_error(&error))
 }
 
+pub(crate) async fn upload_image_bytes(
+    body: Vec<u8>,
+    state: &AppState,
+) -> Result<BlobDescriptor, String> {
+    let mime = detect_and_validate_mime(&body)?;
+    if !mime.starts_with("image/") {
+        return Err("profile avatar must be an image".to_string());
+    }
+    let body = sanitize_image_for_upload(body, &mime)?;
+    do_upload(body, &mime, state, None).await
+}
+
 async fn do_upload(
     body: Vec<u8>,
     mime: &str,
-    state: &State<'_, AppState>,
+    state: &AppState,
     progress: Option<(tauri::AppHandle, String)>,
 ) -> Result<BlobDescriptor, String> {
     let sha256 = hex::encode(Sha256::digest(&body));
@@ -559,7 +571,7 @@ pub async fn upload_media(
 /// files from ever leaving the client on image-only surfaces.
 async fn process_picked_path(
     path: std::path::PathBuf,
-    state: &State<'_, AppState>,
+    state: &AppState,
     images_only: bool,
 ) -> Result<BlobDescriptor, String> {
     // Pin the inode by opening the fd BEFORE spawn_blocking. This prevents a
diff --git a/desktop/src-tauri/src/commands/personas/snapshot/import.rs b/desktop/src-tauri/src/commands/personas/snapshot/import.rs
index 7648941d23..d23efe7730 100644
--- a/desktop/src-tauri/src/commands/personas/snapshot/import.rs
+++ b/desktop/src-tauri/src/commands/personas/snapshot/import.rs
@@ -256,6 +256,24 @@ pub(crate) fn decode_snapshot_from_bytes(
     Ok(snapshot)
 }
 
+async fn materialize_import_avatar<F, Fut>(
+    avatar_data_url: Option<&str>,
+    avatar_url: Option<&str>,
+    upload: F,
+) -> Result<Option<String>, String>
+where
+    F: FnOnce(Vec<u8>) -> Fut,
+    Fut: std::future::Future<Output = Result<String, String>>,
+{
+    let Some(avatar_data_url) = avatar_data_url else {
+        return Ok(avatar_url.map(str::to_string));
+    };
+    let avatar_bytes =
+        crate::managed_agents::agent_snapshot::decode_avatar_data_url(avatar_data_url)
+            .ok_or_else(|| "Snapshot avatar data is malformed.".to_string())?;
+    upload(avatar_bytes).await.map(Some)
+}
+
 // ── `preview_agent_snapshot_import` ──────────────────────────────────────────
 
 /// Decode and validate a snapshot file, returning a preview for the
@@ -354,12 +372,21 @@ pub async fn confirm_agent_snapshot_import(
     )?;
     let minted_parallelism = minted.parallelism;
 
-    // Effective avatar: data URL wins; URL fallback when data URL is absent.
-    let effective_avatar: Option<String> = snapshot
-        .profile
-        .avatar_data_url
-        .clone()
-        .or_else(|| snapshot.profile.avatar_url.clone());
+    // Profile metadata must contain a hosted URL. Inline avatar data can be far
+    // larger than the relay's kind:0 content limit, so upload imported pixels
+    // before minting or persisting the new agent. Failing here keeps import
+    // atomic instead of creating an agent whose profile can never publish.
+    let effective_avatar = materialize_import_avatar(
+        snapshot.profile.avatar_data_url.as_deref(),
+        snapshot.profile.avatar_url.as_deref(),
+        |avatar_bytes| async {
+            crate::commands::media::upload_image_bytes(avatar_bytes, &state)
+                .await
+                .map(|descriptor| descriptor.url)
+                .map_err(|error| format!("Could not upload the imported avatar: {error}"))
+        },
+    )
+    .await?;
 
     // Wire-format string for the persona definition's respond_to field.
     // Omit when it is the default (owner-only) to keep definitions clean.
@@ -711,3 +738,114 @@ async fn submit_engram_event(
     }
     Ok(())
 }
+
+#[cfg(test)]
+mod import_avatar_tests {
+    use super::materialize_import_avatar;
+    use std::cell::Cell;
+
+    #[tokio::test]
+    async fn inline_avatar_is_uploaded_and_replaced_with_hosted_url() {
+        let uploaded = Cell::new(false);
+        let result = materialize_import_avatar(
+            Some("data:image/png;base64,iVBORw0KGgo="),
+            Some("https://sender.invalid/avatar.png"),
+            |bytes| {
+                uploaded.set(true);
+                async move {
+                    assert_eq!(bytes, b"\x89PNG\r\n\x1a\n");
+                    Ok("https://relay.example/media/avatar.png".to_string())
+                }
+            },
+        )
+        .await
+        .unwrap();
+
+        assert!(uploaded.get());
+        assert_eq!(
+            result.as_deref(),
+            Some("https://relay.example/media/avatar.png")
+        );
+    }
+
+    #[tokio::test]
+    async fn hosted_avatar_skips_upload() {
+        let result =
+            materialize_import_avatar(None, Some("https://sender.example/avatar.png"), |_| async {
+                panic!("hosted avatars must not be uploaded")
+            })
+            .await
+            .unwrap();
+
+        assert_eq!(result.as_deref(), Some("https://sender.example/avatar.png"));
+    }
+
+    #[tokio::test]
+    async fn relay_sized_inline_avatar_becomes_bounded_signed_profile() {
+        use base64::{engine::general_purpose::STANDARD, Engine};
+        use image::ImageEncoder;
+        use nostr::JsonUtil;
+
+        let mut pixels = vec![0_u8; 512 * 512 * 4];
+        let mut seed = 0x1234_5678_u32;
+        for byte in &mut pixels {
+            seed ^= seed << 13;
+            seed ^= seed >> 17;
+            seed ^= seed << 5;
+            *byte = seed as u8;
+        }
+        let mut source = Vec::new();
+        image::codecs::png::PngEncoder::new(&mut source)
+            .write_image(&pixels, 512, 512, image::ExtendedColorType::Rgba8)
+            .unwrap();
+        assert!(source.len() > 256 * 1024);
+        let data_url = format!("data:image/png;base64,{}", STANDARD.encode(&source));
+        assert!(data_url.len() > 256 * 1024);
+
+        let avatar = materialize_import_avatar(Some(&data_url), None, |bytes| async move {
+            let mime = crate::commands::media::detect_and_validate_mime(&bytes)?;
+            assert_eq!(mime, "image/png");
+            let sanitized = crate::commands::media::sanitize_image_for_upload(bytes, &mime)?;
+            image::load_from_memory(&sanitized).map_err(|error| error.to_string())?;
+            Ok("https://relay.example/media/avatar.png".to_string())
+        })
+        .await
+        .unwrap()
+        .unwrap();
+
+        let event =
+            crate::events::build_profile(Some("Imported agent"), None, Some(&avatar), None, None)
+                .unwrap()
+                .sign_with_keys(&nostr::Keys::generate())
+                .unwrap();
+        assert!(event.content.len() < 64 * 1024);
+        assert!(!event.content.contains("data:image/"));
+        assert!(event
+            .content
+            .contains("https://relay.example/media/avatar.png"));
+        assert!(event.as_json().len() < 256 * 1024);
+    }
+
+    #[tokio::test]
+    async fn upload_failure_aborts_avatar_materialization() {
+        let result = materialize_import_avatar(
+            Some("data:image/png;base64,iVBORw0KGgo="),
+            None,
+            |_| async { Err("relay upload failed".to_string()) },
+        )
+        .await;
+
+        assert_eq!(result.unwrap_err(), "relay upload failed");
+    }
+
+    #[tokio::test]
+    async fn malformed_inline_avatar_fails_before_upload() {
+        let result =
+            materialize_import_avatar(Some("data:image/png;base64,not-base64!"), None, |_| async {
+                panic!("malformed avatars must not be uploaded")
+            })
+            .await;
+
+        assert_eq!(result.unwrap_err(), "Snapshot avatar data is malformed.");
+    }
+}
