diff --git a/README.md b/README.md
index 879c4d7772667fcee9e8dc55ccc9b04241dc7611..e04dce2e703ae122ac6c11f6cdb79f7fce1d920c 100644
--- a/README.md
+++ b/README.md
@@ -38,3 +38,11 @@ When your node is ready to forge blocks, rotate keys and go online:
 gcli --secret "my secret phrase" update-keys
 gcli --secret "my secret phrase" go-online
 ```
+
+### Keys
+
+Secret and/or public keys can always be passed using `--secret` and `--address`. If needed, stdin will be prompted for secret key. An error will occur if secret and address are both given but do not match.
+
+Secret key format can be changed using `--secret-format` with the following values:
+* `substrate`: a Substrate secret address (optionally followed by a derivation path), or BIP39 mnemonic
+* `seed`: a 32-bytes seed in hexadecimal (Duniter v1 compatible)
diff --git a/src/keys.rs b/src/keys.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d9bebddd50bfb6c14e548cd0155b71e1d5737f82
--- /dev/null
+++ b/src/keys.rs
@@ -0,0 +1,125 @@
+use anyhow::{anyhow, Result};
+use clap::builder::OsStr;
+use sp_core::crypto::{AccountId32, Ss58Codec};
+use sp_core::{crypto::Pair as _, sr25519::Pair};
+use std::str::FromStr;
+
+#[allow(dead_code)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum NeededKeys {
+    None,
+    Public,
+    Secret,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum SecretFormat {
+    /// Raw 32B seed
+    Seed,
+    /// Substrate secret key or BIP39 mnemonic (optionally followed by derivation path)
+    Substrate,
+}
+
+impl FromStr for SecretFormat {
+    type Err = std::io::Error;
+
+    fn from_str(s: &str) -> std::io::Result<Self> {
+        match s {
+            "seed" => Ok(SecretFormat::Seed),
+            "substrate" => Ok(SecretFormat::Substrate),
+            _ => Err(std::io::Error::from(std::io::ErrorKind::InvalidInput)),
+        }
+    }
+}
+
+impl From<SecretFormat> for &'static str {
+    fn from(val: SecretFormat) -> &'static str {
+        match val {
+            SecretFormat::Seed => "seed",
+            SecretFormat::Substrate => "substrate",
+        }
+    }
+}
+
+impl From<SecretFormat> for OsStr {
+    fn from(val: SecretFormat) -> OsStr {
+        OsStr::from(Into::<&str>::into(val))
+    }
+}
+
+pub fn pair_from_str(secret_format: SecretFormat, secret: &str) -> Result<Pair> {
+    match secret_format {
+        SecretFormat::Seed => {
+            let mut seed = [0; 32];
+            hex::decode_to_slice(secret, &mut seed).map_err(|_| anyhow!("Invalid secret"))?;
+            let pair = Pair::from_seed(&seed);
+            Ok(pair)
+        }
+        SecretFormat::Substrate => {
+            Pair::from_string(secret, None).map_err(|_| anyhow!("Invalid secret"))
+        }
+    }
+}
+
+pub fn prompt_secret(secret_format: SecretFormat) -> Pair {
+    let mut line = String::new();
+    loop {
+        println!("Secret key ({secret_format:?}): ");
+        std::io::stdin().read_line(&mut line).unwrap();
+        match pair_from_str(secret_format, line.trim()) {
+            Ok(pair) => return pair,
+            Err(_) => println!("Invalid secret"),
+        }
+        line.clear();
+    }
+}
+
+pub fn get_keys(
+    secret_format: SecretFormat,
+    address: &Option<String>,
+    secret: &Option<String>,
+    needed_keys: NeededKeys,
+) -> Result<(Option<AccountId32>, Option<Pair>)> {
+    // Get from args
+    let mut account_id = match (address, secret) {
+        (Some(address), Some(secret)) => {
+            let pair = pair_from_str(secret_format, secret)?;
+            let address = AccountId32::from_string(address)
+                .map_err(|_| anyhow!("Invalid address {}", address))?;
+            assert_eq!(
+                address,
+                pair.public().into(),
+                "Secret and address do not match."
+            );
+            return Ok((Some(pair.public().into()), Some(pair)));
+        }
+        (None, Some(secret)) => {
+            let pair = pair_from_str(secret_format, secret)?;
+            return Ok((Some(pair.public().into()), Some(pair)));
+        }
+        (Some(address), None) => Some(
+            AccountId32::from_str(address).map_err(|_| anyhow!("Invalid address {}", address))?,
+        ),
+        (None, None) => None,
+    };
+
+    // Prompt
+    if needed_keys == NeededKeys::Secret
+        || (account_id.is_none() && needed_keys == NeededKeys::Public)
+    {
+        loop {
+            let pair = prompt_secret(secret_format);
+
+            if let Some(account_id) = &account_id {
+                if account_id != &pair.public().into() {
+                    println!("Secret and address do not match.");
+                }
+            } else {
+                account_id = Some(pair.public().into());
+                return Ok((account_id, Some(pair)));
+            }
+        }
+    }
+
+    Ok((account_id, None))
+}
diff --git a/src/main.rs b/src/main.rs
index 0672751fd57ae755645d99ff4bbd3b0563842188..07b1adc95a5575f1a4d98e65a5b6aa21f60f21df 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,16 +1,14 @@
 mod cache;
 mod commands;
 mod indexer;
+mod keys;
 
-use anyhow::{anyhow, Result};
+use keys::*;
+
+use anyhow::Result;
 use clap::Parser;
 use codec::Encode;
-use sp_core::{
-    crypto::{Pair as _, Ss58Codec},
-    sr25519::Pair,
-    H256,
-};
-use std::str::FromStr;
+use sp_core::H256;
 
 #[subxt::subxt(runtime_metadata_path = "res/metadata.scale")]
 pub mod gdev {}
@@ -71,6 +69,9 @@ pub struct Args {
     /// (eventually followed by derivation path)
     #[clap(short, long)]
     secret: Option<String>,
+    /// Secret key format (seed, substrate)
+    #[clap(short = 'S', long, default_value = SecretFormat::Substrate)]
+    secret_format: SecretFormat,
     /// Address
     #[clap(short, long)]
     address: Option<String>,
@@ -181,54 +182,31 @@ async fn main() -> Result<()> {
 
     let args = Args::parse();
 
-    let (account_id, pair) = match (&args.address, &args.secret) {
-        (Some(address), Some(secret)) => {
-            let pair = Pair::from_string(secret, None)
-                .map_err(|_| anyhow!("Invalid secret {}", secret))?;
-            let address = sp_core::crypto::AccountId32::from_string(address)
-                .map_err(|_| anyhow!("Invalid address {}", address))?;
-            assert_eq!(
-                address,
-                pair.public().into(),
-                "Secret and address do not match."
-            );
-            (Some(pair.public().into()), Some(pair))
-        }
-        (None, Some(secret)) => {
-            let pair = Pair::from_string(secret, None)
-                .map_err(|_| anyhow!("Invalid secret {}", secret))?;
-            (Some(pair.public().into()), Some(pair))
-        }
-        (Some(address), None) => (
-            Some(
-                sp_core::crypto::AccountId32::from_str(address)
-                    .map_err(|_| anyhow!("Invalid address {}", address))?,
-            ),
-            None,
-        ),
-        (None, None) => (None, None),
-    };
-
-    if let Some(account_id) = &account_id {
+    /*if let Some(account_id) = &account_id {
         println!("Account address: {account_id}");
-    }
-
-    let client = Client::from_url(&args.url).await.unwrap();
+    }*/
 
-    if let Some(account_id) = &account_id {
+    /*if let Some(account_id) = &account_id {
         let account = client
             .storage()
             .fetch(&gdev::storage().system().account(account_id), None)
             .await?
             .expect("Cannot fetch account");
         logs::info!("Account free balance: {}", account.data.free);
-    }
+    }*/
 
     match args.subcommand {
         Subcommand::CreateOneshot { balance, dest } => {
             commands::oneshot::create_oneshot_account(
-                pair.expect("This subcommand needs a secret."),
-                client,
+                get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
+                Client::from_url(&args.url).await.unwrap(),
                 balance,
                 dest,
             )
@@ -236,8 +214,15 @@ async fn main() -> Result<()> {
         }
         Subcommand::ConsumeOneshot { dest, dest_oneshot } => {
             commands::oneshot::consume_oneshot_account(
-                pair.expect("This subcommand needs a secret."),
-                client,
+                get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
+                Client::from_url(&args.url).await.unwrap(),
                 dest,
                 dest_oneshot,
             )
@@ -251,8 +236,15 @@ async fn main() -> Result<()> {
             remaining_to_oneshot,
         } => {
             commands::oneshot::consume_oneshot_account_with_remaining(
-                pair.expect("This subcommand needs a secret."),
-                client,
+                get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
+                Client::from_url(&args.url).await.unwrap(),
                 balance,
                 dest,
                 dest_oneshot,
@@ -262,7 +254,13 @@ async fn main() -> Result<()> {
             .await?
         }
         Subcommand::Expire { blocks, sessions } => {
-            commands::expire::monitor_expirations(client, blocks, sessions, &args).await?
+            commands::expire::monitor_expirations(
+                Client::from_url(&args.url).await.unwrap(),
+                blocks,
+                sessions,
+                &args,
+            )
+            .await?
         }
         Subcommand::Identity {
             ref account_id,
@@ -270,7 +268,7 @@ async fn main() -> Result<()> {
             ref username,
         } => {
             commands::identity::get_identity(
-                client,
+                Client::from_url(&args.url).await.unwrap(),
                 account_id.clone(),
                 identity_id,
                 username.clone(),
@@ -280,30 +278,70 @@ async fn main() -> Result<()> {
         }
         Subcommand::GenRevocDoc => {
             commands::revocation::gen_revoc_doc(
-                &client,
-                &pair.expect("This subcommand needs a secret."),
+                &Client::from_url(&args.url).await.unwrap(),
+                &get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
             )
             .await?
         }
         Subcommand::GoOffline => {
-            commands::smith::go_offline(pair.expect("This subcommand needs a secret."), client)
-                .await?
+            commands::smith::go_offline(
+                get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
+                Client::from_url(&args.url).await.unwrap(),
+            )
+            .await?
         }
         Subcommand::GoOnline => {
-            commands::smith::go_online(pair.expect("This subcommand needs a secret."), client)
-                .await?
+            commands::smith::go_online(
+                get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
+                Client::from_url(&args.url).await.unwrap(),
+            )
+            .await?
         }
         Subcommand::OneshotBalance { account } => {
-            commands::oneshot::oneshot_account_balance(client, account).await?
+            commands::oneshot::oneshot_account_balance(
+                Client::from_url(&args.url).await.unwrap(),
+                account,
+            )
+            .await?
+        }
+        Subcommand::Online => {
+            commands::smith::online(Client::from_url(&args.url).await.unwrap(), &args).await?
         }
-        Subcommand::Online => commands::smith::online(client, &args).await?,
         Subcommand::Repart {
             target,
             actual_repart,
         } => {
             commands::net_test::repart(
-                pair.expect("This subcommand needs a secret."),
-                client,
+                get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
+                Client::from_url(&args.url).await.unwrap(),
                 target,
                 actual_repart,
             )
@@ -311,25 +349,46 @@ async fn main() -> Result<()> {
         }
         Subcommand::SpamRoll { actual_repart } => {
             commands::net_test::spam_roll(
-                pair.expect("This subcommand needs a secret."),
-                client,
+                get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
+                Client::from_url(&args.url).await.unwrap(),
                 actual_repart,
             )
             .await?
         }
         Subcommand::SudoSetKey { new_key } => {
             commands::sudo::set_key(
-                pair.expect("This subcommand needs a secret."),
-                client,
+                get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
+                Client::from_url(&args.url).await.unwrap(),
                 new_key,
             )
             .await?
         }
         Subcommand::TechMembers => {
-            commands::collective::technical_committee_members(client, &args).await?
+            commands::collective::technical_committee_members(
+                Client::from_url(&args.url).await.unwrap(),
+                &args,
+            )
+            .await?
         }
         Subcommand::TechProposals => {
-            commands::collective::technical_committee_proposals(client).await?
+            commands::collective::technical_committee_proposals(
+                Client::from_url(&args.url).await.unwrap(),
+            )
+            .await?
         }
         Subcommand::TechVote { hash, index, vote } => {
             let vote = match vote {
@@ -338,8 +397,15 @@ async fn main() -> Result<()> {
                 _ => panic!("Vote must be written 0 if you disagree, or 1 if you agree."),
             };
             commands::collective::technical_committee_vote(
-                pair.expect("This subcommand needs a secret."),
-                client,
+                get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
+                Client::from_url(&args.url).await.unwrap(),
                 hash, //H256::from_str(&hash).expect("Invalid hash formatting"),
                 index,
                 vote,
@@ -352,8 +418,15 @@ async fn main() -> Result<()> {
             keep_alive,
         } => {
             commands::transfer::transfer(
-                pair.expect("This subcommand needs a secret."),
-                client,
+                get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
+                Client::from_url(&args.url).await.unwrap(),
                 amount,
                 dest,
                 keep_alive,
@@ -362,16 +435,30 @@ async fn main() -> Result<()> {
         }
         Subcommand::TransferMultiple { amount, dests } => {
             commands::transfer::transfer_multiple(
-                pair.expect("This subcommand needs a secret."),
-                client,
+                get_keys(
+                    args.secret_format,
+                    &args.address,
+                    &args.secret,
+                    NeededKeys::Secret,
+                )?
+                .1
+                .unwrap(),
+                Client::from_url(&args.url).await.unwrap(),
                 amount,
                 dests,
             )
             .await?
         }
         Subcommand::UpdateKeys => commands::smith::update_session_keys(
-            pair.expect("This subcommand needs a secret."),
-            client,
+            get_keys(
+                args.secret_format,
+                &args.address,
+                &args.secret,
+                NeededKeys::Secret,
+            )?
+            .1
+            .unwrap(),
+            Client::from_url(&args.url).await.unwrap(),
         )
         .await
         .unwrap(),