Skip to main content

Using a cycles management service

Beginner
Concept

Overview

Once a canister is deployed to the Internet Computer, the compute and storage it utilizes must be pre-paid. The process of burning ICP to obtain cycles and transferring them to a canister is referred to as "topping-up" a canister.

Cycles management services provide canister monitoring and automated cycles top-ups to ensure canister applications remain up and running and do not run out of cycles.

Instead of manually topping up a canister via dfx or scripting a custom solution, cycles management services provide tested top-up automation and canister metric insights.

Popular cycles management services include:

  • CycleOps - On-chain, proactive, no-code canister monitoring with historical trend graphs, top-up email notifications & downloadable transaction history.
  • TipJar - Donate cycles to your favorite canisters on the Internet Computer and keep them alive and healthy!
  • Canistergeek – Top up your canisters, monitor cycles, memory, and logs, and get your monthly reports in one place.

Cycles management libraries

Motoko

  • cycles-manager - Provides a simplified, permissioned cycles management framework for multi-canister applications (sponsored by CycleOps).
  • canistergeek-ic-motoko – Canistergeek-IC-Motoko is an open-source tool for Internet Computer to track your project canisters cycles and memory status and collect log messages.
  • canistergeek_ic_rust – canistergeek_ic_rust is an open-source tool for Internet Computer to track your project canisters cycles and memory status and collect log messages.

Creating custom autonomous top-up solutions

To create an autonomous canister that doesn't rely on a third-party service, you can create your own autonomous canister top-up solution. One option is to use a similar architecture implemented by CycleOps, where one canister acts as the cycles manager for the project and provides the API used to request more cycles when a canister within the project has its cycles balance dip below a preset threshold. Below is sample code for this architecture, where 'CyclesManager' is the management canister and 'Child' is another canister in the project that requests cycles from the 'CyclesManager'.

You will still need to top up the CyclesManager canister manually.

// Import packages
import { logand } "mo:base/Bool";
import { trap } "mo:base/Debug";
import Principal "mo:base/Principal";
import { endsWith; size } "mo:base/Text";

// Import the Mops package CyclesManager
import CyclesManager "mo:cycles-manager/CyclesManager";

// Implements the cycles_manager_transferCycles API of the CyclesManager.Interface

actor CyclesManager {
  // Initializes a cycles manager
  stable let cyclesManager = CyclesManager.init({
    // By default, with each transfer request 500 billion cycles will be transferred
    // to the requesting canister, provided they are permitted to request cycles
    //
    // This means that if a canister is added with no quota, it will default to the quota of #fixedAmount(500)
    defaultCyclesSettings = {
      quota = #fixedAmount(500_000_000_000);
    };
    // Allow an aggregate of 10 trillion cycles to be transferred every 24 hours
    aggregateSettings = {
      quota = #rate({
        maxAmount = 10_000_000_000_000;
        durationInSeconds = 24 * 60 * 60;
      });
    };
    // 50 billion is a good default minimum for most low use canisters
    minCyclesPerTopup = ?50_000_000_000;
  });

  // @required - IMPORTANT!!!
  // Allows canisters to request cycles from this "manager canister" that implements
  // the cycles manager
  public shared ({ caller }) func cycles_manager_transferCycles(
    cyclesRequested: Nat
  ): async CyclesManager.TransferCyclesResult {
    if (not isCanister(caller)) trap("Calling principal must be a canister");

    let result = await* CyclesManager.transferCycles({
      cyclesManager;
      canister = caller;
      cyclesRequested;
    });
    result;
  };

  // A very basic example of adding a canister to the cycles manager
  // This adds a canister with a 1 trillion cycles allowed per 24 hours cycles quota
  //
  // IMPORTANT: You must add authorization for production implementations so that not just any canister can add themselves
  public shared func addCanisterWith1TrillionPer24HoursLimit(canisterId: Principal) {
    CyclesManager.addChildCanister(cyclesManager, canisterId, {
      // This top up rule 1 Trillion every 24 hours
      quota = ?(#rate({
        maxAmount = 1_000_000_000_000;
        durationInSeconds = 24 * 60 * 60;
      }));
    })
  };

  // **DO NOT USE IN PRODUCTION** - for developer debugging and testing purposes only
  public func toText() : async Text {
    let result = CyclesManager.toText(cyclesManager);
    result;
  };

  func isCanister(p : Principal) : Bool {
    let principal_text = Principal.toText(p);
    // Canister principals have 27 characters
    size(principal_text) == 27
    and
    // Canister principals end with "-cai"
    endsWith(principal_text, #text "-cai");
  };
}

// Import the Mops package CyclesManager
import CyclesRequester "mo:cycles-manager/CyclesRequester";
import CyclesManager "mo:cycles-manager/CyclesManager";

import { print } "mo:base/Debug";

actor Child {
  // Stable variable holding the cycles requester
  stable var cyclesRequester: ?CyclesRequester.CyclesRequester = null;

  // A simple counter, for the purposes of this example
  stable var counter: Nat = 0;

  // Initialize the cycles requester
  // As an alternative, you can also initialize the cycles requester in the constructor
  public func initializeCyclesRequester(
    batteryCanisterPrincipal: Principal,
    topupRule: CyclesRequester.TopupRule,
  ) {
    cyclesRequester := ?CyclesRequester.init({
      batteryCanisterPrincipal;
      topupRule
    });
  };

  // An example of adding cycles request functionality to an arbitrary update function
  public func justAnotherCounterExample(): async () {
    // before doing something, check if we need to request cycles
    let result = await* requestTopupIfLow();
    print(debug_show(result));

    // Do something in the rest of the function;
    counter += 1;
  };

  // Local helper function you can use in your actor if the cyclesRequester could possibly be null
  func requestTopupIfLow(): async* CyclesManager.TransferCyclesResult {
    switch(cyclesRequester) {
      case null #err(#other("CyclesRequester not initialized"));
      case (?requester) await* CyclesRequester.requestTopupIfBelowThreshold(requester);
    }
  };
}

View the full example on GitHub.