diff --git a/Cargo.lock b/Cargo.lock
index 4299623faad69e00142959ce20afd58650451cfd..15ebd51ea40ecfe4498d14ffc7f91702585641d0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -54,7 +54,7 @@ checksum = "edeef7d7b199195a2d7d7a8155d2d04aee736e60c5c7bdd7097d115369a8817d"
 dependencies = [
  "age-core",
  "base64 0.21.7",
- "bech32",
+ "bech32 0.9.1",
  "chacha20poly1305",
  "cookie-factory",
  "hmac 0.12.1",
@@ -806,6 +806,12 @@ version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
 
+[[package]]
+name = "bech32"
+version = "0.10.0-beta"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea"
+
 [[package]]
 name = "beef"
 version = "0.5.2"
@@ -851,6 +857,20 @@ dependencies = [
  "unicode-normalization",
 ]
 
+[[package]]
+name = "bitcoin"
+version = "0.31.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae"
+dependencies = [
+ "bech32 0.10.0-beta",
+ "bitcoin-internals",
+ "bitcoin_hashes 0.13.0",
+ "hex-conservative",
+ "hex_lit",
+ "secp256k1",
+]
+
 [[package]]
 name = "bitcoin-internals"
 version = "0.2.0"
@@ -1596,6 +1616,12 @@ dependencies = [
  "parking_lot_core",
 ]
 
+[[package]]
+name = "data-encoding"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
 [[package]]
 name = "der"
 version = "0.7.9"
@@ -2351,7 +2377,10 @@ version = "0.4.2"
 dependencies = [
  "age",
  "anyhow",
+ "base64 0.21.7",
+ "bech32 0.9.1",
  "bip39",
+ "bitcoin",
  "bs58",
  "clap",
  "clap_complete",
@@ -2371,12 +2400,16 @@ dependencies = [
  "rstest",
  "scrypt",
  "sea-orm",
+ "secp256k1",
  "serde",
  "serde_json",
+ "sha2 0.10.8",
  "sp-core",
  "sp-runtime",
  "subxt",
  "tokio",
+ "tokio-tungstenite",
+ "url",
 ]
 
 [[package]]
@@ -2618,6 +2651,12 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20"
 
+[[package]]
+name = "hex_lit"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
+
 [[package]]
 name = "hkdf"
 version = "0.12.4"
@@ -5182,6 +5221,8 @@ version = "0.28.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10"
 dependencies = [
+ "bitcoin_hashes 0.13.0",
+ "rand",
  "secp256k1-sys",
 ]
 
@@ -6752,6 +6793,22 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "tokio-tungstenite"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
+dependencies = [
+ "futures-util",
+ "log",
+ "rustls 0.22.4",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "tungstenite",
+ "webpki-roots 0.26.6",
+]
+
 [[package]]
 name = "tokio-util"
 version = "0.7.12"
@@ -6979,6 +7036,27 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
 
+[[package]]
+name = "tungstenite"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http 1.1.0",
+ "httparse",
+ "log",
+ "rand",
+ "rustls 0.22.4",
+ "rustls-pki-types",
+ "sha1",
+ "thiserror",
+ "url",
+ "utf-8",
+]
+
 [[package]]
 name = "twox-hash"
 version = "1.6.3"
@@ -7124,6 +7202,12 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
 [[package]]
 name = "utf8parse"
 version = "0.2.2"
diff --git a/Cargo.toml b/Cargo.toml
index 2e952bc9668c455c9fe0582ad789672a5734250c..cef19f6105f8c0473498ab230c9d2f9b82126965 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,8 @@ subxt = { git = 'https://github.com/duniter/subxt', branch = 'subxt-v0.37.0-duni
     "native",
     "jsonrpsee",
 ] }
+base64 = "0.21.0" 
+bitcoin = "0.31.1" # ou une version compatible
 
 # substrate primitives dependencies
 sp-core = { git = "https://github.com/duniter/duniter-polkadot-sdk.git", branch = "duniter-substrate-v1.14.0" }
@@ -41,12 +43,17 @@ inquire = "^0.7.5"
 serde = { version = "^1.0", features = ["derive"] }
 serde_json = "^1.0.128"
 tokio = { version = "^1.40.0", features = ["macros"] }
+tokio-tungstenite = { version = "0.21.0", features = ["rustls-tls-webpki-roots"] }
 confy = "^0.5.1"
 bs58 = "^0.5.1"
 directories = "^5.0.1"
 comfy-table = "^7.1.1"
 sea-orm = { version = "1.1.0", features = [ "sqlx-sqlite", "runtime-tokio-native-tls", "macros" ] }
 indoc = "2.0.5"
+bech32 = "^0.9.1"
+sha2 = "0.10.8"
+secp256k1 = { version = "0.28.2", features = ["rand", "recovery"] }
+url = "2.5.0"
 
 # crypto
 scrypt = { version = "^0.11", default-features = false } # for old-style key generation
diff --git a/examples/profile.examples.md b/examples/profile.examples.md
new file mode 100644
index 0000000000000000000000000000000000000000..ec2a23c56351194b4ea200da6d6d9ee4b6580fd1
--- /dev/null
+++ b/examples/profile.examples.md
@@ -0,0 +1,193 @@
+# Exemples d'utilisation de la commande `gcli profile`
+
+## Notes importantes
+- L'option `-o json` doit toujours être placée au début de la commande, juste après `gcli`
+- L'option `--no-password` doit être placée avant la sous-commande `profile`
+- Le relais par défaut est `wss://relay.copylaradio.com`
+- Les clés sont stockées dans un coffre-fort local
+
+## Exemple complet : Gestion du profil de TOTO
+
+### 1. Import de la clé G1v1
+```bash
+# Import de la clé G1v1 de TOTO
+gcli -o json --no-password vault import -S g1v1 --g1v1-id "toto" --g1v1-password "toto" --no-password -n TOTO
+```
+
+### 2. Configuration du profil
+```bash
+# Définition du profil avec informations personnelles
+gcli -o json --no-password profile set \
+  --name "TOTO" \
+  --display-name "TOTO le Super" \
+  --about "Développeur passionné de cryptographie" \
+  --picture "https://example.com/toto.jpg" \
+  --website "https://toto.dev" \
+  --github "toto-dev" \
+  --twitter "toto_crypto" \
+  --mastodon "mastodon.social/@toto" \
+  --ipfs-gw "https://ipfs.io" \
+  --ipns-vault "/ipns/QmNostrVaultKey" \
+  --zencard "zencard_address" \
+  --tw-feed "tw_feed_key"
+```
+
+### 3. Vérification du profil
+```bash
+# Vérification que le profil a bien été publié
+gcli -o json --no-password profile get
+```
+
+### 4. Export des clés jumelles
+```bash
+# Export des clés IPFS
+gcli -o json --no-password profile export --format ipfs
+
+# Export des clés Nostr
+gcli -o json --no-password profile export --format nostr
+
+# Export des clés Bitcoin
+gcli -o json --no-password profile export --format bitcoin
+
+# Export des clés Monero
+gcli -o json --no-password profile export --format monero
+
+# Export des clés Duniter v1
+gcli -o json --no-password profile export --format g1v1
+
+# Export des clés Duniter v2
+gcli -o json --no-password profile export --format g1v2
+```
+
+### 5. Suppression du profil (si nécessaire)
+```bash
+# Suppression du profil
+gcli -o json --no-password profile remove
+```
+
+### Champs supplémentaires créés par make_NOSTRCARD.sh
+
+Le script `make_NOSTRCARD.sh` ajoute automatiquement plusieurs champs spéciaux au profil Nostr :
+
+1. **Clés jumelles** (twin_keys) :
+   - `ipns` : Clé IPNS pour le stockage personnel
+   - `bitcoin` : Adresse Bitcoin jumelle
+   - `monero` : Adresse Monero jumelle
+
+2. **Tags NIP-39** pour les identités externes :
+   - `g1pubv2` : Clé publique Duniter v2 (format SS58)
+   - `g1pub` : Clé publique Duniter v1
+   - `ipfs_gw` : URL de la passerelle IPFS
+   - `ipns_vault` : Clé IPNS du coffre-fort
+   - `zencard` : Adresse ZenCard
+   - `tw_feed` : Clé IPNS du flux
+
+3. **Métadonnées de localisation** :
+   - `ZUMAP` : Coordonnées géographiques (latitude/longitude)
+   - `GPS` : Données GPS formatées
+
+4. **Informations de sécurité** :
+   - `TODATE` : Horodatage de création
+   - `LANG` : Langue préférée
+   - `HEX` : Clé publique en format hexadécimal
+   - `NPUB` : Clé publique en format bech32
+
+5. **QR Codes générés** :
+   - `IPNS.QR.png` : QR code pour l'accès au coffre-fort IPNS
+   - `G1PUBNOSTR.QR.png` : QR code de l'identité G1
+   - `scan_${MOATS}.png` : Photo de profil scannée
+
+Ces champs sont automatiquement ajoutés lors de la création du profil via `make_NOSTRCARD.sh` et sont visibles dans la sortie JSON de la commande `profile get`.
+
+## 1. Récupérer un profil (`get`)
+
+```bash
+# Récupérer le profil depuis le relais par défaut
+gcli profile get
+
+# Récupérer le profil depuis un relais spécifique
+gcli profile get --relay wss://relay.example.com
+
+# Récupérer le profil au format JSON
+gcli -o json profile get
+
+# Récupérer le profil sans mot de passe
+gcli --no-password profile get
+```
+
+## 2. Définir un profil (`set`)
+
+```bash
+# Définir le nom et la description
+gcli profile set --name "Alice" --about "Nostr user"
+
+# Définir l'image de profil et la bannière
+gcli profile set --picture "https://example.com/avatar.jpg" --banner "https://example.com/banner.jpg"
+
+# Définir les identités sociales
+gcli profile set --github "alice" --twitter "alice" --mastodon "mastodon.social/@alice"
+
+# Définir les clés jumelles
+gcli profile set --ipfs-gw "https://ipfs.io" --ipns-vault "key123" --zencard "address123"
+
+# Publier sur un relais spécifique
+gcli profile set --name "Alice" --relay wss://relay.example.com
+
+# Publier sans mot de passe
+gcli --no-password profile set --name "Alice"
+
+# Publier au format JSON
+gcli -o json profile set --name "Alice"
+```
+
+## 3. Supprimer un profil (`remove`)
+
+```bash
+# Supprimer le profil du relais par défaut
+gcli profile remove
+
+# Supprimer le profil de plusieurs relais
+gcli profile remove --relays "wss://relay1.com,wss://relay2.com"
+
+# Supprimer sans mot de passe
+gcli --no-password profile remove
+
+# Supprimer au format JSON
+gcli -o json profile remove
+```
+
+## 4. Exporter les clés (`export`)
+
+```bash
+# Exporter les clés Nostr
+gcli profile export --format nostr
+
+# Exporter les clés IPFS
+gcli profile export --format ipfs
+
+# Exporter les clés Bitcoin
+gcli profile export --format bitcoin
+
+# Exporter les clés Monero
+gcli profile export --format monero
+
+# Exporter les clés Duniter v1
+gcli profile export --format g1v1
+
+# Exporter les clés Duniter v2
+gcli profile export --format g1v2
+
+# Exporter sans mot de passe
+gcli --no-password profile export --format nostr
+
+# Exporter au format JSON
+gcli -o json profile export --format nostr
+
+# Exporter sans mot de passe au format JSON
+gcli -o json --no-password profile export --format nostr
+```
+
+## Notes supplémentaires
+- Pour la commande `set`, si aucune option n'est fournie, l'aide s'affiche
+- Pour la commande `export`, si le format n'est pas reconnu, la liste des formats disponibles s'affiche
+- Les formats d'export disponibles sont : `ipfs`, `nostr`, `bitcoin`, `monero`, `g1v1`, `g1v2` 
\ No newline at end of file
diff --git a/examples/vault.examples.md b/examples/vault.examples.md
new file mode 100644
index 0000000000000000000000000000000000000000..dec5d16a970101995aee5b9d5dd71966fe8888bc
--- /dev/null
+++ b/examples/vault.examples.md
@@ -0,0 +1,137 @@
+# Exemples d'utilisation de la commande `vault`
+
+La commande `vault` permet de gérer les clés et les comptes dans le coffre-fort. Voici les différentes sous-commandes disponibles :
+
+## Lister les comptes
+
+### Lister tous les comptes
+```bash
+# Format humain (par défaut)
+gcli vault list all
+
+# Afficher les clés publiques G1v1 pour les clés ed25519
+gcli vault list all --show-g1v1
+```
+
+### Lister uniquement les comptes de base
+```bash
+# Format humain (par défaut)
+gcli vault list base
+
+# Afficher les clés publiques G1v1 pour les clés ed25519
+gcli vault list base --show-g1v1
+```
+
+### Lister les comptes liés à une adresse spécifique
+```bash
+# Par adresse SS58
+gcli vault list for -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
+
+# Par nom de compte
+gcli vault list for -v MonCompte
+
+# Afficher les clés publiques G1v1
+gcli vault list for -v MonCompte --show-g1v1
+```
+
+## Importer une clé
+
+### Import en mode interactif
+```bash
+# Format Substrate (par défaut)
+gcli vault import
+
+# Format Seed
+gcli vault import -S seed
+
+# Format G1v1
+gcli vault import -S g1v1
+```
+
+### Import en mode non-interactif
+```bash
+# Format Substrate avec URI
+gcli vault import -u "bottom drive obey lake curtain smoke basket hold race lonely fit walk//0"
+
+# Format G1v1 avec ID et mot de passe
+gcli vault import -S g1v1 --g1v1-id "mon_id" --g1v1-password "mon_mot_de_passe"
+
+# Sans mot de passe
+gcli vault import -u "bottom drive obey lake curtain smoke basket hold race lonely fit walk" --no-password
+
+# Avec un nom personnalisé
+gcli vault import -u "bottom drive obey lake curtain smoke basket hold race lonely fit walk" -n "MonCompte"
+```
+
+## Dériver un compte
+
+### Dérivation en mode interactif
+```bash
+# Par adresse SS58
+gcli vault derive -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
+
+# Par nom de compte
+gcli vault derive -v MonCompte
+```
+
+### Dérivation en mode non-interactif
+```bash
+# Par adresse SS58 avec chemin de dérivation
+gcli vault derive -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r -d "//0"
+
+# Par nom de compte avec chemin de dérivation
+gcli vault derive -v MonCompte -d "//0"
+
+# Sans mot de passe
+gcli vault derive -v MonCompte -d "//0" --no-password
+
+# Avec un nom personnalisé
+gcli vault derive -v MonCompte -d "//0" -n "MonCompteDerive"
+```
+
+## Renommer un compte
+```bash
+gcli vault rename 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
+```
+
+## Supprimer un compte
+```bash
+# Par adresse SS58
+gcli vault remove -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
+
+# Par nom de compte
+gcli vault remove -v MonCompte
+```
+
+## Inspecter un compte
+```bash
+# Par adresse SS58
+gcli vault inspect -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
+
+# Par nom de compte
+gcli vault inspect -v MonCompte
+```
+
+## Utiliser un compte
+```bash
+# Par adresse SS58
+gcli vault use -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
+
+# Par nom de compte
+gcli vault use -v MonCompte
+```
+
+## Générer une mnémonique
+```bash
+gcli vault generate
+```
+
+## Voir l'emplacement du coffre-fort
+```bash
+gcli vault where
+```
+
+## Migrer les anciens fichiers de clés
+```bash
+gcli vault migrate
+``` 
\ No newline at end of file
diff --git a/src/commands.rs b/src/commands.rs
index 15f96bacd9e9eef0bd51f4edc3ddcb6d864e88b5..a1e0dc895b41dadb61d5af272e9fcbfdb658a793 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -8,6 +8,7 @@ pub mod expire;
 pub mod identity;
 pub mod net_test;
 pub mod oneshot;
