Using a cycles management service
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
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: An open source Motoko tool for Internet Computer to track your project canisters cycles and memory status and collect log messages.
- canistergeek_ic_rust: An open source Rust 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.
Here is example code for the 'CyclesManager' canister:
// 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");
};
}
Here is example code for the 'Child' canister:
// 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);
}
};
}