diff --git a/query-security-txt/Cargo.toml b/query-security-txt/Cargo.toml
index 43b8da1..9f4189b 100644
--- a/query-security-txt/Cargo.toml
+++ b/query-security-txt/Cargo.toml
@@ -11,6 +11,6 @@ license = "MIT OR Apache-2.0"
 bincode = "1.3.3"
 clap = { version = "3.1.6", features = ["derive"] }
 color-eyre = "0.6.1"
-solana-security-txt = { version = "0.1.4", path = "../security-txt" }
+solana-security-txt = { version = "0.1.4", path = "../security-txt", features = ["parser"] }
 solana-client = "1.10.0"
 solana-sdk = "1.10.0"
diff --git a/security-txt/Cargo.toml b/security-txt/Cargo.toml
index f42c2fb..17de4e5 100644
--- a/security-txt/Cargo.toml
+++ b/security-txt/Cargo.toml
@@ -7,7 +7,10 @@ readme = "../README.md"
 repository = "https://github.com/neodyme-labs/solana-security-txt"
 license = "MIT OR Apache-2.0"
 
+[features]
+# when no features are enabled, this crate has no dependencies. The parser requires two, but won't be used on-chain.
+parser = ["thiserror", "twoway"]
 
 [dependencies]
-thiserror = "1.0.30"
-twoway = "0.2.2"
+thiserror = {version = "=1.0.30", optional = true}
+twoway = {version = "=0.2.2", optional = true}
diff --git a/security-txt/src/lib.rs b/security-txt/src/lib.rs
index aa68303..960ad98 100644
--- a/security-txt/src/lib.rs
+++ b/security-txt/src/lib.rs
@@ -60,11 +60,11 @@
 //!
 //! The string the macro builds begins with the start marker `=======BEGIN SECURITY.TXT V1=======\0`, and ends with the end marker `=======END SECURITY.TXT V1=======\0`. In between is a list of an even amount of strings, delimited by nullbytes. Every two strings form a key-value-pair.
 
-use core::fmt;
-use std::{collections::HashMap, fmt::Display};
 
-use thiserror::Error;
-use twoway::find_bytes;
+#[cfg(feature = "parser")]
+mod parser;
+#[cfg(feature = "parser")]
+pub use crate::parser::*;
 
 pub const SECURITY_TXT_BEGIN: &str = "=======BEGIN SECURITY.TXT V1=======\0";
 pub const SECURITY_TXT_END: &str = "=======END SECURITY.TXT V1=======\0";
@@ -83,226 +83,3 @@ macro_rules! security_txt {
     };
 }
 