+pub mod profile;
 pub mod publish;
 pub mod revocation;
 pub mod runtime;
diff --git a/src/commands/profile.rs b/src/commands/profile.rs
new file mode 100644
index 0000000000000000000000000000000000000000..37970132515a8a945362f6eb31fb772dbf0098e1
--- /dev/null
+++ b/src/commands/profile.rs
@@ -0,0 +1,1788 @@
+use crate::*;
+use anyhow::{anyhow, Result};
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+use std::collections::HashMap;
+use std::time::{SystemTime, UNIX_EPOCH};
+use sp_core::crypto::Pair;
+use sp_core::ByteArray;
+use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
+use futures::{SinkExt, StreamExt};
+use url::Url;
+use sha2::{Sha256, Digest};
+use secp256k1::{Secp256k1, SecretKey, PublicKey};
+use bech32::{self, ToBase32, Variant};
+use sp_core::crypto::Ss58Codec;
+use crate::commands::cesium;
+use bs58;
+use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
+
+/// Derive a Nostr key from a Substrate key
+fn derive_nostr_keys_from_substrate(keypair: &KeyPair) -> Result<(SecretKey, PublicKey), GcliError> {
+    // Get the seed from the keypair - use a more direct approach
+    let seed = match keypair {
+        KeyPair::Sr25519(pair) => {
+            // For Sr25519, we'll use the raw bytes directly
+            let key_bytes = pair.to_raw_vec();
+            // Take the first 32 bytes as the seed
+            let mut seed = [0u8; 32];
+            for i in 0..std::cmp::min(32, key_bytes.len()) {
+                seed[i] = key_bytes[i];
+            }
+            seed.to_vec()
+        }
+        KeyPair::Ed25519(pair) => {
+            // For Ed25519, we'll use the raw bytes directly
+            let key_bytes = pair.to_raw_vec();
+            // Take the first 32 bytes as the seed
+            let mut seed = [0u8; 32];
+            for i in 0..std::cmp::min(32, key_bytes.len()) {
+                seed[i] = key_bytes[i];
+            }
+            seed.to_vec()
+        }
+    };
+    
+    // Derive a seed for secp256k1 by hashing the Ed25519 seed with SHA256, like keygen.py
+    let secp = Secp256k1::new();
+    let mut hasher = Sha256::new();
+    hasher.update(&seed);
+    let secp256k1_seed = hasher.finalize().to_vec();
+
+    let secret_key = SecretKey::from_slice(&secp256k1_seed[0..32])
+        .map_err(|e| anyhow!("Failed to create secp256k1 secret key: {}", e))?;
+    
+    // Derive the public key
+    let public_key = PublicKey::from_secret_key(&secp, &secret_key);
+    
+    Ok((secret_key, public_key))
+}
+
+/// Convert a hex string to bech32 format with the given prefix (npub/nsec)
+fn hex_to_bech32(hex_key: &str, prefix: &str) -> Result<String, GcliError> {
+    // Decode the hex string to bytes
+    let bytes = hex::decode(hex_key)
+        .map_err(|e| anyhow!("Failed to decode hex key: {}", e))?;
+    
+    // Convert bytes to base32
+    let base32_data = bytes.to_base32();
+    
+    // Encode as bech32
+    let bech32_str = bech32::encode(prefix, base32_data, Variant::Bech32)
+        .map_err(|e| anyhow!("Failed to encode bech32: {}", e))?;
+    
+    Ok(bech32_str)
+}
+
+/// Get Nostr public key in hex format
+fn get_nostr_pubkey(keypair: &KeyPair) -> Result<String, GcliError> {
+    let (_, public_key) = derive_nostr_keys_from_substrate(keypair)?;
+    // Nostr uses the x-only public key (32 bytes)
+    let serialized = public_key.serialize();
+    // Skip the first byte (format byte) and take only the x coordinate
+    let pubkey = hex::encode(&serialized[1..33]);
+    Ok(pubkey)
+}
+
+/// Get Nostr public key in bech32 format (npub)
+fn get_nostr_npub(keypair: &KeyPair) -> Result<String, GcliError> {
+    let hex_pubkey = get_nostr_pubkey(keypair)?;
+    let npub = hex_to_bech32(&hex_pubkey, "npub")?;
+    Ok(npub)
+}
+
+/// Get Nostr private key in bech32 format (nsec)
+fn get_nostr_nsec(keypair: &KeyPair) -> Result<String, GcliError> {
+    let (secret_key, _) = derive_nostr_keys_from_substrate(keypair)?;
+    let hex_seckey = hex::encode(secret_key.secret_bytes());
+    let nsec = hex_to_bech32(&hex_seckey, "nsec")?;
+    Ok(nsec)
+}
+
+/// Profile subcommands
+#[derive(Clone, Debug, clap::Parser)]
+pub enum Subcommand {
+    /// Get Nostr profile data from a relay
+    Get {
+        /// Relay URL to fetch profile from
+        #[clap(short, long)]
+        relay: Option<String>,
+    },
+    /// Set Nostr profile data (NIP-01 kind 0 metadata)
+    Set {
+        /// Profile name
+        #[clap(short, long)]
+        name: Option<String>,
+        
+        /// Profile display name
+        #[clap(short = 'd', long)]
+        display_name: Option<String>,
+        
+        /// Profile picture URL
+        #[clap(short = 'p', long)]
+        picture: Option<String>,
+
+        /// Profile banner URL
+        #[clap(short = 'b', long)]
+        banner: Option<String>,
+        
+        /// Profile about/description
+        #[clap(short, long)]
+        about: Option<String>,
+        
+        /// Profile website
+        #[clap(short = 'w', long)]
+        website: Option<String>,
+        
+        /// Profile NIP-05 identifier
+        #[clap(short = 'i', long)]
+        nip05: Option<String>,
+
+        /// GitHub username
+        #[clap(long)]
+        github: Option<String>,
+
+        /// Twitter username
+        #[clap(long)]
+        twitter: Option<String>,
+
+        /// Mastodon identity (format: instance/@username)
+        #[clap(long)]
+        mastodon: Option<String>,
+
+        /// Telegram user ID
+        #[clap(long)]
+        telegram: Option<String>,
+
+        /// IPFS Gateway URL
+        #[clap(long)]
+        ipfs_gw: Option<String>,
+
+        /// NOSTR Card IPNS vault key
+        #[clap(long)]
+        ipns_vault: Option<String>,
+
+        /// ZenCard wallet address
+        #[clap(long)]
+        zencard: Option<String>,
+
+        /// TW Feed IPNS key
+        #[clap(long)]
+        tw_feed: Option<String>,
+        
+        /// Relay URL to publish profile to
+        #[clap(short, long)]
+        relay: Option<String>,
+    },
+    /// Remove Nostr profile and associated events
+    Remove {
+        /// List of relay URLs to remove profile from (comma-separated)
+        #[clap(short, long)]
+        relays: Option<String>,
+    },
+    /// Export keys in different formats
+    Export {
+        /// Export format (ipfs, nostr, bitcoin, monero)
+        #[clap(short, long)]
+        format: String,
+    },
+}
+
+/// Nostr profile metadata (NIP-01 kind 0)
+#[derive(Debug, Serialize, Deserialize)]
+pub struct NostrProfile {
+    pub name: Option<String>,
+    pub display_name: Option<String>,
+    pub picture: Option<String>,
+    pub banner: Option<String>,
+    pub about: Option<String>,
+    pub website: Option<String>,
+    pub nip05: Option<String>,
+    pub bot: Option<bool>,
+    pub twin_keys: Option<TwinKeys>,
+    #[serde(flatten)]
+    pub additional_fields: HashMap<String, String>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TwinKeys {
+    pub ipns: Option<String>,
+    pub bitcoin: Option<String>,
+    pub monero: Option<String>,
+}
+
+impl Default for NostrProfile {
+    fn default() -> Self {
+        Self {
+            name: None,
+            display_name: None,
+            picture: None,
+            banner: None,
+            about: None,
+            website: None,
+            nip05: None,
+            bot: Some(false),
+            twin_keys: Some(TwinKeys {
+                ipns: None,
+                bitcoin: None,
+                monero: None,
+            }),
+            additional_fields: HashMap::new(),
+        }
+    }
+}
+
+/// Nostr event structure
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct NostrEvent {
+    pub id: String,
+    pub pubkey: String,
+    pub created_at: u64,
+    pub kind: u32,
+    pub tags: Vec<Vec<String>>,
+    pub content: String,
+    pub sig: String,
+}
+
+impl NostrEvent {
+    /// Create a new unsigned Nostr event
+    pub fn new(pubkey: String, kind: u32, content: String) -> Self {
+        let created_at = SystemTime::now()
+            .duration_since(UNIX_EPOCH)
+            .unwrap()
+            .as_secs();
+        
+        Self {
+            id: String::new(), // Will be set after serialization
+            pubkey,
+            created_at,
+            kind,
+            tags: Vec::new(),
+            content,
+            sig: String::new(), // Will be set after signing
+        }
+    }
+    
+    /// Calculate the event ID (SHA-256 hash of the serialized event)
+    pub fn calculate_id(&mut self) -> Result<(), GcliError> {
+        // Create a temporary event for serialization exactly as specified by NIP-01
+        // The order is important: [0, pubkey, created_at, kind, tags, content]
+        let temp_event = json!([
+            0,
+            self.pubkey,
+            self.created_at,
+            self.kind,
+            self.tags,
+            self.content
+        ]);
+        
+        // Serialize the event with no whitespace and canonical ordering
+        // Using to_string() instead of to_string_pretty() to avoid whitespace
+        let serialized = serde_json::to_string(&temp_event)
+            .map_err(|e| anyhow!("Failed to serialize event: {}", e))?;
+        
+        // Calculate SHA-256 hash
+        let mut hasher = Sha256::new();
+        hasher.update(serialized.as_bytes());
+        let result = hasher.finalize();
+        
+        // Set the ID
+        self.id = hex::encode(result);
+        Ok(())
+    }
+    
+    /// Sign the event with the given keypair
+    pub fn sign(&mut self, keypair: &KeyPair) -> Result<(), GcliError> {
+        // Calculate ID if not already set
+        if self.id.is_empty() {
+            self.calculate_id()?;
+        }
+        
+        // Derive Nostr keys from Substrate keypair
+        let (secret_key, public_key) = derive_nostr_keys_from_substrate(keypair)?;
+        
+        // Verify that the pubkey in the event matches our derived pubkey
+        let derived_pubkey = hex::encode(&public_key.serialize()[1..33]);
+        if self.pubkey != derived_pubkey {
+            // Update the pubkey to match
+            self.pubkey = derived_pubkey;
+            // Recalculate ID with the correct pubkey
+            self.calculate_id()?;
+        }
+        
+        // Create a secp256k1 context for Schnorr signatures
+        let secp = Secp256k1::new();
+        
+        // Create a message from the event ID
+        let id_bytes = hex::decode(&self.id)
+            .map_err(|e| anyhow!("Failed to decode hex ID: {}", e))?;
+        
+        let message = secp256k1::Message::from_digest_slice(&id_bytes)
+            .map_err(|e| anyhow!("Failed to create secp256k1 message: {}", e))?;
+        
+        // Sign the message with Schnorr
+        let aux_rand = [0u8; 32]; // Use zeros for deterministic signatures
+        let keypair_secp = secp256k1::Keypair::from_secret_key(&secp, &secret_key);
+        let signature = secp256k1::schnorr::Signature::from_slice(
+            secp.sign_schnorr_with_aux_rand(&message, &keypair_secp, &aux_rand).as_ref()
+        ).unwrap();
+        
+        // Set the signature
+        self.sig = hex::encode(signature.as_ref());
+        
+        // Verify the signature
+        let verify_result = verify_nostr_event(self);
+        
+        match verify_result {
+            Ok(true) => {
+                Ok(())
+            },
+            Ok(false) => {
+                self.try_alternative_signing(keypair)
+            },
+            Err(_e) => {
+                self.try_alternative_signing(keypair)
+            }
+        }
+    }
+    
+    /// Try an alternative signing method if the first one fails
+    fn try_alternative_signing(&mut self, keypair: &KeyPair) -> Result<(), GcliError> {
+        // Use a different approach for key derivation
+        let seed = match keypair {
+            KeyPair::Sr25519(pair) => {
+                // Use a hash of the key bytes
+                let mut hasher = Sha256::new();
+                hasher.update(&pair.to_raw_vec());
+                hasher.finalize().to_vec()
+            }
+            KeyPair::Ed25519(pair) => {
+                // For Ed25519, take the first 32 bytes (the seed/private key part in raw format)
+                // and hash it with SHA256, like keygen.py does with its 'seed'.
+                let raw_bytes = pair.to_raw_vec();
+                let ed25519_seed = &raw_bytes[..32];
+                let mut hasher = sha2::Sha256::new();
+                hasher.update(ed25519_seed);
+                hasher.finalize().to_vec()
+            }
+        };
+        
+        // Create a secp256k1 secret key from the seed
+        let secp = Secp256k1::new();
+        let secret_key = SecretKey::from_slice(&seed[0..32])
+            .map_err(|e| anyhow!("Failed to create alternative secp256k1 secret key: {}", e))?;
+        
+        // Derive the public key
+        let public_key = PublicKey::from_secret_key(&secp, &secret_key);
+        
+        // Update the pubkey in the event
+        let derived_pubkey = hex::encode(&public_key.serialize()[1..33]);
+        self.pubkey = derived_pubkey;
+        
+        // Recalculate ID with the new pubkey
+        self.calculate_id()?;
+        
+        // Create a message from the event ID
+        let id_bytes = hex::decode(&self.id)
+            .map_err(|e| anyhow!("Failed to decode hex ID: {}", e))?;
+        
+        let message = secp256k1::Message::from_digest_slice(&id_bytes)
+            .map_err(|e| anyhow!("Failed to create secp256k1 message: {}", e))?;
+        
+        // Sign the message with Schnorr
+        let aux_rand = [0u8; 32]; // Use zeros for deterministic signatures
+        let keypair_secp = secp256k1::Keypair::from_secret_key(&secp, &secret_key);
+        let signature = secp256k1::schnorr::Signature::from_slice(
+            secp.sign_schnorr_with_aux_rand(&message, &keypair_secp, &aux_rand).as_ref()
+        ).unwrap();
+        
+        // Set the signature
+        self.sig = hex::encode(signature.as_ref());
+        
+        // Verify the signature
+        let verify_result = verify_nostr_event(self);
+        
+        match verify_result {
+            Ok(true) => {
+                Ok(())
+            },
+            Ok(false) => {
+                self.try_third_signing_approach(keypair)
+            },
+            Err(_e) => {
+                self.try_third_signing_approach(keypair)
+            }
+        }
+    }
+
+    /// Try a third signing approach if the first two fail
+    fn try_third_signing_approach(&mut self, keypair: &KeyPair) -> Result<(), GcliError> {
+        // Use a more complex derivation approach
+        let seed = match keypair {
+            KeyPair::Sr25519(pair) => {
+                // Use multiple rounds of hashing
+                let mut hasher1 = Sha256::new();
+                hasher1.update("nostr".as_bytes());
+                hasher1.update(&pair.to_raw_vec());
+                let first_hash = hasher1.finalize();
+                
+                let mut hasher2 = Sha256::new();
+                hasher2.update(first_hash);
+                hasher2.finalize().to_vec()
+            }
+            KeyPair::Ed25519(pair) => {
+                // Use multiple rounds of hashing
+                let mut hasher1 = Sha256::new();
+                hasher1.update("nostr".as_bytes());
+                hasher1.update(&pair.to_raw_vec());
+                let first_hash = hasher1.finalize();
+                
+                let mut hasher2 = Sha256::new();
+                hasher2.update(first_hash);
+                hasher2.finalize().to_vec()
+            }
+        };
+        
+        // Create a secp256k1 secret key from the seed
+        let secp = Secp256k1::new();
+        let secret_key = SecretKey::from_slice(&seed[0..32])
+            .map_err(|e| anyhow!("Failed to create third secp256k1 secret key: {}", e))?;
+        
+        // Derive the public key
+        let public_key = PublicKey::from_secret_key(&secp, &secret_key);
+        
+        // Update the pubkey in the event
+        let derived_pubkey = hex::encode(&public_key.serialize()[1..33]);
+        self.pubkey = derived_pubkey;
+        
+        // Recalculate ID with the new pubkey
+        self.calculate_id()?;
+        
+        // Create a message from the event ID
+        let id_bytes = hex::decode(&self.id)
+            .map_err(|e| anyhow!("Failed to decode hex ID: {}", e))?;
+        
+        let message = secp256k1::Message::from_digest_slice(&id_bytes)
+            .map_err(|e| anyhow!("Failed to create secp256k1 message: {}", e))?;
+        
+        // Sign the message with Schnorr
+        let aux_rand = [0u8; 32]; // Use zeros for deterministic signatures
+        let keypair_secp = secp256k1::Keypair::from_secret_key(&secp, &secret_key);
+        let signature = secp256k1::schnorr::Signature::from_slice(
+            secp.sign_schnorr_with_aux_rand(&message, &keypair_secp, &aux_rand).as_ref()
+        ).unwrap();
+        
+        // Set the signature
+        self.sig = hex::encode(signature.as_ref());
+        
+        // Verify the signature
+        let verify_result = verify_nostr_event(self);
+        
+        match verify_result {
+            Ok(true) => {
+                Ok(())
+            },
+            Ok(false) => {
+                Err(anyhow!("All signature approaches failed").into())
+            },
+            Err(e) => {
+                Err(anyhow!("Error verifying third signature: {}", e).into())
+            }
+        }
+    }
+}
+
+/// Verify a Nostr event signature
+fn verify_nostr_event(event: &NostrEvent) -> Result<bool, GcliError> {
+    // Decode the pubkey
+    let pubkey_bytes = hex::decode(&event.pubkey)
+        .map_err(|e| anyhow!("Failed to decode pubkey: {}", e))?;
+    
+    // Create a secp256k1 context
+    let secp = Secp256k1::new();
+    
+    // Create x-only public key from the 32-byte pubkey
+    let xonly_pubkey = secp256k1::XOnlyPublicKey::from_slice(&pubkey_bytes)
+        .map_err(|e| anyhow!("Failed to create x-only public key: {}", e))?;
+    
+    // Decode the ID
+    let id_bytes = hex::decode(&event.id)
+        .map_err(|e| anyhow!("Failed to decode ID: {}", e))?;
+    
+    // Create a message from the ID
+    let message = secp256k1::Message::from_digest_slice(&id_bytes)
+        .map_err(|e| anyhow!("Failed to create message: {}", e))?;
+    
+    // Decode the signature
+    let sig_bytes = hex::decode(&event.sig)
+        .map_err(|e| anyhow!("Failed to decode signature: {}", e))?;
+    
+    // Create a Schnorr signature from the signature bytes
+    let signature = secp256k1::schnorr::Signature::from_slice(&sig_bytes)
+        .map_err(|e| anyhow!("Failed to create Schnorr signature: {}", e))?;
+    
+    // Verify the Schnorr signature
+    match secp.verify_schnorr(&signature, &message, &xonly_pubkey) {
+        Ok(_) => {
+            Ok(true)
+        },
+        Err(_e) => {
+            Ok(false)
+        }
+    }
+}
+
+/// Handle profile commands
+pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
+    // Enable debug logging for this module if not already set
+    if std::env::var("RUST_LOG").is_err() {
+        std::env::set_var("RUST_LOG", "debug");
+        log::debug!("RUST_LOG environment variable set to debug");
+    }
+    
+    match command {
+        Subcommand::Get { relay } => get_profile(data, relay).await,
+        Subcommand::Set {
+            name,
+            display_name,
+            picture,
+            banner,
+            about,
+            website,
+            nip05,
+            github,
+            twitter,
+            mastodon,
+            telegram,
+            ipfs_gw,
+            ipns_vault,
+            zencard,
+            tw_feed,
+            relay,
+        } => {
+            set_profile(
+                data,
+                name,
+                display_name,
+                picture,
+                banner,
+                about,
+                website,
+                nip05,
+                github,
+                twitter,
+                mastodon,
+                telegram,
+                ipfs_gw,
+                ipns_vault,
+                zencard,
+                tw_feed,
+                relay,
+            )
+            .await
+        }
+        Subcommand::Remove { relays } => remove_profile(data, relays).await,
+        Subcommand::Export { format } => export_keys(data, format).await,
+    }
+}
+
+/// Get Nostr profile from a relay
+async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliError> {
+    // Get keypair for signing
+    // Use the configured address or prompt the user if not set
+    let keypair = if data.args.no_password {
+        // If no_password is set, try to get the keypair without prompting
+        match fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await {
+            Ok(pair) => pair,
+            Err(e) => {
+                if data.args.output_format == OutputFormat::Json {
+                    return Err(anyhow!("Failed to get keypair without password: {}", e).into());
+                } else {
+                    return Err(anyhow!("Failed to get keypair without password. Try removing --no-password option.").into());
+                }
+            }
+        }
+    } else {
+        fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await?
+    };
+    
+    // Get Nostr pubkey in hex format (used for protocol)
+    let pubkey = get_nostr_pubkey(&keypair)?;
+    
+    // Get Nostr pubkey in bech32 format (for display)
+    let npub = get_nostr_npub(&keypair)?;
+    
+    if data.args.output_format == OutputFormat::Human {
+        println!("Searching for profile with Nostr pubkey (hex): {}", pubkey);
+        println!("Nostr pubkey (bech32): {}", npub);
+    }
+    
+    // Use default relay if none provided
+    let relay = relay_url.unwrap_or_else(|| {
+        data.cfg
+            .relay
+            .clone()
+            .unwrap_or_else(|| "wss://relay.copylaradio.com".to_string())
+    });
+    
+    // Ensure the relay URL starts with ws:// or wss://
+    let relay = if !relay.starts_with("ws://") && !relay.starts_with("wss://") {
+        format!("wss://{}", relay)
+    } else {
+        relay
+    };
+    
+    // Parse the URL
+    let url = Url::parse(&relay).map_err(|e| anyhow!("Invalid relay URL: {}", e))?;
+    
+    // Connect to the WebSocket
+    if data.args.output_format == OutputFormat::Human {
+        println!("Connecting to relay {}...", relay);
+    }
+    let (mut ws_stream, _) = connect_async(url).await
+        .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?;
+    
+    if data.args.output_format == OutputFormat::Human {
+        println!("Connected to relay, requesting profile...");
+    }
+    
+    // Create subscription request - CORRECTED FORMAT
+    // According to NIP-01, REQ should be an array: ["REQ", <subscription_id>, <filter>, ...]
+    let filter = json!({
+        "kinds": [0],
+        "authors": [pubkey],
+        "limit": 1
+    });
+    
+    // Send the request in the correct format
+    let req_msg = json!(["REQ", "profile-request", filter]);
+    
+    if data.args.output_format == OutputFormat::Human {
+        println!("Sending request: {}", req_msg.to_string());
+    }
+    ws_stream.send(Message::Text(req_msg.to_string())).await
+        .map_err(|e| anyhow!("Failed to send request: {}", e))?;
+    
+    // Wait for response with timeout
+    let mut profile_found = false;
+    let mut profile_content = NostrProfile::default(); // To store parsed content
+    let mut g1pubv2_from_tags: Option<String> = None;
+    let mut g1pub_from_tags: Option<String> = None;
+    let mut event_received: Option<NostrEvent> = None; // Store the whole event for JSON output
+    
+    // Set a timeout for receiving messages
+    let timeout = tokio::time::Duration::from_secs(10); // Increased timeout
+    let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(100));
+    
+    let start_time = tokio::time::Instant::now();
+    
+    while start_time.elapsed() < timeout {
+        interval.tick().await;
+        
+        // Check for messages with a timeout
+        match tokio::time::timeout(
+            tokio::time::Duration::from_millis(100),
+            ws_stream.next()
+        ).await {
+            Ok(Some(Ok(msg))) => {
+                if let Message::Text(text) = msg {
+                    // Parse the message
+                    if let Ok(json) = serde_json::from_str::<Value>(&text) {
+                        
+                        // Check if it's an EVENT message
+                        if let Some(event_type) = json.get(0).and_then(|v| v.as_str()) {
+                            
+                            if event_type == "EVENT" && json.get(1).is_some() && json.get(2).is_some() {
+                                // Attempt to deserialize the whole event first
+                                if let Ok(full_event_obj) = serde_json::from_value::<NostrEvent>(json[2].clone()) {
+                                    event_received = Some(full_event_obj.clone()); // Clone for later JSON output
+
+                                    // Then parse the content string into NostrProfile
+                                    if let Ok(parsed_profile_content) = serde_json::from_str::<NostrProfile>(&full_event_obj.content) {
+                                        profile_content = parsed_profile_content;
+                                        profile_found = true;
+
+                                        // Extract g1pubv2 and g1pub from tags
+                                        for tag_vec in &full_event_obj.tags {
+                                            if tag_vec.len() >= 2 {
+                                                match tag_vec[0].as_str() {
+                                                    "g1pubv2" => g1pubv2_from_tags = Some(tag_vec[1].clone()),
+                                                    "g1pub" => g1pub_from_tags = Some(tag_vec[1].clone()),
+                                                    _ => {}
+                                                }
+                                            }
+                                        }
+                                        let close_msg = json!(["CLOSE", "profile-request"]);
+                                        ws_stream.send(Message::Text(close_msg.to_string())).await
+                                            .map_err(|e| anyhow!("Failed to close subscription: {}", e))?;
+                                        break;
+                                    } else {
+                                        log::warn!("Failed to parse NostrProfile content from event content string.");
+                                    }
+                                } else {
+                                    log::warn!("Failed to parse full NostrEvent object from relay message.");
+                                }
+                            } else if event_type == "EOSE" {
+                                // End of stored events
+                                if !profile_found {
+                                    // No profile found, close the connection
+                                    let close_msg = json!(["CLOSE", "profile-request"]);
+                                    ws_stream.send(Message::Text(close_msg.to_string())).await
+                                        .map_err(|e| anyhow!("Failed to close subscription: {}", e))?;
+                                    
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            Ok(Some(Err(_e))) => {
+                break;
+            },
+            Ok(None) => {
+                // Connection closed
+                break;
+            },
+            Err(_) => {
+                // Timeout, continue
+                continue;
+            }
+        }
+    }
+    
+    // Close the WebSocket connection
+    ws_stream.close(None).await.ok();
+    
+    // Display the profile or a message if not found
+    if profile_found {
+        match data.args.output_format {
+            OutputFormat::Human => {
+                println!("Profile for npub: {}", npub);
+                if let Some(name) = &profile_content.name {
+                    println!("Name: {}", name);
+                }
+                if let Some(display_name) = &profile_content.display_name {
+                    println!("Display Name: {}", display_name);
+                }
+                if let Some(picture) = &profile_content.picture {
+                    println!("Picture: {}", picture);
+                }
+                if let Some(about) = &profile_content.about {
+                    println!("About: {}", about);
+                }
+                if let Some(website) = &profile_content.website {
+                    println!("Website: {}", website);
+                }
+                if let Some(nip05) = &profile_content.nip05 {
+                    println!("NIP-05: {}", nip05);
+                }
+                // Display from tags
+                if let Some(g1pubv2) = &g1pubv2_from_tags {
+                    println!("g1pubv2 (gdev SS58 Tag): {}", g1pubv2);
+                }
+                if let Some(g1pub) = &g1pub_from_tags {
+                    println!("g1pub (Duniter v1 Pubkey Tag): {}", g1pub);
+                }
+                for (key, value) in &profile_content.additional_fields {
+                    println!("{}: {}", key, value);
+                }
+            }
+            OutputFormat::Json => {
+                // Create a clean JSON structure
+                let output = json!({
+                    "vault": {
+                        "address": data.cfg.address.clone(),
+                        "g1v1_pub_key": match &keypair {
+                            KeyPair::Ed25519(_pair) => {
+                                match cesium::compute_g1v1_public_key(&keypair) {
+                                    Ok(pubkey) => Some(pubkey),
+                                    Err(_) => None
+                                }
+                            },
+                            _ => None
+                        },
+                        "crypto_scheme": match &keypair {
+                            KeyPair::Sr25519(_) => Some("Sr25519"),
+                            KeyPair::Ed25519(_) => Some("Ed25519"),
+                        }
+                    },
+                    "pubkey_hex": pubkey,
+                    "pubkey_bech32": npub,
+                    "profile_content": {
+                        "name": profile_content.name,
+                        "display_name": profile_content.display_name,
+                        "picture": profile_content.picture,
+                        "banner": profile_content.banner,
+                        "about": profile_content.about,
+                        "website": profile_content.website,
+                        "nip05": profile_content.nip05,
+                        "bot": profile_content.bot,
+                        "twin_keys": profile_content.twin_keys
+                    },
+                    "tags_custom": {
+                        "g1pubv2": g1pubv2_from_tags,
+                        "g1pub": g1pub_from_tags
+                    },
+                    "full_event": {
+                        "id": event_received.as_ref().map(|e| &e.id),
+                        "pubkey": event_received.as_ref().map(|e| &e.pubkey),
+                        "created_at": event_received.as_ref().map(|e| e.created_at),
+                        "kind": event_received.as_ref().map(|e| e.kind),
+                        "tags": event_received.as_ref().map(|e| &e.tags),
+                        "content": event_received.as_ref().map(|e| &e.content),
+                        "sig": event_received.as_ref().map(|e| &e.sig)
+                    }
+                });
+                println!("{}", serde_json::to_string_pretty(&output).unwrap());
+            }
+        }
+    } else {
+        match data.args.output_format {
+            OutputFormat::Human => {
+                println!("No profile found for npub: {}", npub);
+            }
+            OutputFormat::Json => {
+                let output = json!({
+                    "vault": {
+                        "address": data.cfg.address.clone(),
+                        "g1v1_pub_key": match &keypair {
+                            KeyPair::Ed25519(_pair) => {
+                                match cesium::compute_g1v1_public_key(&keypair) {
+                                    Ok(pubkey) => Some(pubkey),
+                                    Err(_) => None
+                                }
+                            },
+                            _ => None
+                        },
+                        "crypto_scheme": match &keypair {
+                            KeyPair::Sr25519(_) => Some("Sr25519"),
+                            KeyPair::Ed25519(_) => Some("Ed25519"),
+                        }
+                    },
+                    "pubkey_hex": pubkey,
+                    "pubkey_bech32": npub,
+                    "profile_content": {
+                        "name": null,
+                        "display_name": null,
+                        "picture": null,
+                        "banner": null,
+                        "about": null,
+                        "website": null,
+                        "nip05": null,
+                        "bot": null,
+                        "twin_keys": null
+                    },
+                    "tags_custom": {
+                        "g1pubv2": null,
+                        "g1pub": null
+                    },
+                    "full_event": null
+                });
+                println!("{}", serde_json::to_string_pretty(&output).unwrap());
+            }
+        }
+    }
+    
+    Ok(())
+}
+
+/// Set Nostr profile and publish to a relay
+async fn set_profile(
+    data: Data,
+    name: Option<String>,
+    display_name: Option<String>,
+    picture: Option<String>,
+    banner: Option<String>,
+    about: Option<String>,
+    website: Option<String>,
+    nip05: Option<String>,
+    github: Option<String>,
+    twitter: Option<String>,
+    mastodon: Option<String>,
+    telegram: Option<String>,
+    ipfs_gw: Option<String>,
+    ipns_vault: Option<String>,
+    zencard: Option<String>,
+    tw_feed: Option<String>,
+    relay_url: Option<String>,
+) -> Result<(), GcliError> {
+    // Check if no options were provided, and if so, display help
+    if name.is_none() && display_name.is_none() && picture.is_none() && banner.is_none() &&
+        about.is_none() && website.is_none() && nip05.is_none() && github.is_none() &&
+        twitter.is_none() && mastodon.is_none() && telegram.is_none() {
+        if data.args.output_format == OutputFormat::Human {
+            println!("No profile options specified. Available options:");
+            println!("  -n, --name <NAME>                 Set profile name");
+            println!("  -d, --display-name <DISPLAY_NAME> Set profile display name");
+            println!("  -p, --picture <PICTURE>           Set profile picture URL");
+            println!("  -b, --banner <BANNER>             Set profile banner URL");
+            println!("  -a, --about <ABOUT>               Set profile about/description");
+            println!("  -w, --website <WEBSITE>           Set profile website URL");
+            println!("  -i, --nip05 <NIP05>              Set profile NIP-05 identifier");
+            println!("      --github <USERNAME>           Set GitHub username");
+            println!("      --twitter <USERNAME>          Set Twitter username");
+            println!("      --mastodon <IDENTITY>         Set Mastodon identity (instance/@username)");
+            println!("      --telegram <USER_ID>          Set Telegram user ID");
+            println!("      --ipfs-gw <URL>               Set IPFS Gateway URL");
+            println!("      --ipns-vault <KEY>            Set NOSTR Card IPNS vault key");
+            println!("      --zencard <ADDRESS>           Set ZenCard wallet address");
+            println!("      --tw-feed <KEY>               Set TW Feed IPNS key");
+            println!("  -r, --relay <RELAY>               Specify relay URL to publish to");
+            println!("\nExample: gcli profile set --name \"Alice\" --about \"Nostr user\"");
+        } else {
+            println!("{}", serde_json::to_string_pretty(&json!({
+                "error": "No profile options specified",
+                "available_options": {
+                    "name": "Set profile name",
+                    "display_name": "Set profile display name",
+                    "picture": "Set profile picture URL",
+                    "banner": "Set profile banner URL",
+                    "about": "Set profile about/description",
+                    "website": "Set profile website URL",
+                    "nip05": "Set profile NIP-05 identifier",
+                    "github": "Set GitHub username",
+                    "twitter": "Set Twitter username",
+                    "mastodon": "Set Mastodon identity (instance/@username)",
+                    "telegram": "Set Telegram user ID",
+                    "ipfs_gw": "Set IPFS Gateway URL",
+                    "ipns_vault": "Set NOSTR Card IPNS vault key",
+                    "zencard": "Set ZenCard wallet address",
+                    "tw_feed": "Set TW Feed IPNS key",
+                    "relay": "Specify relay URL to publish to"
+                }
+            })).unwrap());
+        }
+        return Ok(());
+    }
+
+    // Get keypair for signing
+    let keypair = if data.args.no_password {
+        // If no_password is set, try to get the keypair without prompting
+        match fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await {
+            Ok(pair) => pair,
+            Err(e) => {
+                if data.args.output_format == OutputFormat::Json {
+                    return Err(anyhow!("Failed to get keypair without password: {}", e).into());
+                } else {
+                    return Err(anyhow!("Failed to get keypair without password. Try removing --no-password option.").into());
+                }
+            }
+        }
+    } else {
+        fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await?
+    };
+    
+    // Get Nostr pubkey in hex format (used for protocol)
+    let pubkey = get_nostr_pubkey(&keypair)?;
+    
+    // Get Nostr pubkey in bech32 format (for display)
+    let npub = get_nostr_npub(&keypair)?;
+    
+    // Get Nostr private key in bech32 format (for display)
+    let nsec = get_nostr_nsec(&keypair)?;
+    
+    // Calculate g1pubv2 (gdev SS58) and g1pub (Duniter v1)
+    let mut g1pub_for_tag: Option<String> = None;
+
+    // Calculate g1pubv2 (SS58 address)
+    let account_id: sp_core::crypto::AccountId32 = match &keypair {
+        KeyPair::Sr25519(pair) => pair.public().into(),
+        KeyPair::Ed25519(pair) => pair.public().into(),
+    };
+    let g1pubv2_for_tag: Option<String> = Some(account_id.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(42)));
+
+    // Calculate g1pub (Duniter v1 pubkey)
+    match cesium::compute_g1v1_public_key(&keypair) {
+        Ok(pubkey_g1) => g1pub_for_tag = Some(pubkey_g1),
+        Err(e) => {
+            if !matches!(keypair, KeyPair::Ed25519(_)) {
+                log::info!("Cannot compute g1pub (Duniter v1 pubkey) as the key is not Ed25519.");
+            } else {
+                log::warn!("Failed to compute g1pub (Duniter v1 pubkey): {}", e);
+            }
+        }
+    }
+    
+    // Create profile content
+    let mut profile_content_obj = NostrProfile::default();
+    if let Some(val) = name { profile_content_obj.name = Some(val); }
+    if let Some(val) = display_name { profile_content_obj.display_name = Some(val); }
+    if let Some(val) = picture { profile_content_obj.picture = Some(val); }
+    if let Some(val) = banner { profile_content_obj.banner = Some(val); }
+    if let Some(val) = about { profile_content_obj.about = Some(val); }
+    if let Some(val) = website { profile_content_obj.website = Some(val); }
+    if let Some(val) = nip05 { profile_content_obj.nip05 = Some(val); }
+    profile_content_obj.bot = Some(false);
+
+    // Generate twin keys
+    let ipns_key = {
+        // First get the public key bytes
+        let pub_bytes = match &keypair {
+            KeyPair::Sr25519(pair) => pair.public().to_raw_vec(),
+            KeyPair::Ed25519(pair) => pair.public().to_raw_vec(),
+        };
+        
+        // Create protobuf format for IPFS public key
+        let mut protobuf = Vec::new();
+        protobuf.extend_from_slice(&[0x08, 0x01]); // Ed25519 key type
+        protobuf.extend_from_slice(&[0x12, 0x20]); // Length prefix
+        protobuf.extend_from_slice(&pub_bytes);
+        
+        // Add multihash prefix
+        let mut peer_id_bytes = Vec::new();
+        peer_id_bytes.extend_from_slice(&[0x00, 0x24]); // Multihash identity prefix
+        peer_id_bytes.extend_from_slice(&protobuf);
+        
+        // Encode as base58
+        format!("12D3Koo{}", bs58::encode(&peer_id_bytes).into_string())
+    };
+
+    // Bitcoin address (using the same derivation as in keygen.py)
+    let bitcoin_address = match &keypair {
+        KeyPair::Sr25519(pair) => {
+            let mut hasher = sha2::Sha256::new();
+            hasher.update(&pair.to_raw_vec());
+            let seed = hasher.finalize();
+            let secp = secp256k1::Secp256k1::new();
+            let secret_key = secp256k1::SecretKey::from_slice(&seed[0..32])
+                .map_err(|e| anyhow!("Failed to create Bitcoin secret key: {}", e))?;
+            let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key);
+            hex::encode(&public_key.serialize())
+        },
+        KeyPair::Ed25519(pair) => {
+            // For Ed25519, take the first 32 bytes (the seed/private key part in raw format)
+            // and hash it with SHA256, like keygen.py does with its 'seed'.
+            let raw_bytes = pair.to_raw_vec();
+            let ed25519_seed = &raw_bytes[..32];
+            let mut hasher = sha2::Sha256::new();
+            hasher.update(ed25519_seed);
+            let seed = hasher.finalize();
+            let secp = secp256k1::Secp256k1::new();
+            let secret_key = secp256k1::SecretKey::from_slice(&seed[0..32])
+                .map_err(|e| anyhow!("Failed to create Bitcoin secret key: {}", e))?;
+            let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key);
+            hex::encode(&public_key.serialize())
+        }
+    };
+
+    // Monero address (using the same derivation as in keygen.py)
+    let monero_address = match &keypair {
+        KeyPair::Sr25519(pair) => {
+            let mut hasher = sha2::Sha256::new();
+            hasher.update(&pair.to_raw_vec());
+            let seed = hasher.finalize();
+            hex::encode(&seed[0..32])
+        },
+        KeyPair::Ed25519(pair) => {
+            let mut hasher = sha2::Sha256::new();
+            hasher.update(&pair.to_raw_vec());
+            let seed = hasher.finalize();
+            hex::encode(&seed[0..32])
+        }
+    };
+    
+    let profile_content_json = serde_json::to_string(&profile_content_obj)
+        .map_err(|e| anyhow!("Failed to serialize profile content: {}", e))?;
+    
+    let mut event = NostrEvent::new(pubkey.clone(), 0, profile_content_json);
+    event.tags = Vec::new();
+
+    // Add twin keys as tags with proper formats
+    event.tags.push(vec!["twin_key".to_string(), "ipns".to_string(), ipns_key]);
+    event.tags.push(vec!["twin_key".to_string(), "bitcoin".to_string(), bitcoin_address]);
+    event.tags.push(vec!["twin_key".to_string(), "monero".to_string(), monero_address]);
+
+    // Add NIP-39 external identity tags
+    if let Some(gh) = &github {
+        event.tags.push(vec!["i".to_string(), format!("github:{}", gh), "".to_string()]);
+    }
+    if let Some(tw) = &twitter {
+        event.tags.push(vec!["i".to_string(), format!("twitter:{}", tw), "".to_string()]);
+    }
+    if let Some(mstd) = &mastodon {
+        event.tags.push(vec!["i".to_string(), format!("mastodon:{}", mstd), "".to_string()]);
+    }
+    if let Some(tg) = &telegram {
+        event.tags.push(vec!["i".to_string(), format!("telegram:{}", tg), "".to_string()]);
+    }
+
+    // Add g1pubv2 and g1pub tags
+    if let Some(g1_ss58) = &g1pubv2_for_tag {
+        event.tags.push(vec!["g1pubv2".to_string(), g1_ss58.clone()]);
+        // Also add as an external identity for compatibility
+        event.tags.push(vec!["i".to_string(), format!("g1pubv2:{}", g1_ss58), "".to_string()]);
+    }
+    if let Some(g1_key) = &g1pub_for_tag {
+        event.tags.push(vec!["g1pub".to_string(), g1_key.clone()]);
+        // Also add as an external identity for compatibility
+        event.tags.push(vec!["i".to_string(), format!("g1pub:{}", g1_key), "".to_string()]);
+    }
+
+    // Add additional custom tags
+    if let Some(gw) = &ipfs_gw {
+        event.tags.push(vec!["i".to_string(), format!("ipfs_gw:{}", gw), "".to_string()]);
+    }
+    if let Some(vault) = &ipns_vault {
+        event.tags.push(vec!["i".to_string(), format!("ipns_vault:{}", vault), "".to_string()]);
+    }
+    if let Some(card) = &zencard {
+        event.tags.push(vec!["i".to_string(), format!("zencard:{}", card), "".to_string()]);
+    }
+    if let Some(feed) = &tw_feed {
+        event.tags.push(vec!["i".to_string(), format!("tw_feed:{}", feed), "".to_string()]);
+    }
+    
+    log::debug!("Created event with pubkey: {}", event.pubkey);
+    log::debug!("Event content: {}", event.content);
+    log::debug!("Event tags: {:?}", event.tags); // Log the tags
+
+    event.calculate_id()?;
+    event.sign(&keypair)?;
+    
+    // Use default relay if none provided
+    let relay = relay_url.unwrap_or_else(|| {
+        data.cfg
+            .relay
+            .clone()
+            .unwrap_or_else(|| "wss://relay.copylaradio.com".to_string())
+    });
+    
+    // Ensure the relay URL starts with ws:// or wss://
+    let relay = if !relay.starts_with("ws://") && !relay.starts_with("wss://") {
+        format!("wss://{}", relay)
+    } else {
+        relay
+    };
+    
+    log::debug!("Using relay URL: {}", relay);
+    
+    // Parse the URL
+    let url = Url::parse(&relay).map_err(|e| anyhow!("Invalid relay URL: {}", e))?;
+    
+    // Connect to the WebSocket
+    if data.args.output_format == OutputFormat::Human {
+        println!("Connecting to relay {}...", relay);
+    }
+    let (mut ws_stream, _) = connect_async(url).await
+        .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?;
+    
+    if data.args.output_format == OutputFormat::Human {
+        println!("Connected to relay, publishing profile...");
+    }
+    
+    // Create event message - CORRECTED FORMAT
+    // According to NIP-01, EVENT should be an array: ["EVENT", <event_object>]
+    let event_msg = json!([
+        "EVENT",
+        event
+    ]);
+    
+    let event_msg_str = event_msg.to_string();
+    log::debug!("Sending message to relay: {}", event_msg_str);
+    
+    // Send the event
+    ws_stream.send(Message::Text(event_msg_str)).await
+        .map_err(|e| anyhow!("Failed to send event: {}", e))?;
+    
+    // Wait for OK message with timeout
+    let mut success = false;
+    let mut response_message = String::new();
+    
+    // Set a timeout for receiving messages
+    let timeout = tokio::time::Duration::from_secs(10); // Increased timeout
+    let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(100));
+    
+    log::debug!("Waiting for relay response with timeout of {} seconds", timeout.as_secs());
+    
+    let start_time = tokio::time::Instant::now();
+    
+    while start_time.elapsed() < timeout {
+        interval.tick().await;
+        
+        // Check for messages with a timeout
+        match tokio::time::timeout(
+            tokio::time::Duration::from_millis(100),
+            ws_stream.next()
+        ).await {
+            Ok(Some(Ok(msg))) => {
+                if let Message::Text(text) = msg {
+                    log::debug!("Received message from relay: {}", text);
+                    response_message = text.clone();
+                    
+                    // Parse the message
+                    if let Ok(json) = serde_json::from_str::<Value>(&text) {
+                        log::debug!("Parsed JSON response: {}", json);
+                        
+                        // Check if it's an OK message
+                        if let Some(msg_type) = json.get(0).and_then(|v| v.as_str()) {
+                            log::debug!("Message type: {}", msg_type);
+                            
+                            if msg_type == "OK" && json.get(1).is_some() && json.get(2).is_some() {
+                                let success_status = json[2].as_bool().unwrap_or(false);
+                                log::debug!("OK status: {}", success_status);
+                                
+                                if success_status {
+                                    success = true;
+                                    log::debug!("Event accepted by relay");
+                                    break;
+                                } else {
+                                    // Error message
+                                    if let Some(error_msg) = json.get(3).and_then(|v| v.as_str()) {
+                                        log::error!("Relay rejected event: {}", error_msg);
+                                        return Err(anyhow!("Relay rejected event: {}", error_msg).into());
+                                    } else {
+                                        log::error!("Relay rejected event without specific error message");
+                                    }
+                                    break;
+                                }
+                            } else if msg_type == "NOTICE" && json.get(1).and_then(|v| v.as_str()).is_some() {
+                                log::debug!("Received NOTICE: {}", json[1].as_str().unwrap());
+                            }
+                        }
+                    } else {
+                        log::warn!("Failed to parse relay response as JSON: {}", text);
+                    }
+                } else {
+                    log::debug!("Received non-text message from relay");
+                }
+            },
+            Ok(Some(Err(e))) => {
+                log::error!("WebSocket error: {}", e);
+                break;
+            },
+            Ok(None) => {
+                log::debug!("Connection closed by relay");
+                break;
+            },
+            Err(_) => {
+                // Timeout, continue
+                continue;
+            }
+        }
+    }
+    
+    if start_time.elapsed() >= timeout {
+        log::warn!("Timeout waiting for relay response");
+    }
+    
+    // Close the WebSocket connection
+    log::debug!("Closing WebSocket connection");
+    ws_stream.close(None).await.ok();
+    
+    match data.args.output_format {
+        OutputFormat::Human => {
+            println!("Profile data published to npub (bech32): {}", npub);
+            println!("Nostr pubkey (hex): {}", pubkey);
+            
+            if let Some(val) = &profile_content_obj.name { println!("Name: {}", val); }
+            if let Some(val) = &profile_content_obj.display_name { println!("Display Name: {}", val); }
+            if let Some(val) = &profile_content_obj.picture { println!("Picture: {}", val); }
+            if let Some(val) = &profile_content_obj.about { println!("About: {}", val); }
+            if let Some(val) = &profile_content_obj.website { println!("Website: {}", val); }
+            if let Some(val) = &profile_content_obj.nip05 { println!("NIP-05: {}", val); }
+
+            // Print calculated tag values for user feedback
+            if let Some(g1_ss58) = &g1pubv2_for_tag {
+                println!("\ng1pubv2 (Tag to be published): {}", g1_ss58);
+            }
+            if let Some(g1_key) = &g1pub_for_tag {
+                println!("g1pub (Tag to be published): {}", g1_key);
+            }
+            
+            println!("\nPublished to relay: {}", relay);
+            if success {
+                println!("Status: Success");
+            } else {
+                println!("Status: No confirmation received from relay");
+                println!("Last response: {}", response_message);
+            }
+        }
+        OutputFormat::Json => {
+            println!("{}", serde_json::to_string_pretty(&json!({
+                "vault": {
+                    "address": data.cfg.address.clone(),
+                    "g1v1_pub_key": match &keypair {
+                        KeyPair::Ed25519(_pair) => {
+                            match cesium::compute_g1v1_public_key(&keypair) {
+                                Ok(pubkey) => Some(pubkey),
+                                Err(_) => None
+                            }
+                        },
+                        _ => None
+                    },
+                    "crypto_scheme": match &keypair {
+                        KeyPair::Sr25519(_) => Some("Sr25519"),
+                        KeyPair::Ed25519(_) => Some("Ed25519"),
+                    }
+                },
+                "pubkey_hex": pubkey,
+                "pubkey_bech32": npub,
+                "seckey_bech32": nsec,
+                "profile_content_sent": profile_content_obj,
+                "calculated_tags": {
+                    "g1pubv2": g1pubv2_for_tag,
+                    "g1pub": g1pub_for_tag,
+                    "twin_keys": {
+                        "ipns": profile_content_obj.twin_keys.as_ref().and_then(|tk| tk.ipns.clone()),
+                        "bitcoin": profile_content_obj.twin_keys.as_ref().and_then(|tk| tk.bitcoin.clone()),
+                        "monero": profile_content_obj.twin_keys.as_ref().and_then(|tk| tk.monero.clone())
+                    }
+                },
+                "event_sent": event,
+                "relay": relay,
+                "success": success,
+                "response": response_message
+            })).unwrap());
+        }
+    }
+    
+    Ok(())
+}
+
+/// Remove Nostr profile and associated events from relays
+async fn remove_profile(data: Data, relay_urls: Option<String>) -> Result<(), GcliError> {
+    // Get keypair for signing
+    let keypair = if data.args.no_password {
+        // If no_password is set, try to get the keypair without prompting
+        match fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await {
+            Ok(pair) => pair,
+            Err(e) => {
+                if data.args.output_format == OutputFormat::Json {
+                    return Err(anyhow!("Failed to get keypair without password: {}", e).into());
+                } else {
+                    return Err(anyhow!("Failed to get keypair without password. Try removing --no-password option.").into());
+                }
+            }
+        }
+    } else {
+        fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await?
+    };
+    
+    // Get Nostr pubkey in hex format
+    let pubkey = get_nostr_pubkey(&keypair)?;
+    
+    // Get Nostr pubkey in bech32 format (for display)
+    let npub = get_nostr_npub(&keypair)?;
+    
+    // Get list of relays
+    let relays = relay_urls
+        .map(|urls| urls.split(',').map(String::from).collect::<Vec<_>>())
+        .unwrap_or_else(|| vec![data.cfg
+            .relay
+            .clone()
+            .unwrap_or_else(|| "wss://relay.copylaradio.com".to_string())]);
+
+    if data.args.output_format == OutputFormat::Human {
+        println!("Removing profile with Nostr pubkey (hex): {}", pubkey);
+        println!("Nostr pubkey (bech32): {}", npub);
+    }
+    
+    let mut results = Vec::new();
+    
+    for relay in &relays {
+        if data.args.output_format == OutputFormat::Human {
+            println!("Sending deletion requests to {}", relay);
+        }
+        
+        // Ensure the relay URL starts with ws:// or wss://
+        let relay = if !relay.starts_with("ws://") && !relay.starts_with("wss://") {
+            format!("wss://{}", relay)
+        } else {
+            relay.clone()
+        };
+        
+        // Parse the URL
+        let url = Url::parse(&relay).map_err(|e| anyhow!("Invalid relay URL: {}", e))?;
+        
+        // Connect to the WebSocket
+        let (mut ws_stream, _) = connect_async(url).await
+            .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?;
+        
+        // Create deletion event for relay list (kind 10002)
+        let mut deletion_event = NostrEvent::new(
+            pubkey.clone(),
+            5, // NIP-09 deletion event kind
+            String::new()
+        );
+        deletion_event.tags = vec![
+            vec!["e".to_string(), pubkey.clone()], // Delete relay list
+            vec!["p".to_string(), pubkey.clone()]  // Delete all events by this pubkey
+        ];
+        
+        // Sign the event
+        deletion_event.sign(&keypair)?;
+        
+        // Create event message
+        let event_msg = json!(["EVENT", deletion_event]);
+        
+        // Send the deletion event
+        ws_stream.send(Message::Text(event_msg.to_string())).await
+            .map_err(|e| anyhow!("Failed to send deletion event: {}", e))?;
+        
+        // Wait for confirmation with timeout
+        let timeout = tokio::time::Duration::from_secs(5);
+        let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(100));
+        let start_time = tokio::time::Instant::now();
+        let mut success = false;
+        let mut response_message = String::new();
+        
+        while start_time.elapsed() < timeout {
+            interval.tick().await;
+            
+            match tokio::time::timeout(
+                tokio::time::Duration::from_millis(100),
+                ws_stream.next()
+            ).await {
+                Ok(Some(Ok(msg))) => {
+                    if let Message::Text(text) = msg {
+                        response_message = text.clone();
+                        if let Ok(json) = serde_json::from_str::<Value>(&text) {
+                            if let Some(msg_type) = json.get(0).and_then(|v| v.as_str()) {
+                                if msg_type == "OK" {
+                                    success = true;
+                                    if data.args.output_format == OutputFormat::Human {
+                                        println!("Deletion request confirmed by relay");
+                                    }
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                },
+                Ok(Some(Err(e))) => {
+                    if data.args.output_format == OutputFormat::Human {
+                        println!("Error receiving confirmation: {}", e);
+                    }
+                    break;
+                },
+                Ok(None) => {
+                    if data.args.output_format == OutputFormat::Human {
+                        println!("Connection closed by relay");
+                    }
+                    break;
+                },
+                Err(_) => continue,
+            }
+        }
+        
+        results.push(json!({
+            "relay": relay,
+            "success": success,
+            "response": response_message
+        }));
+        
+        // Close the WebSocket connection
+        ws_stream.close(None).await.ok();
+    }
+    
+    if data.args.output_format == OutputFormat::Human {
+        println!("Deletion requests sent to all specified relays");
+    } else {
+        println!("{}", serde_json::to_string_pretty(&json!({
+            "pubkey_hex": pubkey,
+            "pubkey_bech32": npub,
+            "results": results
+        })).unwrap());
+    }
+    
+    Ok(())
+}
+
+/// Export keys in different formats
+async fn export_keys(data: Data, format: String) -> Result<(), GcliError> {
+    // Get keypair for deriving other keys
+    let keypair = if data.args.no_password {
+        // If no_password is set, try to get the keypair without prompting
+        match fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await {
+            Ok(pair) => pair,
+            Err(e) => {
+                if data.args.output_format == OutputFormat::Json {
+                    return Err(anyhow!("Failed to get keypair without password: {}", e).into());
+                } else {
+                    return Err(anyhow!("Failed to get keypair without password. Try removing --no-password option.").into());
+                }
+            }
+        }
+    } else {
+        fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await?
+    };
+
+    match format.to_lowercase().as_str() {
+        "g1v1" => {
+            // Calculate g1pub (Duniter v1 pubkey)
+            let g1pub = match cesium::compute_g1v1_public_key(&keypair) {
+                Ok(pubkey) => pubkey,
+                Err(e) => {
+                    if !matches!(keypair, KeyPair::Ed25519(_)) {
+                        return Err(anyhow!("Cannot compute g1pub (Duniter v1 pubkey) as the key is not Ed25519.").into());
+                    } else {
+                        return Err(anyhow!("Failed to compute g1pub (Duniter v1 pubkey): {}", e).into());
+                    }
+                }
+            };
+
+            // Get raw key bytes and convert to pubsec format
+            let (secret_pubsec, _) = match &keypair {
+                KeyPair::Ed25519(pair) => {
+                    // Get the raw bytes
+                    let raw_bytes = pair.to_raw_vec();
+                    
+                    // In Ed25519, the first 32 bytes are the private key
+                    let private_key = &raw_bytes[..32];
+                    
+                    // The next 32 bytes are the public key
+                    let public_key = &raw_bytes[32..];
+
+                    // Create the seed from private key
+                    let mut hasher = sha2::Sha512::new();
+                    hasher.update(private_key);
+                    let hash = hasher.finalize();
+                    let seed = &hash[..32];
+
+                    // Format according to duniterpy's SigningKey format
+                    let mut signing_key = Vec::new();
+                    signing_key.extend_from_slice(&[0x01]); // Version
+                    signing_key.extend_from_slice(seed);    // Seed
+                    signing_key.extend_from_slice(public_key); // Public key
+
+                    (bs58::encode(&signing_key).into_string(), raw_bytes)
+                },
+                _ => return Err(anyhow!("G1v1 format only supports Ed25519 keys").into()),
+            };
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Duniter v1 Keys:");
+                    println!("Public Key: {}", g1pub);
+                    println!("Private Key: {}", secret_pubsec);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "g1v1",
+                        "public_key": g1pub,
+                        "private_key": secret_pubsec,
+                        "type": "PubSec",
+                        "version": "1"
+                    })).unwrap());
+                }
+            }
+        },
+        "g1v2" => {
+            // Calculate g1pubv2 (SS58 address)
+            let account_id: sp_core::crypto::AccountId32 = match &keypair {
+                KeyPair::Sr25519(pair) => pair.public().into(),
+                KeyPair::Ed25519(pair) => pair.public().into(),
+            };
+            let g1pubv2 = account_id.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(42));
+
+            // Get raw key bytes for secret
+            let secret = match &keypair {
+                KeyPair::Sr25519(pair) => hex::encode(pair.to_raw_vec()),
+                KeyPair::Ed25519(pair) => hex::encode(pair.to_raw_vec()),
+            };
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Duniter v2 (gdev) Keys:");
+                    println!("SS58 Address: {}", g1pubv2);
+                    println!("Private Key: {}", secret);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "g1v2",
+                        "ss58_address": g1pubv2,
+                        "private_key": secret
+                    })).unwrap());
+                }
+            }
+        },
+        "ipfs" => {
+            // Get public key bytes
+            let pub_bytes = match &keypair {
+                KeyPair::Sr25519(pair) => pair.public().to_raw(),
+                KeyPair::Ed25519(pair) => pair.public().to_raw(),
+            };
+            
+            // Create protobuf format for IPFS key
+            let mut protobuf = Vec::new();
+            protobuf.extend_from_slice(&[0x08, 0x01]); // Ed25519 key type
+            protobuf.extend_from_slice(&[0x12, 0x20]); // Length prefix
+            protobuf.extend_from_slice(&pub_bytes);
+            
+            // Add multihash prefix for PeerID
+            let mut peer_id_bytes = Vec::new();
+            peer_id_bytes.extend_from_slice(&[0x00, 0x24]); // Multihash identity prefix
+            peer_id_bytes.extend_from_slice(&protobuf);
+            
+            // Create private key protobuf
+            let mut priv_protobuf = Vec::new();
+            priv_protobuf.extend_from_slice(&[0x08, 0x01]); // Ed25519 key type
+            priv_protobuf.extend_from_slice(&[0x12, 0x40]); // Length prefix
+            match &keypair {
+                KeyPair::Sr25519(pair) => priv_protobuf.extend_from_slice(&pair.to_raw_vec()),
+                KeyPair::Ed25519(pair) => priv_protobuf.extend_from_slice(&pair.to_raw_vec()),
+            }
+
+            let peer_id = format!("12D3Koo{}", bs58::encode(&peer_id_bytes).into_string());
+            let private_key = BASE64.encode(&priv_protobuf);
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("IPFS Keys:");
+                    println!("PeerID: {}", peer_id);
+                    println!("Private Key (base64): {}", private_key);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "ipfs",
+                        "peer_id": peer_id,
+                        "private_key": private_key
+                    })).unwrap());
+                }
+            }
+        },
+        "nostr" => {
+            // Get Nostr keys
+            let pubkey = get_nostr_pubkey(&keypair)?;
+            let npub = get_nostr_npub(&keypair)?;
+            let nsec = get_nostr_nsec(&keypair)?;
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Nostr Keys:");
+                    println!("Public Key (hex): {}", pubkey);
+                    println!("Public Key (npub): {}", npub);
+                    println!("Private Key (nsec): {}", nsec);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "nostr",
+                        "public_key_hex": pubkey,
+                        "public_key_bech32": npub,
+                        "private_key_bech32": nsec
+                    })).unwrap());
+                }
+            }
+        },
+        "bitcoin" => {
+            // Generate Bitcoin keys using seed from keypair
+            let seed = match &keypair {
+                KeyPair::Sr25519(pair) => {
+                    let mut hasher = sha2::Sha256::new();
+                    hasher.update(&pair.to_raw_vec());
+                    hasher.finalize()
+                },
+                KeyPair::Ed25519(pair) => {
+                    // For Ed25519, take the first 32 bytes (the seed/private key part in raw format)
+                    // and hash it with SHA256, like keygen.py does with its 'seed'.
+                    let raw_bytes = pair.to_raw_vec();
+                    let ed25519_seed = &raw_bytes[..32];
+                    let mut hasher = sha2::Sha256::new();
+                    hasher.update(ed25519_seed);
+                    hasher.finalize()
+                }
+            };
+
+            let secp = secp256k1::Secp256k1::new();
+            let secret_key = secp256k1::SecretKey::from_slice(&seed[0..32])
+                .map_err(|e| anyhow!("Failed to create Bitcoin secret key: {}", e))?;
+            let _public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key);
+
+            // Derive the corresponding public key
+            let public_key_secp = secp256k1::PublicKey::from_secret_key(&secp, &secret_key);
+
+            // Use the 'bitcoin' and 'bitcoin-addr' crates for proper formatting
+            use bitcoin::{PrivateKey, Network, Address};
+            // use bitcoin_addr::AddressType; // Removed
+
+            // Create a PrivateKey object (for mainnet or testnet, let's use Testnet for example)
+            // You might want to make the network configurable
+            let private_key_btc = PrivateKey::new(secret_key, Network::Bitcoin);
+
+            // Get the WIF format private key
+            let private_key_wif = private_key_btc.to_wif();
+
+            // Get the public key hex (compressed format is standard)
+            let public_key_hex = hex::encode(public_key_secp.serialize());
+
+            // Derive the P2PKH address (most common)
+            // Note: This assumes P2PKH. Other address types (P2SH, Bech32) are possible.
+            // keygen.py seems to derive a simple address from compressed pubkey.
+            // Convert secp256k1::PublicKey to bitcoin::PublicKey (using compressed format)
+            let public_key_btc = bitcoin::PublicKey::from_slice(&public_key_secp.serialize()).map_err(|e| anyhow!("Failed to create bitcoin::PublicKey: {}", e))?;
+
+            // Derive the P2PKH address using the bitcoin::PublicKey
+            let address = Address::p2pkh(&public_key_btc, Network::Bitcoin).to_string();
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Bitcoin Keys:");
+                    println!("Private Key (WIF): {}", private_key_wif);
+                    println!("Public Key (hex): {}", public_key_hex);
+                    println!("Address: {}", address);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "bitcoin",
+                        "private_key_wif": private_key_wif,
+                        "public_key_hex": public_key_hex,
+                        "address_p2pkh": address
+                    })).unwrap());
+                }
+            }
+        },
+        "monero" => {
+            // Generate Monero keys using seed from keypair
+            let seed = match &keypair {
+                KeyPair::Sr25519(pair) => {
+                    let mut hasher = sha2::Sha256::new();
+                    hasher.update(&pair.to_raw_vec());
+                    hasher.finalize()
+                },
+                KeyPair::Ed25519(pair) => {
+                    let mut hasher = sha2::Sha256::new();
+                    hasher.update(&pair.to_raw_vec());
+                    hasher.finalize()
+                }
+            };
+
+            let private_spend_key = hex::encode(&seed[0..32]);
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Monero Keys:");
+                    println!("Private Spend Key: {}", private_spend_key);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "monero",
+                        "private_spend_key": private_spend_key
+                    })).unwrap());
+                }
+            }
+        },
+        _ => {
+            let available_formats = vec!["ipfs", "nostr", "bitcoin", "monero", "g1v1", "g1v2"];
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Available export formats:");
+                    println!("  ipfs    - IPFS PeerID and private key");
+                    println!("  nostr   - Nostr npub/nsec keys");
+                    println!("  bitcoin - Bitcoin WIF and address");
+                    println!("  monero  - Monero keys");
+                    println!("  g1v1    - Duniter v1 public/private keys");
+                    println!("  g1v2    - Duniter v2 (gdev) SS58/private keys");
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "available_formats": available_formats
+                    })).unwrap());
+                }
+            }
+            return Ok(());
+        }
+    }
+    
+    Ok(())
+} 
\ No newline at end of file
diff --git a/src/commands/vault.rs b/src/commands/vault.rs
index b46606e1d2171dbe2d16b5ee98d0658b57ae38e1..57b352e0b1d311ad97bf4860770bb84375fe8e2c 100644
--- a/src/commands/vault.rs
+++ b/src/commands/vault.rs
@@ -15,6 +15,8 @@ use std::cell::RefCell;
 use std::io::{Read, Write};
 use std::path::PathBuf;
 use std::rc::Rc;
