diff --git a/move/operators/Move.lock b/move/operators/Move.lock
new file mode 100644
index 00000000..18983bb6
--- /dev/null
+++ b/move/operators/Move.lock
@@ -0,0 +1,26 @@
+# @generated by Move, please check-in and do not edit manually.
+
+[move]
+version = 2
+manifest_digest = "F3843E711A5044D1CF9708AF5AA3CEC7EDA8C17E39065DE7ACBC14C91B9C3B24"
+deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082"
+dependencies = [
+  { name = "Sui" },
+]
+
+[[move.package]]
+name = "MoveStdlib"
+source = { git = "https://github.com/MystenLabs/sui.git", rev = "mainnet-v1.25.3", subdir = "crates/sui-framework/packages/move-stdlib" }
+
+[[move.package]]
+name = "Sui"
+source = { git = "https://github.com/MystenLabs/sui.git", rev = "mainnet-v1.25.3", subdir = "crates/sui-framework/packages/sui-framework" }
+
+dependencies = [
+  { name = "MoveStdlib" },
+]
+
+[move.toolchain-version]
+compiler-version = "1.26.1"
+edition = "2024.beta"
+flavor = "sui"
diff --git a/move/operators/Move.toml b/move/operators/Move.toml
new file mode 100644
index 00000000..6e2e2051
--- /dev/null
+++ b/move/operators/Move.toml
@@ -0,0 +1,10 @@
+[package]
+name = "Operators"
+version = "0.1.0"
+edition = "2024.beta"
+
+[dependencies]
+Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "mainnet-v1.25.3" }
+
+[addresses]
+operators = "0x107"
diff --git a/move/operators/info.json b/move/operators/info.json
new file mode 100644
index 00000000..171fda61
--- /dev/null
+++ b/move/operators/info.json
@@ -0,0 +1,4 @@
+{
+    "singletons": [
+    ]
+}
\ No newline at end of file
diff --git a/move/operators/sources/operators.move b/move/operators/sources/operators.move
new file mode 100644
index 00000000..a68e262d
--- /dev/null
+++ b/move/operators/sources/operators.move
@@ -0,0 +1,381 @@
+module operators::operators {
+    use sui::bag::{Self, Bag};
+    use sui::vec_set::{Self, VecSet};
+    use sui::event;
+    use std::ascii::String;
+    use std::type_name;
+
+    // -----
+    // Types
+    // -----
+
+    /// The `OwnerCap` capability representing the owner of the contract.
+    public struct OwnerCap has key, store {
+        id: UID,
+    }
+
+    /// The `OperatorCap` capability representing an approved operator.
+    public struct OperatorCap has key, store {
+        id: UID,
+    }
+
+    /// The main `Operators` struct storing the capabilities and operator IDs.
+    public struct Operators has key {
+        id: UID,
+        // The number of operators are small in practice, and under the Sui object size limit, so a dynamic collection doesn't need to be used
+        operators: VecSet<address>,
+        // map-like collection of capabilities stored as Sui objects
+        caps: Bag,
+    }
+
+    // ------
+    // Errors
+    // ------
+
+    /// When the operator is not found in the set of approved operators.
+    const EOperatorNotFound: u64 = 0;
+
+    /// When the capability is not found.
+    const ECapNotFound: u64 = 1;
+
+    // ------
+    // Events
+    // ------
+
+    /// Event emitted when a new operator is added.
+    public struct OperatorAdded has copy, drop {
+        operator: address,
+    }
+
+    /// Event emitted when an operator is removed.
+    public struct OperatorRemoved has copy, drop {
+        operator: address,
+    }
+
+    /// Event emitted when a capability is stored.
+    public struct CapabilityStored has copy, drop {
+        cap_id: ID,
+        cap_name: String,
+    }
+
+    /// Event emitted when a capability is removed.
+    public struct CapabilityRemoved has copy, drop {
+        cap_id: ID,
+        cap_name: String,
+    }
+
+    // -----
+    // Setup
+    // -----
+
+    /// Initializes the contract and transfers the `OwnerCap` to the sender.
+    fun init(ctx: &mut TxContext) {
+        transfer::share_object(Operators {
+            id: object::new(ctx),
+            operators: vec_set::empty(),
+            caps: bag::new(ctx),
+        });
+
+        let cap = OwnerCap {
+            id: object::new(ctx),
+        };
+
+        transfer::transfer(cap, ctx.sender());
+    }
+
+    // ----------------
+    // Public Functions
+    // ----------------
+
+    /// Adds a new operator by issuing an `OperatorCap` and storing its ID.
+    public fun add_operator(self: &mut Operators, _: &OwnerCap, new_operator: address, ctx: &mut TxContext) {
+        let operator_cap = OperatorCap {
+            id: object::new(ctx),
+        };
+
+        transfer::transfer(operator_cap, new_operator);
+        self.operators.insert(new_operator);
+
+        event::emit(OperatorAdded {
+            operator: new_operator,
+        });
+    }
+
+    /// Removes an operator by ID, revoking their `OperatorCap`.
+    public fun remove_operator(self: &mut Operators, _: &OwnerCap, operator: address) {
+        self.operators.remove(&operator);
+
+        event::emit(OperatorRemoved {
+            operator,
+        });
+    }
+
+    /// Stores a capability in the `Operators` struct.
+    public fun store_cap<T: key + store>(self: &mut Operators, _: &OwnerCap, cap: T) {
+        let cap_id = object::id(&cap);
+        self.caps.add(cap_id, cap);
+
+        event::emit(CapabilityStored {
+            cap_id,
+            cap_name: type_name::get<T>().into_string(),
+        });
+    }
+
+    /// Allows an approved operator to borrow a capability by its ID.
+    public fun borrow_cap<T: key + store>(
+        self: &Operators,
+        _operator_cap: &OperatorCap,
+        cap_id: ID,
+        ctx: &mut TxContext
+    ): &T {
+        assert!(self.operators.contains(&ctx.sender()), EOperatorNotFound);
+        assert!(self.caps.contains(cap_id), ECapNotFound);
+
+        &self.caps[cap_id]
+    }
+
+    /// Allows an approved operator to borrow a capability by its ID.
+    public fun borrow_cap_mut<T: key + store>(
+        self: &mut Operators,
+        _operator_cap: &OperatorCap,
+        cap_id: ID,
+        ctx: &mut TxContext
+    ): &mut T {
+        assert!(self.operators.contains(&ctx.sender()), EOperatorNotFound);
+        assert!(self.caps.contains(cap_id), ECapNotFound);
+
+        &mut self.caps[cap_id]
+    }
+
+    /// Removes a capability from the `Operators` struct.
+    public fun remove_cap<T: key + store>(self: &mut Operators, _: &OwnerCap, cap_id: ID): T {
+        event::emit(CapabilityRemoved {
+            cap_id,
+            cap_name: type_name::get<T>().into_string(),
+        });
+
+        self.caps.remove<ID, T>(cap_id)
+    }
+
+    // -----
+    // Tests
+    // -----
+
+    #[test_only]
+    fun new_operators(ctx: &mut TxContext): Operators {
+        Operators {
+            id: object::new(ctx),
+            operators: vec_set::empty(),
+            caps: bag::new(ctx),
+        }
+    }
+
+    #[test_only]
+    fun destroy_operators(operators: Operators) {
+        let Operators { id, operators, caps } = operators;
+
+        id.delete();
+        caps.destroy_empty();
+
+        let mut keys = operators.into_keys();
+
+        while (!keys.is_empty()) {
+            keys.pop_back();
+        };
+
+        keys.destroy_empty();
+    }
+
+    #[test_only]
+    fun new_owner_cap(ctx: &mut TxContext): OwnerCap {
+        OwnerCap {
+            id: object::new(ctx),
+        }
+    }
+
+    #[test_only]
+    fun destroy_owner_cap(owner_cap: OwnerCap) {
+        let OwnerCap { id } = owner_cap;
+        object::delete(id);
+    }
+
+    #[test_only]
+    fun new_operator_cap(self: &mut Operators, ctx: &mut TxContext): OperatorCap {
+        let operator_cap = OperatorCap {
+            id: object::new(ctx),
+        };
+
+        self.operators.insert(ctx.sender());
+        operator_cap
+    }
+
+    #[test_only]
+    fun destroy_operator_cap(operator_cap: OperatorCap) {
+        let OperatorCap { id } = operator_cap;
+        object::delete(id);
+    }
+
+    #[test]
+    fun test_init() {
+        let ctx = &mut tx_context::dummy();
+        init(ctx);
+
+        let owner_cap = new_owner_cap(ctx);
+        destroy_owner_cap(owner_cap);
+    }
+
+    #[test]
+    fun test_add_and_remove_operator() {
+        let ctx = &mut tx_context::dummy();
+        let mut operators = new_operators(ctx);
+        let owner_cap = new_owner_cap(ctx);
+
+        let new_operator = @0x1;
+        add_operator(&mut operators, &owner_cap, new_operator, ctx);
+        assert!(operators.operators.size() == 1, 0);
+
+        let operator_id = operators.operators.keys()[0];
+        remove_operator(&mut operators, &owner_cap, operator_id);
+        assert!(operators.operators.is_empty(), 1);
+
+        destroy_owner_cap(owner_cap);
+        destroy_operators(operators);
+    }
+
+    #[test]
+    fun test_store_and_remove_cap() {
+        let ctx = &mut tx_context::dummy();
+        let mut operators = new_operators(ctx);
+        let owner_cap = new_owner_cap(ctx);
+        let operator_cap = new_operator_cap(&mut operators, ctx);
+        let external_cap = new_owner_cap(ctx);
+
+        let external_id = object::id(&external_cap);
+
+        store_cap(&mut operators, &owner_cap, external_cap);
+        assert!(operators.caps.contains(external_id), 0);
+
+        let borrowed_cap = borrow_cap<OwnerCap>(&operators, &operator_cap, external_id, ctx);
+        assert!(object::id(borrowed_cap) == external_id, 1);
+
+        let borrowed_mut_cap = borrow_cap_mut<OwnerCap>(&mut operators, &operator_cap, external_id, ctx);
+        assert!(object::id(borrowed_mut_cap) == external_id, 1);
+
+        let removed_cap = remove_cap<OwnerCap>(&mut operators, &owner_cap, external_id);
+        assert!(!operators.caps.contains(external_id), 2);
+
+        destroy_operator_cap(operator_cap);
+        destroy_owner_cap(owner_cap);
+        destroy_owner_cap(removed_cap);
+        destroy_operators(operators);
+    }
+
+    #[test]
+    #[expected_failure(abort_code = vec_set::EKeyDoesNotExist)]
+    fun test_remove_operator_fail() {
+        let ctx = &mut tx_context::dummy();
+        let mut operators = new_operators(ctx);
+        let owner_cap = new_owner_cap(ctx);
+
+        remove_operator(&mut operators, &owner_cap, ctx.sender());
+
+        destroy_owner_cap(owner_cap);
+        destroy_operators(operators);
+    }
+
+    #[test]
+    #[expected_failure(abort_code = EOperatorNotFound)]
+    fun test_borrow_cap_not_operator() {
+        let ctx = &mut tx_context::dummy();
+        let mut operators = new_operators(ctx);
+        let owner_cap = new_owner_cap(ctx);
+        let operator_cap = new_operator_cap(&mut operators, ctx);
+        let external_cap = new_owner_cap(ctx);
+
+        let external_id = object::id(&external_cap);
+
+        store_cap(&mut operators, &owner_cap, external_cap);
+        remove_operator(&mut operators, &owner_cap, ctx.sender());
+
+        borrow_cap<OwnerCap>(&operators, &operator_cap, external_id, ctx);
+
+        destroy_operator_cap(operator_cap);
+        destroy_owner_cap(owner_cap);
+        destroy_operators(operators);
+    }
+
+    #[test]
+    #[expected_failure(abort_code = ECapNotFound)]
+    fun test_borrow_cap_no_such_cap() {
+        let ctx = &mut tx_context::dummy();
+        let mut operators = new_operators(ctx);
+        let owner_cap = new_owner_cap(ctx);
+        let operator_cap = new_operator_cap(&mut operators, ctx);
+
+        let operator_id = object::id(&operator_cap);
+
+        borrow_cap<OwnerCap>(&operators, &operator_cap, operator_id, ctx);
+
+        destroy_operator_cap(operator_cap);
+        destroy_owner_cap(owner_cap);
+        destroy_operators(operators);
+    }
+
+    #[test]
+    #[expected_failure(abort_code = EOperatorNotFound)]
+    fun test_borrow_cap_mut_not_operator() {
+        let ctx = &mut tx_context::dummy();
+        let mut operators = new_operators(ctx);
+        let owner_cap = new_owner_cap(ctx);
+        let operator_cap = new_operator_cap(&mut operators, ctx);
+        let external_cap = new_owner_cap(ctx);
+
+        let external_id = object::id(&external_cap);
+
+        store_cap(&mut operators, &owner_cap, external_cap);
+        remove_operator(&mut operators, &owner_cap, ctx.sender());
+
+        borrow_cap_mut<OwnerCap>(&mut operators, &operator_cap, external_id, ctx);
+
+        destroy_operator_cap(operator_cap);
+        destroy_owner_cap(owner_cap);
+        destroy_operators(operators);
+    }
+
+    #[test]
+    #[expected_failure(abort_code = ECapNotFound)]
+    fun test_borrow_cap_mut_no_such_cap() {
+        let ctx = &mut tx_context::dummy();
+        let mut operators = new_operators(ctx);
+        let owner_cap = new_owner_cap(ctx);
+        let operator_cap = new_operator_cap(&mut operators, ctx);
+
+        let operator_id = object::id(&operator_cap);
+
+        borrow_cap_mut<OwnerCap>(&mut operators, &operator_cap, operator_id, ctx);
+
+        destroy_operator_cap(operator_cap);
+        destroy_owner_cap(owner_cap);
+        destroy_operators(operators);
+    }
+
+    #[test]
+    #[expected_failure(abort_code = sui::dynamic_field::EFieldDoesNotExist)]
+    fun test_remove_cap_fail() {
+        let ctx = &mut tx_context::dummy();
+        let mut operators = new_operators(ctx);
+        let owner_cap = new_owner_cap(ctx);
+        let operator_cap = new_operator_cap(&mut operators, ctx);
+        let external_cap = new_owner_cap(ctx);
+
+        let external_id = object::id(&external_cap);
+
+        let removed_cap = remove_cap<OwnerCap>(&mut operators, &owner_cap, external_id);
+
+        destroy_operator_cap(operator_cap);
+        destroy_owner_cap(owner_cap);
+        destroy_owner_cap(external_cap);
+        destroy_owner_cap(removed_cap);
+        destroy_operators(operators);
+    }
+}