diff --git a/apps/README.md b/apps/README.md
index abed47abfac0594c968b0d0e4857fc3d1815505b..20cc02072a153e156482705352901880a938f37d 100644
--- a/apps/README.md
+++ b/apps/README.md
@@ -6,6 +6,10 @@ This project has three command-line applications:
 - [sidenor-cli](./sidenor-cli)
 - [pubadmin-cli](./pubadmin-cli)
 
+There is also a simulator to generate a bunch of transactions and affect stats.
+
+- [@hypercog/batch-sim](./batch-sim)
+
 Finally, there is a common library that they use:
 
 - [@hypercog/utils](./utils)
@@ -98,4 +102,8 @@ Bid from cement-company1-com:eDUwOTo6Q049dXNlcixPVT1jbGllbnQrT1U9b3JnMStPVT1kZXB
 
 $ cd ../pubadmin-cli
 $ node index.js stats
+
+# Or to generate/simulate a complete cycle:
+$ cd ../batch-sim
+$ node index.js simulate
 ```
diff --git a/apps/batch-sim/index.js b/apps/batch-sim/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..22491b112ffdfa12a3405759107e228a2108f9e6
--- /dev/null
+++ b/apps/batch-sim/index.js
@@ -0,0 +1,82 @@
+// COPYRIGHT: FUNDACIÓN TECNALIA RESEARCH & INNOVATION, 2022.
+
+const { Option } = require("commander")
+
+const { createCLIApp, HypercogApiClient } = require("@hypercog/utils")
+const {
+  createAsset,
+  editComposition,
+  sendToCone,
+  sendToStock,
+  acceptBid
+} = require("@hypercog/sidenor-cli")
+
+const { bidAsset } = require("@hypercog/cement-cli")
+
+const signInAs = async (apiClient, userMail, password) => {
+  const [username, organization] = userMail.split("@")
+
+  const okLogin = await apiClient.signIn(username, password, organization)
+  if (!okLogin) {
+    console.error("Unauthorized login")
+    return
+  }
+}
+
+const createAssetAndSendToCone = async (apiClient, name) => {
+  const assetId = await createAsset(apiClient, name)
+  await editComposition(apiClient, { assetId })
+  return sendToCone(apiClient, { assetId })
+}
+
+const simulateCycle = async (arg1, cmd) => {
+  const { api, network, channel, chaincode, password, ...otherArgs } =
+    cmd.parent.opts()
+
+  const apiClient = new HypercogApiClient(api, network, channel, chaincode)
+
+  await signInAs(apiClient, otherArgs.sidenorUser, password)
+
+  let batchId = await createAssetAndSendToCone(apiClient, "simulated asset 1")
+  batchId = await createAssetAndSendToCone(apiClient, "simulated asset 2")
+  batchId = await createAssetAndSendToCone(apiClient, "simulated asset 3")
+
+  const stockId = await sendToStock(apiClient, { assetId: batchId })
+
+  console.log("Stock Id", stockId)
+  //await createAsset(apiClient, "simulated asset 2")
+  //await createAsset(apiClient, "simulated asset 3")
+
+  await signInAs(apiClient, otherArgs.cement1User, password)
+  await bidAsset(apiClient, { assetId: stockId, quantity: 1000, price: 2 })
+
+  await signInAs(apiClient, otherArgs.cement2User, password)
+  await bidAsset(apiClient, { assetId: stockId, quantity: 500, price: 3 })
+
+  await signInAs(apiClient, otherArgs.sidenorUser, password)
+  await acceptBid(apiClient, { assetId: stockId })
+}
+
+const program = createCLIApp()
+  .addOption(
+    new Option(
+      "-su --sidenorUser <value>",
+      "User who will operate with blockchain as a steel manufacturer"
+    ).env("SIDENOR_USER_ID")
+  )
+  .addOption(
+    new Option(
+      "-cu1 --cement1User <value>",
+      "User who will operate with blockchain as a cement company 1"
+    ).env("CEMENT1_USER_ID")
+  )
+  .addOption(
+    new Option(
+      "-cu2 --cement2User <value>",
+      "User who will operate with blockchain as a cement company 2"
+    ).env("CEMENT2_USER_ID")
+  )
+
+program.command("simulate").description("Simulate usage").action(simulateCycle)
+
+program.parse(process.argv)
diff --git a/apps/batch-sim/package-lock.json b/apps/batch-sim/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..860683f2e1dfb02d38d8dfd7131fde7b7eef18e8
--- /dev/null
+++ b/apps/batch-sim/package-lock.json
@@ -0,0 +1,57 @@
+{
+	"name": "@hypercog/batch-sim",
+	"lockfileVersion": 2,
+	"requires": true,
+	"packages": {
+		"": {
+			"name": "@hypercog/batch-sim",
+			"dependencies": {
+				"commander": "^9.0.0"
+			}
+		},
+		"../cement-cli": {
+			"name": "@hypercog/cement-cli",
+			"version": "0.0.1",
+			"extraneous": true,
+			"dependencies": {
+				"@hypercog/utils": "^0.0.1"
+			}
+		},
+		"../sidenor-cli": {
+			"name": "@hypercog/sidenor-cli",
+			"version": "0.0.1",
+			"extraneous": true,
+			"dependencies": {
+				"@faker-js/faker": "^5.5.3",
+				"@hypercog/utils": "^0.0.1"
+			}
+		},
+		"../utils": {
+			"name": "@hypercog/utils",
+			"version": "0.0.1",
+			"extraneous": true,
+			"dependencies": {
+				"@faker-js/faker": "^5.5.3",
+				"@traceblock/api-client": "^0.2.5",
+				"commander": "^9.0.0",
+				"dotenv": "^16.0.0",
+				"inquirer": "^8.2.0"
+			}
+		},
+		"node_modules/commander": {
+			"version": "9.0.0",
+			"resolved": "https://registry.npmjs.org/commander/-/commander-9.0.0.tgz",
+			"integrity": "sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw==",
+			"engines": {
+				"node": "^12.20.0 || >=14"
+			}
+		}
+	},
+	"dependencies": {
+		"commander": {
+			"version": "9.0.0",
+			"resolved": "https://registry.npmjs.org/commander/-/commander-9.0.0.tgz",
+			"integrity": "sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw=="
+		}
+	}
+}
diff --git a/apps/batch-sim/package.json b/apps/batch-sim/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..d547ac0d7c26fab53bff897ce6473e839fa12bc2
--- /dev/null
+++ b/apps/batch-sim/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "@hypercog/batch-sim",
+  "dependencies": {
+    "@hypercog/utils": "^0.0.1",
+    "@hypercog/sidenor-cli": "^0.0.1",
+    "@hypercog/cement-cli": "^0.0.1",
+    "commander": "^9.0.0"
+  }
+}
diff --git a/apps/cement-cli/index.js b/apps/cement-cli/index.js
index 78b906339d5f8d5cc1d67de4f42c2bb31a10d7df..6bf17a0aca0e51ca7aa45786f2cc00b1d8b6a8c4 100644
--- a/apps/cement-cli/index.js
+++ b/apps/cement-cli/index.js
@@ -2,47 +2,9 @@
 
 const inquirer = require("inquirer")
 
-const {
-  createCLIApp,
-  loginAndCallAfterwards,
-  promptAssetSelection
-} = require("@hypercog/utils")
+const { createCLIApp, loginAndCallAfterwards } = require("@hypercog/utils")
 
-const bidAsset = async (
-  apiClient,
-  { assetId: paramAssetId, quantity, price }
-) => {
-  const biddableAssets = await apiClient.getBiddableAssets()
-
-  const assetId = paramAssetId || (await promptAssetSelection(biddableAssets))
-
-  let chosenQuantity = quantity
-  if (!quantity) {
-    const { inputQuantity } = await inquirer.prompt([
-      {
-        type: "number",
-        name: "inputQuantity",
-        message: "Choose an ammount of slag"
-      }
-    ])
-    chosenQuantity = inputQuantity
-  }
-
-  let chosenPrice = price
-  if (!quantity) {
-    const { inputPrice } = await inquirer.prompt([
-      {
-        type: "number",
-        name: "inputPrice",
-        message: "Provide the offered price"
-      }
-    ])
-    chosenPrice = inputPrice
-  }
-
-  await apiClient.bidForAsset(assetId, chosenQuantity, chosenPrice)
-  console.log("Bid for slag stock", assetId)
-}
+const { bidAsset } = require("./lib")
 
 const program = createCLIApp()
 
@@ -55,3 +17,6 @@ program
   .action(loginAndCallAfterwards(bidAsset))
 
 program.parse(process.argv)
+
+// To be reused as a library in @hypercog/batch-sim
+module.exports = { bidAsset }
diff --git a/apps/cement-cli/lib.js b/apps/cement-cli/lib.js
new file mode 100644
index 0000000000000000000000000000000000000000..d76da1924639e51385f591c48cacaa1531234335
--- /dev/null
+++ b/apps/cement-cli/lib.js
@@ -0,0 +1,44 @@
+// COPYRIGHT: FUNDACIÓN TECNALIA RESEARCH & INNOVATION, 2022.
+
+const inquirer = require("inquirer")
+
+const { promptAssetSelection } = require("@hypercog/utils")
+
+const bidAsset = async (
+  apiClient,
+  { assetId: paramAssetId, quantity, price }
+) => {
+  const biddableAssets = await apiClient.getBiddableAssets()
+
+  const assetId = paramAssetId || (await promptAssetSelection(biddableAssets))
+
+  let chosenQuantity = quantity
+  if (!quantity) {
+    const { inputQuantity } = await inquirer.prompt([
+      {
+        type: "number",
+        name: "inputQuantity",
+        message: "Choose an ammount of slag"
+      }
+    ])
+    chosenQuantity = inputQuantity
+  }
+
+  let chosenPrice = price
+  if (!quantity) {
+    const { inputPrice } = await inquirer.prompt([
+      {
+        type: "number",
+        name: "inputPrice",
+        message: "Provide the offered price"
+      }
+    ])
+    chosenPrice = inputPrice
+  }
+
+  await apiClient.bidForAsset(assetId, chosenQuantity, chosenPrice)
+  console.log("Bid for slag stock", assetId)
+}
+
+// To be reused as a library in @hypercog/batch-sim
+module.exports = { bidAsset }
diff --git a/apps/cement-cli/package.json b/apps/cement-cli/package.json
index bc45a1c2d04829c4d39364fd44601ea063320885..b08528409da1f2a08288a2fb3b56435b35992f9a 100644
--- a/apps/cement-cli/package.json
+++ b/apps/cement-cli/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@hypercog/cement-cli",
   "version": "0.0.1",
-  "main": "index.js",
+  "main": "lib.js",
   "dependencies": {
     "@hypercog/utils": "^0.0.1"
   }
diff --git a/apps/lerna.json b/apps/lerna.json
index 5b6dae47fc33974a268dc6b4810d6061056b7930..f64339158c45c4752dfe6b92de81704bd10a7cde 100644
--- a/apps/lerna.json
+++ b/apps/lerna.json
@@ -1,4 +1,10 @@
 {
-  "packages": ["utils", "sidenor-cli", "cement-cli", "pubadmin-cli"],
+  "packages": [
+    "utils",
+    "sidenor-cli",
+    "cement-cli",
+    "pubadmin-cli",
+    "batch-sim"
+  ],
   "version": "0.0.1"
 }
diff --git a/apps/sidenor-cli/index.js b/apps/sidenor-cli/index.js
index 9f453de8ca039963f746bc7a1f9ca32b4ea4886b..ee4592d80aaf851fdc90465eca75c846d7faf761 100644
--- a/apps/sidenor-cli/index.js
+++ b/apps/sidenor-cli/index.js
@@ -1,211 +1,15 @@
 // COPYRIGHT: FUNDACIÓN TECNALIA RESEARCH & INNOVATION, 2021.
 
-const faker = require("@faker-js/faker")
-const inquirer = require("inquirer")
+const { createCLIApp, loginAndCallAfterwards } = require("@hypercog/utils")
 
 const {
-  createCLIApp,
-  loginAndCallAfterwards,
-  createRandomLocation,
-  promptAssetSelection,
-  decodeUserId,
-  TYPES,
-  STATUSES
-} = require("@hypercog/utils")
-
-const createAsset = async (apiClient, { assetName }) => {
-  const newAsset = await apiClient.createAsset({
-    id: `asst_${faker.datatype.uuid()}`,
-    type: TYPES.RESIDUO_PROCESOS_TERMICOS,
-    units: "kg",
-    quantity: faker.datatype.number({ max: 5000 }),
-    fields: {
-      description: "Escoria",
-      name: assetName,
-      status: STATUSES.SLAG,
-      castingId: 1
-    },
-    location: createRandomLocation()
-  })
-
-  console.log("Registered asset", newAsset.id)
-}
-
-const editComposition = async (apiClient, { assetId: paramAssetId }) => {
-  const assetId =
-    paramAssetId || (await promptAssetSelection(await apiClient.richQuery()))
-
-  const previousVersion = await apiClient.getAsset(assetId)
-
-  await apiClient.modifyAsset({
-    id: assetId,
-    fields: {
-      ...previousVersion.fields,
-      composition: "comp1"
-    }
-  })
-
-  console.log("New composition measure for", assetId)
-}
-
-const sendToCone = async (apiClient, { assetId: paramAssetId }) => {
-  const assetId =
-    paramAssetId ||
-    (await promptAssetSelection(
-      await apiClient.richQuery({
-        fields: {
-          status: STATUSES.SLAG
-        }
-      })
-    ))
-
-  const assetToBeSent = await apiClient.getAsset(assetId)
-  const assetsInCone = await apiClient.richQuery({
-    fields: {
-      status: STATUSES.SLAG_BATCH
-    }
-  })
-
-  const [firstAsset] = assetsInCone
-  const newConeQuantity =
-    assetToBeSent.quantity + (firstAsset ? firstAsset.quantity : 0)
-
-  const coneAsset = await apiClient.joinAsset(
-    //`cone_${faker.datatype.uuid()}`,
-    firstAsset ? [firstAsset.id, assetId] : [assetId],
-    { parentsShouldExist: false, bidirectional: true },
-    {
-      type: TYPES.RESIDUO_HIERRO_ACERO,
-      location:
-        assetsInCone.length === 0
-          ? assetToBeSent.location
-          : assetsInCone[0].location,
-      units: "kg",
-      quantity: newConeQuantity,
-      fields: {
-        status: STATUSES.SLAG_BATCH
-      }
-    }
-  )
-
-  // Archive individual slag and previous cone
-  await apiClient.deleteAsset(assetId)
-  if (assetsInCone.length > 0) await apiClient.deleteAsset(assetsInCone[0].id)
-
-  console.log("Moved to cone", coneAsset.id)
-}
-
-const sendToStock = async (apiClient, { assetId: paramAssetId }) => {
-  const assetId =
-    paramAssetId ||
-    (await promptAssetSelection(
-      await apiClient.richQuery({
-        fields: {
-          status: STATUSES.SLAG_BATCH
-        }
-      })
-    ))
-
-  const assetToBeSent = await apiClient.getAsset(assetId)
-  if (assetToBeSent.fields.status !== STATUSES.SLAG_BATCH) {
-    console.error("You can only combine a cone with a stock")
-    return
-  }
-
-  const selectedStockId = await promptAssetSelection(
-    [
-      ...(await apiClient.richQuery({
-        fields: {
-          status: STATUSES.STOCK
-        }
-      })),
-      {
-        id: "newStock",
-        type: TYPES.RESIDUO_HIERRO_ACERO,
-        fields: { name: "New stock", status: STATUSES.STOCK }
-      }
-    ],
-    "Select the stock where the cone will be merged"
-  )
-
-  if (!selectedStockId) {
-    console.error("You must select a stock")
-    return
-  }
-
-  let previousStock = false
-  if (selectedStockId !== "newStock") {
-    previousStock = await apiClient.getAsset(selectedStockId)
-  }
-  const newStockQuantity =
-    assetToBeSent.quantity + (previousStock ? previousStock.quantity : 0)
-
-  const newStockAsset = await apiClient.joinAsset(
-    selectedStockId === "newStock" ? [assetId] : [selectedStockId, assetId],
-    { parentsShouldExist: true, bidirectional: true },
-    {
-      type: TYPES.RESIDUO_HIERRO_ACERO,
-      location:
-        selectedStockId === "newStock"
-          ? assetToBeSent.location
-          : (
-              await apiClient.getAsset(selectedStockId)
-            ).location,
-      units: "kg",
-      quantity: newStockQuantity,
-      fields: {
-        status: STATUSES.STOCK,
-        name: "Stock"
-      }
-    }
-  )
-
-  // Archive individual slag and previous cone
-  await apiClient.deleteAsset(assetId)
-  if (selectedStockId !== "newStock")
-    await apiClient.deleteAsset(selectedStockId)
-
-  console.log("Moved to stock", newStockAsset.id)
-}
-
-const bidResponse =
-  action =>
-  async (apiClient, { assetId: paramAssetId, bidder: paramBidder }) => {
-    const assetId =
-      paramAssetId ||
-      (await promptAssetSelection(
-        await apiClient.richQuery({
-          fields: {
-            status: STATUSES.STOCK
-          }
-        })
-      ))
-
-    const assetSelected = await apiClient.getAsset(assetId)
-    if (!assetSelected.fields.bids || assetSelected.fields.bids.length === 0) {
-      console.error(`There are no active bids to be ${action}ed`)
-      return
-    }
-
-    let bidder = paramBidder
-    if (!bidder) {
-      const { inputBidder } = await inquirer.prompt([
-        {
-          type: "list",
-          name: "inputBidder",
-          message: `Select the bidder whose bid will be ${action}ed`,
-          choices: assetSelected.fields.bids.map(b => ({
-            name: `${b.bidder.id} (quantity: ${b.quantity}, price: ${b.price})`,
-            value: b.bidder
-          }))
-        }
-      ])
-      bidder = inputBidder
-    }
-
-    await apiClient[`${action}Bid`](assetId, bidder.id)
-    console.log(`Bid from ${bidder.id} ${action}ed for asset ${assetId}`)
-  }
+  createAsset,
+  editComposition,
+  sendToCone,
+  sendToStock,
+  acceptBid,
+  rejectBid
+} = require("./lib")
 
 const program = createCLIApp()
 
@@ -247,7 +51,7 @@ program
     "--bidder [value]",
     "Identifier of the bidder for the provided slag stock"
   )
-  .action(loginAndCallAfterwards(bidResponse("accept")))
+  .action(loginAndCallAfterwards(acceptBid))
 
 program
   .command("bid-reject")
@@ -257,7 +61,7 @@ program
     "--bidder [value]",
     "Identifier of the bidder for the provided slag stock"
   )
-  .action(loginAndCallAfterwards(bidResponse("reject")))
+  .action(loginAndCallAfterwards(rejectBid))
 
 /*program
   .command("discard-stock")
diff --git a/apps/sidenor-cli/lib.js b/apps/sidenor-cli/lib.js
new file mode 100644
index 0000000000000000000000000000000000000000..d7b89286d947da6f7aca733ea0ddbb01f5c93e37
--- /dev/null
+++ b/apps/sidenor-cli/lib.js
@@ -0,0 +1,225 @@
+// COPYRIGHT: FUNDACIÓN TECNALIA RESEARCH & INNOVATION, 2022.
+
+const faker = require("@faker-js/faker")
+const inquirer = require("inquirer")
+
+const {
+  createRandomLocation,
+  promptAssetSelection,
+  TYPES,
+  STATUSES
+} = require("@hypercog/utils")
+
+const createAsset = async (apiClient, { assetName }) => {
+  const newAsset = await apiClient.createAsset({
+    id: `asst_${faker.datatype.uuid()}`,
+    type: TYPES.RESIDUO_PROCESOS_TERMICOS,
+    units: "kg",
+    quantity: faker.datatype.number({ max: 5000 }),
+    fields: {
+      description: "Escoria",
+      name: assetName,
+      status: STATUSES.SLAG,
+      castingId: 1
+    },
+    location: createRandomLocation()
+  })
+
+  console.log("Registered asset", newAsset.id)
+
+  return newAsset.id
+}
+
+const editComposition = async (apiClient, { assetId: paramAssetId }) => {
+  const assetId =
+    paramAssetId || (await promptAssetSelection(await apiClient.richQuery()))
+
+  const previousVersion = await apiClient.getAsset(assetId)
+
+  await apiClient.modifyAsset({
+    ...previousVersion,
+    id: assetId,
+    fields: {
+      ...previousVersion.fields,
+      composition: "comp1"
+    }
+  })
+
+  console.log("New composition measure for", assetId)
+}
+
+const sendToCone = async (apiClient, { assetId: paramAssetId }) => {
+  const assetId =
+    paramAssetId ||
+    (await promptAssetSelection(
+      await apiClient.richQuery({
+        fields: {
+          status: STATUSES.SLAG
+        }
+      })
+    ))
+
+  const assetToBeSent = await apiClient.getAsset(assetId)
+  const assetsInCone = await apiClient.richQuery({
+    fields: {
+      status: STATUSES.SLAG_BATCH
+    }
+  })
+
+  const [firstAsset] = assetsInCone
+  const newConeQuantity =
+    assetToBeSent.quantity + (firstAsset ? firstAsset.quantity : 0)
+
+  const coneAsset = await apiClient.joinAsset(
+    //`cone_${faker.datatype.uuid()}`,
+    firstAsset ? [firstAsset.id, assetId] : [assetId],
+    { parentsShouldExist: false, bidirectional: true },
+    {
+      type: TYPES.RESIDUO_HIERRO_ACERO,
+      location:
+        assetsInCone.length === 0
+          ? assetToBeSent.location
+          : assetsInCone[0].location,
+      units: "kg",
+      quantity: newConeQuantity,
+      fields: {
+        status: STATUSES.SLAG_BATCH
+      }
+    }
+  )
+
+  // Archive individual slag and previous cone
+  await apiClient.deleteAsset(assetId)
+  if (assetsInCone.length > 0) await apiClient.deleteAsset(assetsInCone[0].id)
+
+  console.log("Moved to cone", coneAsset.id)
+
+  return coneAsset.id
+}
+
+const sendToStock = async (apiClient, { assetId: paramAssetId }) => {
+  const assetId =
+    paramAssetId ||
+    (await promptAssetSelection(
+      await apiClient.richQuery({
+        fields: {
+          status: STATUSES.SLAG_BATCH
+        }
+      })
+    ))
+
+  const assetToBeSent = await apiClient.getAsset(assetId)
+  if (assetToBeSent.fields.status !== STATUSES.SLAG_BATCH) {
+    console.error("You can only combine a cone with a stock")
+    return
+  }
+
+  const selectedStockId = await promptAssetSelection(
+    [
+      ...(await apiClient.richQuery({
+        fields: {
+          status: STATUSES.STOCK
+        }
+      })),
+      {
+        id: "newStock",
+        type: TYPES.RESIDUO_HIERRO_ACERO,
+        fields: { name: "New stock", status: STATUSES.STOCK }
+      }
+    ],
+    "Select the stock where the cone will be merged"
+  )
+
+  if (!selectedStockId) {
+    console.error("You must select a stock")
+    return
+  }
+
+  let previousStock = false
+  if (selectedStockId !== "newStock") {
+    previousStock = await apiClient.getAsset(selectedStockId)
+  }
+  const newStockQuantity =
+    assetToBeSent.quantity + (previousStock ? previousStock.quantity : 0)
+
+  const newStockAsset = await apiClient.joinAsset(
+    selectedStockId === "newStock" ? [assetId] : [selectedStockId, assetId],
+    { parentsShouldExist: true, bidirectional: true },
+    {
+      type: TYPES.RESIDUO_HIERRO_ACERO,
+      location:
+        selectedStockId === "newStock"
+          ? assetToBeSent.location
+          : (
+              await apiClient.getAsset(selectedStockId)
+            ).location,
+      units: "kg",
+      quantity: newStockQuantity,
+      fields: {
+        status: STATUSES.STOCK,
+        name: "Stock"
+      }
+    }
+  )
+
+  // Archive individual slag and previous cone
+  await apiClient.deleteAsset(assetId)
+  if (selectedStockId !== "newStock")
+    await apiClient.deleteAsset(selectedStockId)
+
+  console.log("Moved to stock", newStockAsset.id)
+
+  return newStockAsset.id
+}
+
+const bidResponse =
+  action =>
+  async (apiClient, { assetId: paramAssetId, bidder: paramBidder }) => {
+    const assetId =
+      paramAssetId ||
+      (await promptAssetSelection(
+        await apiClient.richQuery({
+          fields: {
+            status: STATUSES.STOCK
+          }
+        })
+      ))
+
+    const assetSelected = await apiClient.getAsset(assetId)
+    if (!assetSelected.fields.bids || assetSelected.fields.bids.length === 0) {
+      console.error(`There are no active bids to be ${action}ed`)
+      return
+    }
+
+    let bidder = paramBidder
+    if (!bidder) {
+      const { inputBidder } = await inquirer.prompt([
+        {
+          type: "list",
+          name: "inputBidder",
+          message: `Select the bidder whose bid will be ${action}ed`,
+          choices: assetSelected.fields.bids.map(b => ({
+            name: `${b.bidder.id} (quantity: ${b.quantity}, price: ${b.price})`,
+            value: b.bidder
+          }))
+        }
+      ])
+      bidder = inputBidder
+    }
+
+    await apiClient[`${action}Bid`](assetId, bidder.id)
+    console.log(`Bid from ${bidder.id} ${action}ed for asset ${assetId}`)
+  }
+
+const acceptBid = bidResponse("accept")
+const rejectBid = bidResponse("reject")
+
+// To be reused as a library in @hypercog/batch-sim
+module.exports = {
+  createAsset,
+  editComposition,
+  sendToCone,
+  sendToStock,
+  acceptBid,
+  rejectBid
+}
diff --git a/apps/sidenor-cli/package.json b/apps/sidenor-cli/package.json
index 9b0366e6a9cbb19e3a82734cb72662defeb0a638..3a3ea1f8db8a3cb61601bdd620420e7eb672a1aa 100644
--- a/apps/sidenor-cli/package.json
+++ b/apps/sidenor-cli/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@hypercog/sidenor-cli",
   "version": "0.0.1",
-  "main": "index.js",
+  "main": "lib.js",
   "scripts": {
     "sidenor": "func() { node index.js $@; }; func",
     "sidenor2": "node index.js --"
diff --git a/apps/utils/utils.js b/apps/utils/utils.js
index 2ccb083407de1ac2611f60de9abc5455e70fe6a1..8293535c98320635e210d22cf31fe787fececdb6 100644
--- a/apps/utils/utils.js
+++ b/apps/utils/utils.js
@@ -150,6 +150,7 @@ const decodeUserId = userId => {
 }
 
 module.exports = {
+  HypercogApiClient,
   createCLIApp,
   loginAndCallAfterwards,
   createRandomLocation,