+use serde::Serialize;
+use std::fmt;
 
 /// vault subcommands
 #[derive(Clone, Debug, clap::Parser)]
@@ -221,25 +223,73 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 				let all_account_tree_node_hierarchies =
 					vault_account::fetch_all_base_account_tree_node_hierarchies(db).await?;
 
-				let table = display::compute_vault_accounts_table_with_g1v1(
-					&all_account_tree_node_hierarchies,
-					show_g1v1,
-				)?;
-
-				println!("available SS58 Addresses:");
-				println!("{table}");
+				match data.args.output_format {
+					OutputFormat::Json => {
+						let accounts: Vec<_> = all_account_tree_node_hierarchies
+							.iter()
+							.map(|node| {
+								let node = node.borrow();
+								VaultAccountJson {
+									address: node.account.address.to_string(),
+									crypto_scheme: node.account.crypto_scheme.map(|s| s.to_string()),
+									path: node.account.path.clone(),
+									name: node.account.name.clone(),
+									g1v1_public_key: if show_g1v1 && node.account.crypto_scheme == Some(CryptoScheme::Ed25519.into()) {
+										Some(cesium::compute_g1v1_public_key_from_ed25519_account_id(&node.account.address.0))
+									} else {
+										None
+									}
+								}
+							})
+							.collect();
+						println!("{}", serde_json::to_string_pretty(&accounts)?);
+					}
+					OutputFormat::Human => {
+						let table = display::compute_vault_accounts_table_with_g1v1(
+							&all_account_tree_node_hierarchies,
+							show_g1v1,
+						)?;
+
+						println!("available SS58 Addresses:");
+						println!("{table}");
+					}
+				}
 			}
 			ListChoice::Base { show_g1v1 } => {
 				let base_account_tree_nodes =
 					vault_account::fetch_only_base_account_tree_nodes(db).await?;
 
-				let table = display::compute_vault_accounts_table_with_g1v1(
-					&base_account_tree_nodes,
-					show_g1v1,
-				)?;
-
-				println!("available <Base> SS58 Addresses:");
-				println!("{table}");
+				match data.args.output_format {
+					OutputFormat::Json => {
+						let accounts: Vec<_> = base_account_tree_nodes
+							.iter()
+							.map(|node| {
+								let node = node.borrow();
+								VaultAccountJson {
+									address: node.account.address.to_string(),
+									crypto_scheme: node.account.crypto_scheme.map(|s| s.to_string()),
+									path: node.account.path.clone(),
+									name: node.account.name.clone(),
+									g1v1_public_key: if show_g1v1 && node.account.crypto_scheme == Some(CryptoScheme::Ed25519.into()) {
+										Some(cesium::compute_g1v1_public_key_from_ed25519_account_id(&node.account.address.0))
+									} else {
+										None
+									}
+								}
+							})
+							.collect();
+						println!("{}", serde_json::to_string_pretty(&accounts)?);
+					}
+					OutputFormat::Human => {
+						let table = display::compute_vault_accounts_table_with_g1v1(
+							&base_account_tree_nodes,
+							show_g1v1,
+						)?;
+
+						println!("available <Base> SS58 Addresses:");
+						println!("{table}");
+					}
+				}
 			}
 			ListChoice::For {
 				address_or_vault_name,
@@ -251,16 +301,40 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 				let base_account_tree_node =
 					vault_account::get_base_account_tree_node(&account_tree_node);
 
-				let table = display::compute_vault_accounts_table_with_g1v1(
-					&[base_account_tree_node],
-					show_g1v1,
-				)?;
-
-				println!(
-					"available SS58 Addresses linked to {}:",
-					account_tree_node.borrow().account
-				);
-				println!("{table}");
+				match data.args.output_format {
+					OutputFormat::Json => {
+						let accounts: Vec<_> = vec![base_account_tree_node]
+							.iter()
+							.map(|node| {
+								let node = node.borrow();
+								VaultAccountJson {
+									address: node.account.address.to_string(),
+									crypto_scheme: node.account.crypto_scheme.map(|s| s.to_string()),
+									path: node.account.path.clone(),
+									name: node.account.name.clone(),
+									g1v1_public_key: if show_g1v1 && node.account.crypto_scheme == Some(CryptoScheme::Ed25519.into()) {
+										Some(cesium::compute_g1v1_public_key_from_ed25519_account_id(&node.account.address.0))
+									} else {
+										None
+									}
+								}
+							})
+							.collect();
+						println!("{}", serde_json::to_string_pretty(&accounts)?);
+					}
+					OutputFormat::Human => {
+						let table = display::compute_vault_accounts_table_with_g1v1(
+							&[base_account_tree_node],
+							show_g1v1,
+						)?;
+
+						println!(
+							"available SS58 Addresses linked to {}:",
+							account_tree_node.borrow().account
+						);
+						println!("{table}");
+					}
+				}
 			}
 		},
 		Subcommand::ListFiles => {
@@ -376,6 +450,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 				provided_password.as_ref(),
 				Some(crypto_scheme),
 				name,
+				&data,
 			)
 			.await?;
 
