diff --git a/Cargo.lock b/Cargo.lock
index b503c6b..237c75f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2614,6 +2614,27 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "nunchi-perpetuals"
+version = "2026.5.0"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "commonware-codec",
+ "commonware-cryptography",
+ "commonware-formatting",
+ "commonware-runtime",
+ "futures",
+ "jsonrpsee",
+ "nunchi-coins",
+ "nunchi-common",
+ "nunchi-crypto",
+ "nunchi-rpc",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
[[package]]
name = "nunchi-rpc"
version = "2026.5.0"
diff --git a/Cargo.toml b/Cargo.toml
index 41f8fda..b5804a1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ members = [
"rpc",
"examples/template",
"examples/coins-chain",
+ "perpetuals",
]
resolver = "2"
@@ -28,6 +29,7 @@ nunchi-crypto = { version = "2026.5.0", path = "crypto" }
nunchi-dkg = { version = "2026.5.0", path = "dkg" }
nunchi-mempool = { version = "2026.5.0", path = "mempool" }
nunchi-rpc = { version = "2026.5.0", path = "rpc" }
+nunchi-perpetuals = { version = "2026.5.0", path = "perpetuals" }
nunchi-template = { version = "2026.5.0", path = "examples/template" }
nunchi-coins-chain = { version = "2026.5.0", path = "examples/coins-chain" }
commonware-actor = "2026.5.0"
diff --git a/README.md b/README.md
index 54f460d..604678e 100644
--- a/README.md
+++ b/README.md
@@ -44,10 +44,10 @@ This repository will contain modules for building public and private blockchains
### Financial Primitives
+* [`perpetuals`](perpetuals/) - defines perpetual swap markets, leveraged positions, liquidation checks, and RPC queries
* `margin` - user has BTC + nunchi and doesn't want to sell, and deposits BTC+nunchi and gets a stablecoin. Could be backed by other coins, not just btc and nunchi.
* `securities` - Non-synthetic perps contracts (delivery of tokenized stock)
* `vaults` - a module for running vaults composed of many types of capital, traded by an authorised offchain party
* `clob` - used on the global chain, provides liquidity between local chain tokens
* `derivatives` - ingests a price feed and creates derivatives products
* `stablecoin` - a wrapper of coins special for the needs of stablecoins
-
diff --git a/perpetuals/Cargo.toml b/perpetuals/Cargo.toml
new file mode 100644
index 0000000..fd56597
--- /dev/null
+++ b/perpetuals/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "nunchi-perpetuals"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+
+[features]
+default = ["rpc"]
+rpc = ["dep:nunchi-rpc", "dep:jsonrpsee", "dep:futures"]
+
+[dependencies]
+async-trait = { workspace = true }
+bytes = { workspace = true }
+thiserror = { workspace = true }
+nunchi-coins = { workspace = true }
+nunchi-common = { workspace = true }
+nunchi-crypto = { workspace = true }
+nunchi-rpc = { workspace = true, optional = true }
+commonware-codec = { workspace = true }
+commonware-cryptography = { workspace = true }
+commonware-formatting = { workspace = true }
+jsonrpsee = { workspace = true, optional = true }
+serde = { workspace = true }
+futures = { workspace = true, optional = true }
+
+[dev-dependencies]
+commonware-runtime = { workspace = true }
+serde_json = { workspace = true }
diff --git a/perpetuals/src/db.rs b/perpetuals/src/db.rs
new file mode 100644
index 0000000..c7c1b39
--- /dev/null
+++ b/perpetuals/src/db.rs
@@ -0,0 +1,149 @@
+//! Persistence layer for the perpetuals module.
+
+use crate::ledger::LedgerError;
+use crate::{Market, MarketId, Position, PositionId, PERPETUALS_NAMESPACE};
+use async_trait::async_trait;
+use commonware_codec::{Encode, Read, ReadExt};
+use commonware_cryptography::sha256::Digest;
+use nunchi_common::state_db::{Namespace, StateStore};
+use nunchi_common::Address;
+
+const NS: Namespace = Namespace::new(PERPETUALS_NAMESPACE);
+
+#[repr(u8)]
+#[derive(Clone, Copy)]
+enum Table {
+ Account = 0,
+ MarketNonce = 1,
+ PositionNonce = 2,
+ Market = 3,
+ Position = 4,
+}
+
+impl From
for u8 {
+ fn from(table: Table) -> Self {
+ table as Self
+ }
+}
+
+fn encoded(value: &T) -> Vec {
+ value.encode().as_ref().to_vec()
+}
+
+fn decoded>(bytes: &[u8]) -> Result {
+ let mut buf = bytes;
+ T::read(&mut buf).map_err(|err| LedgerError::Storage(err.to_string()))
+}
+
+#[async_trait]
+pub trait PerpetualDB {
+ async fn nonce(&self, id: &Address) -> Result;
+
+ fn set_nonce(&mut self, id: &Address, nonce: u64);
+
+ async fn market_nonce(&self) -> Result;
+
+ fn set_market_nonce(&mut self, nonce: u64);
+
+ async fn position_nonce(&self) -> Result;
+
+ fn set_position_nonce(&mut self, nonce: u64);
+
+ async fn market(&self, market: &MarketId) -> Result