-#[derive(Error, Debug)]
-pub enum SecurityTxtError {
-    #[error("security.txt doesn't start with the right string")]
-    InvalidSecurityTxtBegin,
-    #[error("Couldn't find end string")]
-    EndNotFound,
-    #[error("Couldn't find start string")]
-    StartNotFound,
-    #[error("Invalid field: `{0:?}`")]
-    InvalidField(Vec<u8>),
-    #[error("Unknown field: `{0}`")]
-    UnknownField(String),
-    #[error("Invalid value `{0:?}` for field `{1}`")]
-    InvalidValue(Vec<u8>, String),
-    #[error("Invalid contact `{0}`")]
-    InvalidContact(String),
-    #[error("Missing field: `{0}`")]
-    MissingField(String),
-    #[error("Duplicate field: `{0}`")]
-    DuplicateField(String),
-    #[error("Uneven amount of parts")]
-    Uneven,
-}
-
-pub enum Contact {
-    Email(String),
-    Discord(String),
-    Telegram(String),
-    Twitter(String),
-    Link(String),
-    Other(String),
-}
-
-impl Display for Contact {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Contact::Discord(s) => write!(f, "Discord: {}", s),
-            Contact::Email(s) => write!(f, "Email: {}", s),
-            Contact::Telegram(s) => write!(f, "Telegram: {}", s),
-            Contact::Twitter(s) => write!(f, "Twitter: {}", s),
-            Contact::Link(s) => write!(f, "Link: {}", s),
-            Contact::Other(s) => write!(f, "Other: {}", s),
-        }
-    }
-}
-
-impl Contact {
-    pub fn from_str(s: &str) -> Result<Self, SecurityTxtError> {
-        let (typ, value) = s
-            .split_once(":")
-            .ok_or_else(|| SecurityTxtError::InvalidContact(s.to_string()))?;
-        let (contact_type, contact_info) = (typ.trim(), value.trim());
-        match contact_type.to_ascii_lowercase().as_str() {
-            "email" => Ok(Contact::Email(contact_info.to_string())),
-            "discord" => Ok(Contact::Discord(contact_info.to_string())),
-            "telegram" => Ok(Contact::Telegram(contact_info.to_string())),
-            "twitter" => Ok(Contact::Twitter(contact_info.to_string())),
-            "link" => Ok(Contact::Link(contact_info.to_string())),
-            "other" => Ok(Contact::Other(contact_info.to_string())),
-            _ => Err(SecurityTxtError::InvalidContact(s.to_string())),
-        }
-    }
-}
-
-pub struct SecurityTxt {
-    pub name: String,
-    pub project_url: String,
-    pub source_code: Option<String>,
-    pub expiry: Option<String>,
-    pub preferred_languages: Vec<String>,
-    pub contacts: Vec<Contact>,
-    pub auditors: Vec<String>,
-    pub encryption: Option<String>,
-    pub acknowledgements: Option<String>,
-    pub policy: String,
-}
-
-impl Display for SecurityTxt {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        writeln!(f, "Name: {}", self.name)?;
-        writeln!(f, "Project URL: {}", self.project_url)?;
-
-        if let Some(expiry) = &self.expiry {
-            writeln!(f, "Expires at: {}", expiry)?;
-        }
-
-        if let Some(source_code) = &self.source_code {
-            writeln!(f, "Source code: {}", source_code)?;
-        }
-
-        if !self.contacts.is_empty() {
-            writeln!(f, "\nContacts:")?;
-            for contact in &self.contacts {
-                writeln!(f, "  {}", contact)?;
-            }
-        }
-
-        if !self.preferred_languages.is_empty() {
-            writeln!(f, "\nPreferred Languages:")?;
-            for languages in &self.preferred_languages {
-                writeln!(f, "  {}", languages)?;
-            }
-        }
-
-        if let Some(encryption) = &self.encryption {
-            writeln!(f, "\nEncryption:")?;
-            writeln!(f, "{}", encryption)?;
-        }
-
-        if let Some(acknowledegments) = &self.acknowledgements {
-            writeln!(f, "\nAcknowledgements:")?;
-            writeln!(f, "{}", acknowledegments)?;
-        }
-
-        if !self.auditors.is_empty() {
-            writeln!(f, "\nAuditors:")?;
-            for auditor in &self.auditors {
-                writeln!(f, "  {}", auditor)?;
-            }
-        }
-
-        writeln!(f, "\nPolicy:")?;
-        writeln!(f, "{}", self.policy)?;
-
-        Ok(())
-    }
-}
-
-/// Parses a security.txt. Might not consume all of `data`.
-pub fn parse(mut data: &[u8]) -> Result<SecurityTxt, SecurityTxtError> {
-    if !data.starts_with(SECURITY_TXT_BEGIN.as_bytes()) {
-        return Err(SecurityTxtError::InvalidSecurityTxtBegin);
-    }
-
-    let end = match find_bytes(data, SECURITY_TXT_END.as_bytes()) {
-        Some(i) => i,
-        None => return Err(SecurityTxtError::EndNotFound),
-    };
-
-    data = &data[SECURITY_TXT_BEGIN.len()..end];
-
-    let mut attributes = HashMap::<String, String>::default();
-    let mut field: Option<String> = None;
-    for part in data.split(|&b| b == 0) {
-        if let Some(ref f) = field {
-            let value = std::str::from_utf8(part)
-                .map_err(|_| SecurityTxtError::InvalidValue(part.to_vec(), f.clone()))?;
-            attributes.insert(f.clone(), value.to_string());
-            field = None;
-        } else {
-            field = Some({
-                let field = std::str::from_utf8(part)
-                    .map_err(|_| SecurityTxtError::InvalidField(part.to_vec()))?
-                    .to_string();
-                if attributes.contains_key(&field) {
-                    return Err(SecurityTxtError::DuplicateField(field));
-                }
-                field
-            });
-        }
-    }
-
-    let name = attributes
-        .remove("name")
-        .ok_or_else(|| SecurityTxtError::MissingField("name".to_string()))?;
-    let project_url = attributes
-        .remove("project_url")
-        .ok_or_else(|| SecurityTxtError::MissingField("project_url".to_string()))?;
-    let source_code = attributes.remove("source_code");
-    let expiry = attributes.remove("expiry");
-    let preferred_languages: Vec<_> = attributes
-        .remove("preferred_languages")
-        .ok_or_else(|| SecurityTxtError::MissingField("preferred_languages".to_string()))?
-        .split(',')
-        .map(|s| s.trim().to_string())
-        .collect();
-    let contacts: Result<Vec<_>, SecurityTxtError> = attributes
-        .remove("contacts")
-        .ok_or_else(|| SecurityTxtError::MissingField("contacts".to_string()))?
-        .split(",")
-        .map(|s| Contact::from_str(s.trim()))
-        .collect();
-    let contacts = contacts?;
-    let auditors: Vec<_> = attributes
-        .remove("auditors")
-        .unwrap_or_default()
-        .split(",")
-        .map(|s| s.trim().to_string())
-        .collect();
-    let encryption = attributes.remove("encryption");
-    let acknowledgements = attributes.remove("acknowledgements");
-    let policy = attributes
-        .remove("policy")
-        .ok_or_else(|| SecurityTxtError::MissingField("policy".to_string()))?;
-
-    if !attributes.is_empty() {
-        return Err(SecurityTxtError::UnknownField(
-            attributes.keys().next().unwrap().clone(),
-        ));
-    }
-
-    Ok(SecurityTxt {
-        name,
-        project_url,
-        source_code,
-        expiry,
-        preferred_languages,
-        contacts,
-        auditors,
-        encryption,
-        acknowledgements,
-        policy,
-    })
-}
-
-/// Finds and parses the security.txt in the haystack
-pub fn find_and_parse(data: &[u8]) -> Result<SecurityTxt, SecurityTxtError> {
-    let start = match find_bytes(data, SECURITY_TXT_BEGIN.as_bytes()) {
-        Some(i) => i,
-        None => return Err(SecurityTxtError::StartNotFound),
-    };
-    parse(&data[start..])
-}
diff --git a/security-txt/src/parser.rs b/security-txt/src/parser.rs
new file mode 100644
index 0000000..7d03332
--- /dev/null
+++ b/security-txt/src/parser.rs
@@ -0,0 +1,229 @@
+
+use thiserror::Error;
+use twoway::find_bytes;
+use crate::{SECURITY_TXT_BEGIN, SECURITY_TXT_END};
+use std::collections::HashMap;
+use core::fmt::{Display, self};
+
+pub enum Contact {
+    Email(String),
+    Discord(String),
+    Telegram(String),
+    Twitter(String),
+    Link(String),
+    Other(String),
+}
+
+impl Display for Contact {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Contact::Discord(s) => write!(f, "Discord: {}", s),
+            Contact::Email(s) => write!(f, "Email: {}", s),
+            Contact::Telegram(s) => write!(f, "Telegram: {}", s),
+            Contact::Twitter(s) => write!(f, "Twitter: {}", s),
+            Contact::Link(s) => write!(f, "Link: {}", s),
+            Contact::Other(s) => write!(f, "Other: {}", s),
+        }
+    }
+}
+
+pub struct SecurityTxt {
+    pub name: String,
+    pub project_url: String,
+    pub source_code: Option<String>,
+    pub expiry: Option<String>,
+    pub preferred_languages: Vec<String>,
+    pub contacts: Vec<Contact>,
+    pub auditors: Vec<String>,
+    pub encryption: Option<String>,
+    pub acknowledgements: Option<String>,
+    pub policy: String,
+}
+
+impl Display for SecurityTxt {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        writeln!(f, "Name: {}", self.name)?;
+        writeln!(f, "Project URL: {}", self.project_url)?;
+
+        if let Some(expiry) = &self.expiry {
+            writeln!(f, "Expires at: {}", expiry)?;
+        }
+
+        if let Some(source_code) = &self.source_code {
+            writeln!(f, "Source code: {}", source_code)?;
+        }
+
+        if !self.contacts.is_empty() {
+            writeln!(f, "\nContacts:")?;
+            for contact in &self.contacts {
+                writeln!(f, "  {}", contact)?;
+            }
+        }
+
+        if !self.preferred_languages.is_empty() {
+            writeln!(f, "\nPreferred Languages:")?;
+            for languages in &self.preferred_languages {
+                writeln!(f, "  {}", languages)?;
+            }
+        }
+
+        if let Some(encryption) = &self.encryption {
+            writeln!(f, "\nEncryption:")?;
+            writeln!(f, "{}", encryption)?;
+        }
+
+        if let Some(acknowledegments) = &self.acknowledgements {
+            writeln!(f, "\nAcknowledgements:")?;
+            writeln!(f, "{}", acknowledegments)?;
+        }
+
+        if !self.auditors.is_empty() {
+            writeln!(f, "\nAuditors:")?;
+            for auditor in &self.auditors {
+                writeln!(f, "  {}", auditor)?;
+            }
+        }
+
+        writeln!(f, "\nPolicy:")?;
+        writeln!(f, "{}", self.policy)?;
+
+        Ok(())
+    }
+}
+
+
+impl Contact {
+    pub fn from_str(s: &str) -> Result<Self, SecurityTxtError> {
+        let (typ, value) = s
+            .split_once(":")
+            .ok_or_else(|| SecurityTxtError::InvalidContact(s.to_string()))?;
+        let (contact_type, contact_info) = (typ.trim(), value.trim());        match contact_type.to_ascii_lowercase().as_str() {
+            "email" => Ok(Contact::Email(contact_info.to_string())),
+            "discord" => Ok(Contact::Discord(contact_info.to_string())),
+            "telegram" => Ok(Contact::Telegram(contact_info.to_string())),
+            "twitter" => Ok(Contact::Twitter(contact_info.to_string())),
+            "link" => Ok(Contact::Link(contact_info.to_string())),
+            "other" => Ok(Contact::Other(contact_info.to_string())),
+            _ => Err(SecurityTxtError::InvalidContact(s.to_string())),
+        }
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum SecurityTxtError {
+    #[error("security.txt doesn't start with the right string")]
+    InvalidSecurityTxtBegin,
+    #[error("Couldn't find end string")]
+    EndNotFound,
+    #[error("Couldn't find start string")]
+    StartNotFound,
+    #[error("Invalid field: `{0:?}`")]
+    InvalidField(Vec<u8>),
+    #[error("Unknown field: `{0}`")]
+    UnknownField(String),
+    #[error("Invalid value `{0:?}` for field `{1}`")]
+    InvalidValue(Vec<u8>, String),
+    #[error("Invalid contact `{0}`")]
+    InvalidContact(String),
+    #[error("Missing field: `{0}`")]
+    MissingField(String),
+    #[error("Duplicate field: `{0}`")]
+    DuplicateField(String),
+    #[error("Uneven amount of parts")]
+    Uneven,
+}
+/// Parses a security.txt. Might not consume all of `data`.
+pub fn parse(mut data: &[u8]) -> Result<SecurityTxt, SecurityTxtError> {
+    if !data.starts_with(SECURITY_TXT_BEGIN.as_bytes()) {
+        return Err(SecurityTxtError::InvalidSecurityTxtBegin);
+    }
+
+    let end = match find_bytes(data, SECURITY_TXT_END.as_bytes()) {
+        Some(i) => i,
+        None => return Err(SecurityTxtError::EndNotFound),
+    };
+
+    data = &data[SECURITY_TXT_BEGIN.len()..end];
+
+    let mut attributes = HashMap::<String, String>::default();
+    let mut field: Option<String> = None;
+    for part in data.split(|&b| b == 0) {
+        if let Some(ref f) = field {
+            let value = std::str::from_utf8(part)
+                .map_err(|_| SecurityTxtError::InvalidValue(part.to_vec(), f.clone()))?;
+            attributes.insert(f.clone(), value.to_string());
+            field = None;
+        } else {
+            field = Some({
+                let field = std::str::from_utf8(part)
+                    .map_err(|_| SecurityTxtError::InvalidField(part.to_vec()))?
+                    .to_string();
+                if attributes.contains_key(&field) {
+                    return Err(SecurityTxtError::DuplicateField(field));
+                }
+                field
+            });
+        }
+    }
+
+    let name = attributes
+        .remove("name")
+        .ok_or_else(|| SecurityTxtError::MissingField("name".to_string()))?;
+    let project_url = attributes
+        .remove("project_url")
+        .ok_or_else(|| SecurityTxtError::MissingField("project_url".to_string()))?;
+    let source_code = attributes.remove("source_code");
+    let expiry = attributes.remove("expiry");
+    let preferred_languages: Vec<_> = attributes
+        .remove("preferred_languages")
+        .ok_or_else(|| SecurityTxtError::MissingField("preferred_languages".to_string()))?
+        .split(',')
+        .map(|s| s.trim().to_string())
+        .collect();
+    let contacts: Result<Vec<_>, SecurityTxtError> = attributes
+        .remove("contacts")
+        .ok_or_else(|| SecurityTxtError::MissingField("contacts".to_string()))?
+        .split(",")
+        .map(|s| Contact::from_str(s.trim()))
+        .collect();
+    let contacts = contacts?;
+    let auditors: Vec<_> = attributes
+        .remove("auditors")
+        .unwrap_or_default()
+        .split(",")
+        .map(|s| s.trim().to_string())
+        .collect();
+    let encryption = attributes.remove("encryption");
+    let acknowledgements = attributes.remove("acknowledgements");
+    let policy = attributes
+        .remove("policy")
+        .ok_or_else(|| SecurityTxtError::MissingField("policy".to_string()))?;
+
+    if !attributes.is_empty() {
+        return Err(SecurityTxtError::UnknownField(
+            attributes.keys().next().unwrap().clone(),
+        ));
+    }
+
+    Ok(SecurityTxt {
+        name,
+        project_url,
+        source_code,
+        expiry,
+        preferred_languages,
+        contacts,
+        auditors,
+        encryption,
+        acknowledgements,
+        policy,
+    })
+}
+
+/// Finds and parses the security.txt in the haystack
+pub fn find_and_parse(data: &[u8]) -> Result<SecurityTxt, SecurityTxtError> {
+    let start = match find_bytes(data, SECURITY_TXT_BEGIN.as_bytes()) {
+        Some(i) => i,
+        None => return Err(SecurityTxtError::StartNotFound),
+    };
+    parse(&data[start..])
+}
\ No newline at end of file