diff --git a/Cargo.lock b/Cargo.lock
index 128a860..c99a8ed 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2723,6 +2723,29 @@ dependencies = [
"tracing-subscriber 0.3.23",
]
+[[package]]
+name = "nunchi-cbc"
+version = "2026.6.0"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "commonware-codec",
+ "commonware-cryptography",
+ "commonware-formatting",
+ "commonware-macros",
+ "commonware-runtime",
+ "futures",
+ "jsonrpsee",
+ "nunchi-clob",
+ "nunchi-common",
+ "nunchi-crypto",
+ "nunchi-house",
+ "nunchi-rpc",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
[[package]]
name = "nunchi-chain"
version = "2026.6.0"
diff --git a/Cargo.toml b/Cargo.toml
index ca436c2..1349e9b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,7 @@
members = [
"authority",
"bridge",
+ "cbc",
"common",
"chain",
"clob",
@@ -39,6 +40,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [
nunchi-coins = { version = "2026.5.0", path = "coins" }
nunchi-authority = { version = "2026.5.0", path = "authority" }
nunchi-bridge = { version = "2026.5.0", path = "bridge" }
+nunchi-cbc = { version = "2026.5.0", path = "cbc" }
nunchi-chain = { version = "2026.5.0", path = "chain" }
nunchi-clob = { version = "2026.5.0", path = "clob" }
nunchi-common = { version = "2026.5.0", path = "common" }
diff --git a/cbc/Cargo.toml b/cbc/Cargo.toml
new file mode 100644
index 0000000..bef16d2
--- /dev/null
+++ b/cbc/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "nunchi-cbc"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+description = "Cooperative batch clearing module for Nunchi hybrid AMM liquidity: uniform-price batches over house vault intents."
+
+[lints]
+workspace = true
+
+[features]
+default = ["rpc"]
+rpc = ["dep:futures", "dep:jsonrpsee", "dep:nunchi-rpc"]
+
+[dependencies]
+async-trait = { workspace = true }
+bytes = { workspace = true }
+commonware-codec = { workspace = true }
+commonware-cryptography = { workspace = true }
+commonware-formatting = { workspace = true }
+commonware-macros = { workspace = true }
+futures = { workspace = true, optional = true }
+jsonrpsee = { workspace = true, optional = true }
+nunchi-clob = { workspace = true }
+nunchi-common = { workspace = true }
+nunchi-crypto = { workspace = true }
+nunchi-house = { workspace = true }
+nunchi-rpc = { workspace = true, optional = true }
+serde = { workspace = true }
+thiserror = { workspace = true }
+
+[dev-dependencies]
+commonware-runtime = { workspace = true }
+serde_json = { workspace = true }
diff --git a/cbc/README.md b/cbc/README.md
new file mode 100644
index 0000000..3756d1a
--- /dev/null
+++ b/cbc/README.md
@@ -0,0 +1,28 @@
+# nunchi-cbc
+
+`nunchi-cbc` is the cooperative batch clearing module of the Nunchi hybrid AMM stack. It clears signed liquidity-management intents from house vaults and allowlisted market makers at one deterministic uniform price per market per batch, and settles fills through `nunchi-house`'s checked clearing API.
+
+Uniform pricing is the trust model: every executable fill in a batch receives the same price, so a hostile public submitter cannot selectively trade against another participant's price-time edge, and participants reveal nothing beyond their signed intents. The house-liquidity path therefore needs no TEE; confidential clearing remains a future hardening option.
+
+## v1 operations
+
+- `RegisterMarket` / `SetClearingMode` (admin-gated)
+- `SubmitIntent` / `CancelIntent` (authorized vault submitters)
+- `CloseAndClearBatch` (keeper-gated)
+
+## Clearing flow
+
+1. Intents rest in a per-market queue in submission order. Buy intents reserve their worst-case quote cost (`limit_price * base_quantity`) in the house module at submission, so a vault can never distort a batch price with intents it cannot settle.
+2. On `CloseAndClearBatch`, expired intents and intents whose vault is halted or has no reducing capacity are retired first.
+3. The clearing price maximizes executable volume over the surviving intents; ties break toward the keeper-posted oracle price, then toward the lower price. Prices outside the oracle band record an `OutsideBand` result and leave the queue untouched.
+4. Matched volume is allocated in submission order. Each matched chunk validates both sides against current house state (mode gates, reducing capacity, caps, leverage ceiling) and settles immediately; an intent whose side fails validation is skipped for the batch and remains pending.
+5. The batch result (outcome, prices, aggregate fills, retired intents) is recorded and queryable; results are the module's event surface for ABM reconciliation.
+
+Settlement is conservation-preserving by construction: every chunk credits and debits equal base and quote across its two sides at the uniform price.
+
+## Trust seams and v1 limits
+
+- The keeper posts the oracle price with each clearing call. This is a documented interim seam until chain-level oracle wiring supplies registry-approved prices directly, and it is why `CloseAndClearBatch` is keeper-gated rather than permissionless.
+- Sell intents post no collateral; they are bounded by per-vault and per-batch notional caps and by house-side exposure checks at settlement. Economic short margining arrives with perps wiring.
+- Reduce-only quantities are capped against pre-batch inventory for price discovery and re-checked exactly during allocation.
+- Risk-weighted KKT clearing (explicit `gamma`/`c` parameters) is the planned v1.1 upgrade of the price rule; the intent codec deliberately leaves room for a versioned extension.
diff --git a/cbc/src/db.rs b/cbc/src/db.rs
new file mode 100644
index 0000000..79e58a0
--- /dev/null
+++ b/cbc/src/db.rs
@@ -0,0 +1,254 @@
+//! Persistence layer for the CBC module.
+
+use crate::{
+ BatchIntent, BatchParams, BatchResult, CbcError, IntentId, MarketClearingState, CBC_NAMESPACE,
+ MAX_CLEARING_MARKETS, MAX_PENDING_INTENTS,
+};
+use async_trait::async_trait;
+use commonware_codec::{Encode, RangeCfg, Read, ReadExt};
+use commonware_cryptography::sha256::Digest;
+use nunchi_clob::MarketId;
+use nunchi_common::{Address, Namespace, StateStore};
+use nunchi_house::VaultId;
+
+const NS: Namespace = Namespace::new(CBC_NAMESPACE);
+
+#[repr(u8)]
+#[derive(Clone, Copy)]
+enum Table {
+ Nonce = 0,
+ Params = 1,
+ MarketIndex = 2,
+ ClearingState = 3,
+ Intent = 4,
+ PendingIntents = 5,
+ Result = 6,
+ VaultNotional = 7,
+}
+
+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| CbcError::Storage(err.to_string()))
+}
+
+fn nonce_key(account: &Address) -> Digest {
+ NS.key(Table::Nonce, account.encode().as_ref())
+}
+
+fn params_key(market: &MarketId) -> Digest {
+ NS.key(Table::Params, market.encode().as_ref())
+}
+
+fn market_index_key() -> Digest {
+ NS.key(Table::MarketIndex, b"all")
+}
+
+fn clearing_state_key(market: &MarketId) -> Digest {
+ NS.key(Table::ClearingState, market.encode().as_ref())
+}
+
+fn intent_key(intent: &IntentId) -> Digest {
+ NS.key(Table::Intent, intent.encode().as_ref())
+}
+
+fn pending_intents_key(market: &MarketId) -> Digest {
+ NS.key(Table::PendingIntents, market.encode().as_ref())
+}
+
+fn result_key(market: &MarketId, batch_number: u64) -> Digest {
+ let mut logical = market.encode().as_ref().to_vec();
+ logical.extend_from_slice(batch_number.encode().as_ref());
+ NS.key(Table::Result, &logical)
+}
+
+fn vault_notional_key(vault: &VaultId, market: &MarketId) -> Digest {
+ let mut logical = vault.encode().as_ref().to_vec();
+ logical.extend_from_slice(market.encode().as_ref());
+ NS.key(Table::VaultNotional, &logical)
+}
+
+/// Typed state access required by [`crate::CbcLedger`].
+#[async_trait]
+pub trait CbcDB {
+ async fn cbc_nonce(&self, account: &Address) -> Result;
+
+ fn set_cbc_nonce(&mut self, account: &Address, nonce: u64);
+
+ async fn params(&self, market: &MarketId) -> Result