@@ -665,6 +740,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 					Some(&vault_data_from_file.password),
 					Some(CryptoScheme::Sr25519),
 					None,
+					&data,
 				)
 				.await;
 
@@ -947,6 +1023,7 @@ pub async fn create_base_account_for_vault_data_to_import<C>(
 	password_opt: Option<&String>,
 	crypto_scheme: Option<CryptoScheme>,
 	name_opt: Option<String>,
+	data: &Data,
 ) -> Result<vault_account::Model, GcliError>
 where
 	C: ConnectionTrait,
@@ -959,50 +1036,57 @@ where
 		vault_account::find_by_id(db_tx, &DbAccountId::from(address_to_import.clone())).await?
 	{
 		// Existing account
-		if existing_vault_account.is_base_account() {
-			println!("You are trying to add {address_to_import} as a <Base> account while it already exists as a <Base> account.");
-			println!();
-			println!("Do you want to:");
-			println!("1. keep the existing <Base> account and cancel import");
-			println!("2. overwrite existing <Base> account with the new encrypted key (children will be re-parented)");
+		let result = if data.args.no_password {
+			println!("Account already exists. Overwriting due to --no-password flag.");
+			"2".to_string()
 		} else {
-			// Existing derivation account
-			let account_tree_node_hierarchy =
-				vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
-					db_tx,
+			if existing_vault_account.is_base_account() {
+				println!("You are trying to add {} as a <Base> account while it already exists as a <Base> account.", address_to_import);
+				println!();
+				println!("Do you want to:");
+				println!("1. keep the existing <Base> account and cancel import");
+				println!("2. overwrite existing <Base> account with the new encrypted key (children will be re-parented)");
+			} else {
+				// Existing derivation account
+				let account_tree_node_hierarchy =
+					vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
+						db_tx,
+						&address_to_import,
+					)
+					.await?;
+				let account_tree_node_for_address = vault_account::get_account_tree_node_for_address(
+					&account_tree_node_hierarchy,
 					&address_to_import,
-				)
-				.await?;
-			let account_tree_node_for_address = vault_account::get_account_tree_node_for_address(
-				&account_tree_node_hierarchy,
-				&address_to_import,
-			);
-
-			let base_parent_hierarchy_account_tree_node =
-				vault_account::get_base_parent_hierarchy_account_tree_node(
-					&account_tree_node_for_address,
 				);
 
-			let parent_hierarchy_table =
-				display::compute_vault_accounts_table(&[base_parent_hierarchy_account_tree_node])?;
+				let base_parent_hierarchy_account_tree_node =
+					vault_account::get_base_parent_hierarchy_account_tree_node(
+						&account_tree_node_for_address,
+					);
 
-			println!("You are trying to add {address_to_import} as a <Base> account");
-			println!(
-				"but it is already present as `{}` derivation of {} account.",
-				existing_vault_account.path.clone().unwrap(),
-				existing_vault_account.parent.clone().unwrap()
-			);
-			println!();
-			println!("Its parent hierarchy is this:");
-			println!("{parent_hierarchy_table}");
-			println!();
-			println!("Do you want to:");
-			println!("1. keep the existing derivation and cancel import");
-			println!("2. delete the derivation account and replace it with the new <Base> account (children will be re-parented)");
-		}
+				let parent_hierarchy_table =
+					display::compute_vault_accounts_table(&[base_parent_hierarchy_account_tree_node])?;
 
-		let result = inputs::select_action("Your choice?", vec!["1", "2"])?;
-		match result {
+				println!("You are trying to add {} as a <Base> account", address_to_import);
+				println!(
+					"but it is already present as `{}` derivation of '{}' account.",
+					existing_vault_account.path.clone().unwrap(),
+					existing_vault_account.parent.clone().unwrap()
+				);
+				println!();
+				println!("Its parent hierarchy is this:");
+				println!("{parent_hierarchy_table}");
+				println!();
+				println!("Do you want to:");
+				println!("1. keep the existing derivation and cancel import");
+				println!("2. delete the derivation account and replace it with the new <Base> account (children will be re-parented)");
+			}
+
+			let result = inputs::select_action("Your choice?", vec!["1", "2"])?;
+			result.to_string()
+		};
+
+		match result.as_str() {
 			"2" => {
 				let password = match password_opt {
 					Some(password) => password.clone(),
@@ -1259,7 +1343,12 @@ pub async fn try_fetch_key_pair(
 
 		println!("(Vault: {})", account_tree_node.borrow().account);
 
-		let password = inputs::prompt_password()?;
+		let password = if data.args.no_password {
+			"".to_string()
+		} else {
+			inputs::prompt_password()?
+		};
+
 		let secret_suri =
 			vault_account::compute_suri_account_tree_node(&account_tree_node, password)?;
 
@@ -1336,6 +1425,21 @@ pub fn try_fetch_vault_data_from_file(
 	}
 }
 
+#[derive(Serialize)]
+struct VaultAccountJson {
+	address: String,
+	crypto_scheme: Option<String>,
+	path: Option<String>,
+	name: Option<String>,
+	g1v1_public_key: Option<String>,
+}
+
+impl From<serde_json::Error> for GcliError {
+	fn from(err: serde_json::Error) -> Self {
+		GcliError::Input(err.to_string())
+	}
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
diff --git a/src/conf.rs b/src/conf.rs
index 509e0c5bb9dfe36fb572a65eb6f0fff823b28546..8809c3983485ade55bf15e71c564fd2d70a55644 100644
--- a/src/conf.rs
+++ b/src/conf.rs
@@ -15,6 +15,8 @@ pub struct Config {
 	/// user address
 	/// to perform actions, user must provide secret
 	pub address: Option<AccountId>,
+	/// nostr relay endpoint
+	pub relay: Option<String>,
 }
 
 impl std::default::Default for Config {
@@ -23,6 +25,7 @@ impl std::default::Default for Config {
 			duniter_endpoint: String::from(data::LOCAL_DUNITER_ENDPOINT),
 			indexer_endpoint: String::from(data::LOCAL_INDEXER_ENDPOINT),
 			address: None,
+			relay: Some(String::from("wss://relay.copylaradio.com")),
 		}
 	}
 }
@@ -34,10 +37,16 @@ impl std::fmt::Display for Config {
 		} else {
 			"(no address)".to_string()
 		};
+		let relay = if let Some(relay) = &self.relay {
+			relay.clone()
+		} else {
+			"(no relay)".to_string()
+		};
 		writeln!(f, "Ğcli config")?;
 		writeln!(f, "duniter endpoint {}", self.duniter_endpoint)?;
 		writeln!(f, "indexer endpoint {}", self.indexer_endpoint)?;
-		write!(f, "address {address}")
+		writeln!(f, "address {address}")?;
+		write!(f, "nostr relay {relay}")
 	}
 }
 
diff --git a/src/entities/vault_account.rs b/src/entities/vault_account.rs
index 60d663d9cd13c408168ffe93eccb709662ed5b30..8151185756e457c9255908349c13f4bc5b0ae828 100644
--- a/src/entities/vault_account.rs
+++ b/src/entities/vault_account.rs
@@ -22,6 +22,7 @@ use std::future::Future;
 use std::pin::Pin;
 use std::rc::Rc;
 use std::str::FromStr;
+use std::fmt;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
 #[sea_orm(table_name = "vault_account")]
@@ -224,8 +225,19 @@ impl From<DbAccountId> for AccountId {
 	rename_all = "PascalCase"
 )]
 pub enum DbCryptoScheme {
-	Ed25519,
+	#[sea_orm(string_value = "sr25519")]
 	Sr25519,
+	#[sea_orm(string_value = "Ed25519")]
+	Ed25519,
+}
+
+impl fmt::Display for DbCryptoScheme {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		match self {
+			DbCryptoScheme::Sr25519 => write!(f, "sr25519"),
+			DbCryptoScheme::Ed25519 => write!(f, "Ed25519"),
+		}
+	}
 }
 
 impl From<crate::keys::CryptoScheme> for DbCryptoScheme {
diff --git a/src/indexer.rs b/src/indexer.rs
index aa76c1f418f5a95afa642ae9a7d309c2850dba18..93373a745125d9a7bd8a51e71cc945247965bf55 100644
--- a/src/indexer.rs
+++ b/src/indexer.rs
@@ -191,59 +191,97 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 			let i_latest_h = convert_hash_bytea(i_latest_block.hash);
 			let i_latest_n = i_latest_block.height;
 
-			fn color(x: bool) -> Color {
-				match x {
-					true => Color::Green,
-					false => Color::Red,
+			match data.args.output_format {
+				OutputFormat::Json => {
+					let output = serde_json::json!({
+						"duniter": {
+							"url": d_url,
+							"genesis_hash": d_gen_hash,
+							"finalized_block": {
+								"number": d_finalized_n,
+								"hash": d_finalized_h.to_string()
+							},
+							"latest_block": {
+								"number": d_latest_n,
+								"hash": d_latest_h.to_string()
+							}
+						},
+						"indexer": {
+							"url": i_url,
+							"genesis_hash": i_gen_hash,
+							"finalized_block": {
+								"number": i_finalized_n,
+								"hash": i_finalized_h.map(|h| h.to_string())
+							},
+							"latest_block": {
+								"number": i_latest_n,
+								"hash": i_latest_h.to_string()
+							}
+						},
+						"status": {
+							"genesis_hash_match": d_gen_hash == i_gen_hash,
+							"finalized_block_match": i_finalized_h.map_or(false, |h| h == d_finalized_h),
+							"latest_block_match": i_latest_n == d_latest_n as i64 && i_latest_h == d_latest_h
+						}
+					});
+					println!("{}", serde_json::to_string_pretty(&output).unwrap());
+				},
+				OutputFormat::Human => {
+					fn color(x: bool) -> Color {
+						match x {
+							true => Color::Green,
+							false => Color::Red,
+						}
+					}
+
+					let mut table = Table::new();
+					table
+						.load_preset(presets::UTF8_FULL)
+						.apply_modifier(modifiers::UTF8_ROUND_CORNERS)
+						.set_content_arrangement(ContentArrangement::Dynamic)
+						.set_width(120)
+						.set_header(vec!["Variable", "Duniter", "Indexer"])
+						.add_row(vec!["URL", d_url, i_url])
+						.add_row(vec![
+							Cell::new("genesis hash"),
+							Cell::new(d_gen_hash),
+							Cell::new(i_gen_hash).fg(color(d_gen_hash == i_gen_hash)),
+						])
+						.add_row(vec![
+							Cell::new("finalized block number"),
+							Cell::new(d_finalized_n),
+							match i_finalized_n {
+								None => Cell::new("not indexed").fg(Color::Yellow),
+								Some(n) => {
+									// if it exists, it must be the same
+									assert_eq!(n, d_finalized_n as i64);
+									Cell::new("")
+								}
+							},
+						])
+						.add_row(vec![
+							Cell::new("finalized block hash"),
+							Cell::new(d_finalized_h),
+							match i_finalized_h {
+								// number already tells it is not indexed
+								None => Cell::new(""),
+								Some(h) => Cell::new(h).fg(color(h == d_finalized_h)),
+							},
+						])
+						.add_row(vec![
+							Cell::new("latest block number"),
+							Cell::new(d_latest_n),
+							Cell::new(i_latest_n).fg(color(i_latest_n == d_latest_n as i64)),
+						])
+						.add_row(vec![
+							Cell::new("latest block hash"),
+							Cell::new(d_latest_h),
+							Cell::new(i_latest_h).fg(color(i_latest_h == d_latest_h)),
+						]);
+
+					println!("{table}");
 				}
 			}
-
-			let mut table = Table::new();
-			table
-				.load_preset(presets::UTF8_FULL)
-				.apply_modifier(modifiers::UTF8_ROUND_CORNERS)
-				.set_content_arrangement(ContentArrangement::Dynamic)
-				.set_width(120)
-				.set_header(vec!["Variable", "Duniter", "Indexer"])
-				.add_row(vec!["URL", d_url, i_url])
-				.add_row(vec![
-					Cell::new("genesis hash"),
-					Cell::new(d_gen_hash),
-					Cell::new(i_gen_hash).fg(color(d_gen_hash == i_gen_hash)),
-				])
-				.add_row(vec![
-					Cell::new("finalized block number"),
-					Cell::new(d_finalized_n),
-					match i_finalized_n {
-						None => Cell::new("not indexed").fg(Color::Yellow),
-						Some(n) => {
-							// if it exists, it must be the same
-							assert_eq!(n, d_finalized_n as i64);
-							Cell::new("")
-						}
-					},
-				])
-				.add_row(vec![
-					Cell::new("finalized block hash"),
-					Cell::new(d_finalized_h),
-					match i_finalized_h {
-						// number already tells it is not indexed
-						None => Cell::new(""),
-						Some(h) => Cell::new(h).fg(color(h == d_finalized_h)),
-					},
-				])
-				.add_row(vec![
-					Cell::new("latest block number"),
-					Cell::new(d_latest_n),
-					Cell::new(i_latest_n).fg(color(i_latest_n == d_latest_n as i64)),
-				])
-				.add_row(vec![
-					Cell::new("latest block hash"),
-					Cell::new(d_latest_h),
-					Cell::new(i_latest_h).fg(color(i_latest_h == d_latest_h)),
-				]);
-
-			println!("{table}");
 		}
 	};
 
diff --git a/src/main.rs b/src/main.rs
index 8e417d6002ad0fefa59da870cc48aa307b4f7513..2f8405885f944d70b95a800850ca28e89d9401d2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -77,6 +77,9 @@ pub struct Args {
 	/// Output format (human, json, ...)
 	#[clap(short = 'o', long, default_value = OutputFormat::Human)]
 	output_format: OutputFormat,
+	/// Do not prompt for password
+	#[clap(long)]
+	no_password: bool,
 }
 
 // TODO derive the fromstr implementation
@@ -155,6 +158,9 @@ pub enum Subcommand {
 	/// Key management (import, generate, list...)
 	#[clap(subcommand)]
 	Vault(commands::vault::Subcommand),
+	/// Nostr profile management (get, set...)
+	#[clap(subcommand)]
+	Profile(commands::profile::Subcommand),
 	/// Cesium
 	#[clap(subcommand, hide = true)]
 	Cesium(commands::cesium::Subcommand),
@@ -233,6 +239,7 @@ async fn main() -> Result<(), GcliError> {
 		Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await,
 		Subcommand::Config(subcommand) => conf::handle_command(data, subcommand).await,
 		Subcommand::Vault(subcommand) => commands::vault::handle_command(data, subcommand).await,
+		Subcommand::Profile(subcommand) => commands::profile::handle_command(data, subcommand).await,
 		Subcommand::Cesium(subcommand) => commands::cesium::handle_command(data, subcommand).await,
 		Subcommand::Publish => commands::publish::handle_command().await,
 	};
diff --git a/src/utils.rs b/src/utils.rs
index 68ec0118e858a2cbae818b132772aa81cc2bacea..bcff9842f5447165388e78f3f7661e6432febdcb 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -116,6 +116,8 @@ pub enum GcliError {
 	/// error coming from io
 	#[allow(dead_code)]
 	IoError(std::io::Error),
+	/// error coming from config
+	Config(String),
 }
 impl std::fmt::Display for GcliError {
 	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {