An on-chain automation loop for your blockchain automation needs. Perfect for on-chain game loops.
This repository is optimized for automated code agents and LLM retrieval:
- Machine-readable project summary:
llms.txt - API and contract reference:
docs/index.md - Gas and economics assumptions:
gas-cost-analysis.md - ABI artifacts for integration tooling:
abi/contracts/*
Start here:
- Read
llms.txtandREADME.md - Compile and test with
npm run buildandnpm run test - Extract latest ABI bundle with
npm run extract-abi - Pair with
autoloop-workerfor loop execution and VRF proof delivery
AutoLoop is published to the Lucky Machines Verdaccio registry.
Set the registry profile for this repo:
npm run registry:local # http://localhost:4873
npm run registry:staging # https://staging-packages.luckymachines.io
npm run registry:prod # https://packages.luckymachines.io
npm run registry:set -- custom https://your-registry.example.com
npm run registry:pingThen install:
npm install @luckymachines/autoloopPublish to the configured registry:
npm run publish:registryFor Foundry projects, add a remapping to remappings.txt:
@luckymachines/autoloop/=node_modules/@luckymachines/autoloop/
Inherit from AutoLoopCompatible.sol and implement two functions:
import "@luckymachines/autoloop/src/AutoLoopCompatible.sol";
contract MyGame is AutoLoopCompatible {
function shouldProgressLoop()
external view override
returns (bool loopIsReady, bytes memory progressWithData)
{
loopIsReady = /* your condition */;
progressWithData = abi.encode(_loopID);
}
function progressLoop(bytes calldata progressWithData) external override {
uint256 loopID = abi.decode(progressWithData, (uint256));
require(loopID == _loopID, "stale");
// your game logic here
++_loopID;
}
}See NumberGoUp.sol for a complete example.
For contracts that need randomness only on certain ticks (e.g., every 10th tick for loot drops, crits, or spawns), inherit from AutoLoopHybridVRFCompatible.sol:
import "@luckymachines/autoloop/src/AutoLoopHybridVRFCompatible.sol";
contract MyHybridGame is AutoLoopHybridVRFCompatible {
// Decide when VRF is needed (e.g., every 10th tick)
function _needsVRF(uint256 loopID) internal view override returns (bool) {
return loopID % 10 == 0;
}
// Readiness check + game data
function _shouldProgress()
internal view override
returns (bool ready, bytes memory gameData)
{
ready = /* your condition */;
gameData = abi.encode(/* your data */);
}
// Standard tick — cheap, no randomness
function _onTick(bytes memory gameData) internal override {
// standard game logic
}
// VRF tick — includes verified randomness
function _onVRFTick(bytes32 randomness, bytes memory gameData) internal override {
// use randomness for loot, crits, spawns, etc.
}
}How Hybrid VRF works:
shouldProgressLoop()encodes(needsVRF, loopID, gameData)— the worker reads the flag- On standard ticks: worker sends data as-is, contract calls
_onTick()(~90k gas) - On VRF ticks: worker wraps in VRF envelope, contract verifies proof and calls
_onVRFTick()(~240k gas) - Detection uses data size (>= 640 bytes = VRF envelope) and ERC-165 interface ID
See HybridGame.sol for a complete example.
For contracts that need provably fair randomness, inherit from AutoLoopVRFCompatible.sol instead:
How VRF works:
- Your contract returns
shouldProgressLoop() => truewith the current_loopID - The worker detects VRF support via ERC-165 (
supportsInterface) - The worker generates an ECVRF proof off-chain using its private key and a deterministic seed
- The proof is wrapped around your game data and submitted to
progressLoop() VRFVerifier.solverifies the proof on-chain and outputs abytes32random value
Solidity integration (3 steps):
import "@luckymachines/autoloop/src/AutoLoopVRFCompatible.sol";
contract MyVRFGame is AutoLoopVRFCompatible {
// 1. shouldProgressLoop — same as standard loop
function shouldProgressLoop()
external view override
returns (bool loopIsReady, bytes memory progressWithData)
{
loopIsReady = /* your condition */;
progressWithData = abi.encode(_loopID);
}
// 2. progressLoop — verify VRF and use the randomness
function progressLoop(bytes calldata progressWithData) external override {
(bytes32 randomness, bytes memory gameData) =
_verifyAndExtractRandomness(progressWithData, tx.origin);
uint256 loopID = abi.decode(gameData, (uint256));
require(loopID == _loopID, "stale");
// Use randomness: e.g. uint256(randomness) % 6 + 1
++_loopID;
}
}// 3. Register the controller's VRF public key (call once per controller)
myVRFGame.registerControllerKey(controllerAddress, pkX, pkY);See RandomGame.sol for a complete VRF example.
AutoLoop charges a fee on every loop execution to sustain the protocol and compensate controllers (the off-chain bots that trigger progressLoop()).
Every call to progressLoop() charges the registered contract owner:
- Gas reimbursement — actual gas consumed (including a fixed buffer of 94,293 gas for overhead)
- Base fee — 70% of the update's gas cost, charged on top of gas reimbursement
The base fee is computed only on the gas used by the contract's progressLoop() call itself (not the buffer overhead).
The base fee is split between the protocol (Lucky Machines) and the controller that executed the transaction:
| Recipient | Share of base fee |
|---|---|
| Protocol | 50% |
| Controller | 50% |
The controller also receives the full gas reimbursement.
From AutoLoop.sol progressLoop() (lines 125-137):
fee = (txGas * tx.gasprice * 70) / 100
controllerFee = (fee * 50) / 100
totalCost = gasCost + fee
Controller receives: gasCost + controllerFee
Protocol accumulates: fee - controllerFee
Contract is charged: totalCost
Assume a loop execution uses 200,000 gas at a gas price of 50 gwei:
| Item | Calculation | Amount |
|---|---|---|
| Gas used (with buffer) | 200,000 + 94,293 | 294,293 gas |
| Gas cost (reimbursement) | 294,293 × 50 gwei | 0.01471465 ETH |
| Base fee | (200,000 × 50 gwei × 70) / 100 | 0.007 ETH |
| Controller fee (50% of base fee) | 0.007 × 50 / 100 | 0.0035 ETH |
| Protocol fee (50% of base fee) | 0.007 × 50 / 100 | 0.0035 ETH |
| Total charged to contract | 0.01471465 + 0.007 | 0.02171465 ETH |
| Controller receives | 0.01471465 + 0.0035 | 0.01821465 ETH |
| Protocol accumulates | 0.007 - 0.0035 | 0.0035 ETH |
Contract owners must deposit ETH into AutoLoop before their contract can be executed. Each progressLoop() call deducts totalCost from the contract's balance. If the balance is too low to cover gas + fees, the transaction reverts.
Deposits are made through the registrar role via deposit(registeredUser).
Contract owners can request a refund of their entire unused balance via requestRefund() (called through the registrar). The full remaining balance is sent to the specified address and the on-chain balance is reset to zero.
Accumulated protocol fees are tracked in _protocolBalance. The admin can withdraw any amount up to the accumulated balance by calling:
withdrawProtocolFees(uint256 amount, address toAddress)All parameters below can be adjusted by the contract admin (DEFAULT_ADMIN_ROLE):
| Parameter | Default | Setter | Description |
|---|---|---|---|
BASE_FEE |
70 | — | % of gas cost charged as fee (not directly settable post-init) |
PROTOCOL_FEE_PORTION |
50 | setProtocolFeePortion() |
% of base fee to protocol |
CONTROLLER_FEE_PORTION |
50 | setControllerFeePortion() |
% of base fee to controller |
MAX_GAS |
1,000,000 | setMaxGasDefault() |
Default max gas per execution |
MAX_GAS_PRICE |
40,000,000,000,000 (40k gwei) | setMaxGasPriceDefault() |
Default max gas price (wei) |
GAS_BUFFER |
94,293 | setGasBuffer() |
Overhead gas outside contract update |
GAS_THRESHOLD |
14,905,707 | setGasThreshold() |
Highest gas a user can set (15M - buffer) |
Setting PROTOCOL_FEE_PORTION automatically adjusts CONTROLLER_FEE_PORTION to 100 - value, and vice versa. Both must be ≤ 100.
Per-contract overrides for maxGas and maxGasPrice can be set through the registrar via setMaxGas() and setMaxGasPrice().
| Loop Type | Median Gas | Notes |
|---|---|---|
Standard (NumberGoUp) |
~90,000 | Pure automation, no randomness |
Hybrid VRF (HybridGame) |
~90k / ~240k | Standard ticks are cheap; VRF ticks only when _needsVRF() returns true |
Full VRF (RandomGame) |
~240,000 | ECVRF proof verification on every tick |
VRF adds approximately 150k gas of overhead for the elliptic curve operations in VRFVerifier.sol. At current L1 gas prices (~0.05 gwei), the VRF overhead costs less than $0.001 per tick.
Hybrid VRF is the sweet spot for most games — run cheap standard ticks with occasional VRF for random events (loot drops, critical hits, spawns). A game doing VRF every 10th tick pays ~$0.009/tick on average vs $0.022/tick for full VRF.
See gas-cost-analysis.md for full cost projections.
The autoloop-dashboard provides a web UI for one-click local setup, contract deployment, worker management, and real-time event monitoring — including VRF dice rolls and proof verification.
- autoloop-worker — Off-chain worker bot with VRF proof generation
- autoloop-dashboard — Web-based control panel and event monitor