About Autometa

πŸ’‘ Inspiration

The inspiration for Autometa came from a simple frustration: why is blockchain automation so hard?

In traditional software, we have cron jobs, task schedulers, and automation platforms like Zapier. But in Web3, executing recurring tasks requires either:

  • Running your own infrastructure 24/7
  • Trusting centralized services with your private keys
  • Manually executing transactions every single time

We asked ourselves: "What if you could automate blockchain tasks as easily as setting a calendar reminder?"

That's when Autometa was born.

We chose to build on Polkadot's Moonbeam parachain because it offers the best of both worlds:

  • Ethereum compatibility - Use familiar tools and contracts
  • Polkadot security - Benefit from shared security across the entire ecosystem
  • Low fees - Affordable automation for everyone
  • Fast finality - Quick execution with ~12 second block times

πŸŽ“ What We Learned

Building Autometa taught us invaluable lessons about blockchain architecture, decentralized systems, and user experience:

1. Smart Contract Design Challenges

We learned that gas optimization mattersβ€”a lot. Our initial ActionExecutor contract consumed excessive gas due to inefficient data structures. We refactored to use:

// Before: Multiple storage reads
function execute(uint256 workflowId) external {
    Workflow memory wf = workflows[workflowId];  // SLOAD
    require(wf.active, "Inactive");              // Check
    // ... more SLOADs
}

// After: Single storage read with memory operations
function execute(uint256 workflowId) external {
    Workflow memory wf = workflows[workflowId];  // One SLOAD
    // All operations use memory copy
}

Result: ~40% gas savings per execution.

2. EVM Event Indexing at Scale

We discovered that Moonbase Alpha's RPC has strict query limits (1024 blocks max). Fetching historical events required implementing:

// Chunked event fetching with exponential backoff
const chunks = [];
for (let i = 0; i < totalBlocks; i += 1000) {
  const events = await fetchWithRetry(() =>
    contract.getEvents({
      fromBlock: startBlock + i,
      toBlock: Math.min(startBlock + i + 999, endBlock)
    })
  );
  chunks.push(...events);
}

Lesson: Always design for RPC limitations in production systems.

3. Race Conditions in Distributed Workers

Our initial worker design had a critical flaw: multiple workers could execute the same workflow simultaneously! We solved this with Redis-based atomic locks:

# Atomic workflow locking
def acquire_lock(workflow_id: int) -> bool:
    lock_key = f"workflow:{workflow_id}:lock"
    return redis.set(lock_key, worker_id, nx=True, ex=300)  # 5min TTL

Takeaway: Distributed systems require careful synchronization.

4. Web3 UX is Still Hard

We learned that Web3 UX requires hand-holding at every step:

  • Users don't understand gas deposits vs. execution costs
  • Transaction confirmations feel like black boxes
  • Error messages from contracts are cryptic

Our solution: Contextual explanations everywhere:

  • "Why do I need to deposit gas?" tooltips
  • Real-time transaction status updates
  • Human-readable error translations

πŸ—οΈ How We Built Autometa

Architecture Overview

Autometa is a full-stack decentralized application built on Moonbeam (Polkadot parachain) with the following components:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Frontend (Next.js 15)                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚   Dashboard  β”‚  β”‚   Templates  β”‚  β”‚ Wallet (Web3)β”‚      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ HTTP/WebSocket
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Backend API (FastAPI + Python)                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚  Workflow    β”‚  β”‚   Escrow     β”‚  β”‚  Registry    β”‚      β”‚
β”‚  β”‚  Service     β”‚  β”‚   Service    β”‚  β”‚  Service     β”‚      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ Web3.py
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           Moonbase Alpha (Polkadot Parachain)                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚  Workflow    β”‚  β”‚   Action     β”‚  β”‚   Fee        β”‚      β”‚
β”‚  β”‚  Registry    β”‚  β”‚   Executor   β”‚  β”‚   Escrow     β”‚      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Worker Infrastructure (Python + Redis)               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚  β”‚  Scheduler   │──Redis──▢ β”‚    Worker    β”‚               β”‚
β”‚  β”‚  (Scan chain)β”‚   Queue   β”‚  (Execute)   β”‚               β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Technology Stack

Smart Contracts (Solidity 0.8.20)

  • WorkflowRegistry.sol - Workflow storage and state management
  • ActionExecutor.sol - Execute actions (transfers, contract calls)
  • FeeEscrow.sol - Gas fee management with deposit/withdraw

Backend (Python 3.10+)

  • FastAPI for REST API
  • Web3.py for blockchain interaction
  • Redis for job queue
  • Uvicorn ASGI server

Frontend (TypeScript + React)

  • Next.js 15 with App Router
  • Viem for wallet integration
  • Tailwind CSS + shadcn/ui
  • RainbowKit for wallet connection

Worker Services

  • Scheduler: Scans chain every 10s for due workflows
  • Worker: Executes workflows from Redis queue
  • Atomic locks prevent duplicate execution

Key Implementation Details

1. Workflow Execution Flow

# Scheduler (runs every 10 seconds)
def scan_workflows():
    current_time = int(time.time())
    workflows = registry.get_all_workflows()

    for wf in workflows:
        if wf['active'] and wf['next_run'] <= current_time:
            redis.rpush('workflow_queue', wf['id'])

# Worker (processes queue)
def execute_workflow(workflow_id):
    # 1. Acquire lock
    if not acquire_lock(workflow_id):
        return  # Another worker got it

    # 2. Check escrow balance
    balance = escrow.get_balance(owner)
    if balance < gas_budget:
        log.error(f"Insufficient balance")
        return

    # 3. Execute on-chain
    tx_hash = registry.execute_workflow(workflow_id)

    # 4. Release lock
    release_lock(workflow_id)

2. Gas-Efficient Action Encoding

We encode action parameters efficiently to minimize storage costs:

// NATIVE transfer: [type(1B)][address(20B)][amount(32B)] = 53 bytes
bytes memory actionData = abi.encodePacked(
    uint8(1),                    // Action type
    address(recipient),          // 20 bytes
    uint256(amount)              // 32 bytes
);

// ERC20 transfer: [type(1B)][token(20B)][recipient(20B)][amount(32B)] = 73 bytes
bytes memory actionData = abi.encodePacked(
    uint8(2),
    address(token),
    address(recipient),
    uint256(amount)
);

3. User-Signed Workflow Creation

Users sign their own workflow creation transactions (no relayer needed):

// Frontend: User signs transaction
const hash = await writeContract({
  address: WORKFLOW_REGISTRY_ADDRESS,
  abi: WorkflowRegistryABI,
  functionName: 'createWorkflow',
  args: [triggerType, triggerData, actionType, actionData, interval, gasBudget],
  account: userAddress  // User's wallet
});

// Result: User owns the workflow, controls activation

🚧 Challenges We Faced

Challenge 1: Gas Overdraft Crisis

Problem: The FeeEscrow contract charged gas before checking execution success. Failed workflows still consumed gas, leading to:

  • User deposited 0.2 DEV
  • System charged 1.5 DEV (overdraft!)
  • Broken workflows in infinite retry loops

Solution:

// Before: Charge first, execute later
function executeWorkflow(uint256 workflowId) external {
    _chargeGas(owner, gasBudget);  // Charged even if execution fails
    _executeAction(actionData);
}

// After: Execute first, charge only on success
function executeWorkflow(uint256 workflowId) external {
    _executeAction(actionData);    // Revert if fails
    _chargeGas(owner, gasBudget);  // Only charged on success
}

We also added worker-side balance checks to prevent execution attempts when insufficient funds.

Challenge 2: Field Mapping Bug

Problem: Backend was reading wrong indices from smart contract, causing:

# Contract returns: (owner, triggerType, triggerData, actionType, actionData, nextRun, ...)
#                    0      1            2            3           4           5

# Backend read:
workflow['next_run'] = wf[3]      #  Actually actionType (value: 1)
workflow['action_type'] = wf[5]   # Actually nextRun (timestamp)

Result: Workflows showed "Next run: 1/1/1970" (Unix epoch 1)

Fix: Corrected field mapping to match Solidity struct order:

workflow = {
    'next_run': wf[5],       #  Correct index
    'action_type': wf[3],    # Correct index
    # ...
}

Challenge 3: Missing ActionType Byte

Problem: ActionExecutor expected actionData format:

[actionType(1B)][params...]

But frontend was sending:

[params...]  # Missing the type byte!

Result: All executions reverted with "Invalid actionType"

Solution: Updated encoder to prepend the type:

// Before
export function encodeNativeTransfer(to: string, amount: bigint): string {
  return ethers.solidityPacked(['address', 'uint256'], [to, amount]);
}

// After
export function encodeNativeTransfer(to: string, amount: bigint): string {
  return ethers.solidityPacked(
    ['uint8', 'address', 'uint256'],  // βœ… Prepend type
    [1, to, amount]                   // 1 = NATIVE
  );
}

Challenge 4: RPC Rate Limiting

Problem: Moonbase Alpha RPC returns "block range too wide" for queries >1024 blocks.

Solution: Implemented chunked fetching with retry logic:

const fetchWithRetry = async (fetcher: Function, maxRetries = 3) => {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fetcher();
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
      await sleep(1000 * Math.pow(2, attempt));  // Exponential backoff
    }
  }
};

Challenge 5: NULL Next Run Times

Problem: Some workflows had nextRun: null, preventing scheduling.

Root Cause: Frontend calculated nextRun = now + interval, but for immediate execution we wanted now + 60s. However, transaction delays caused the timestamp to be in the past by the time it confirmed.

Solution: Set nextRun = Math.floor(Date.now() / 1000) + 60 for immediate execution, giving a safe 60-second buffer.

🎯 What's Next

Autometa is just getting started. Our roadmap includes:

Phase 1: Enhanced Triggers (Q1 2026)

  • Price oracles integration (Chainlink on Moonbeam)
  • Wallet balance monitoring
  • Event-based triggers (e.g., "when NFT minted")

Phase 2: Multi-Chain Support (Q2 2026)

  • Expand to Moonriver (Kusama)
  • Cross-chain workflows via XCM
  • Polkadot relay chain integration

Phase 3: Advanced Actions (Q3 2026)

  • DeFi integrations (Uniswap, Aave)
  • NFT minting automation
  • DAO governance voting

Phase 4: Decentralization (Q4 2026)

  • Decentralized worker network
  • Staking for executors
  • On-chain governance

πŸ™ Acknowledgments

Autometa wouldn't exist without:

  • Polkadot & Moonbeam - For building the infrastructure that makes this possible
  • Moonbase Alpha Testnet - For providing a robust testing environment
  • OpenZeppelin - For secure smart contract libraries
  • The Web3 Community - For constant feedback and support

Built on Polkadot's Moonbeam parachain

Automating the future of Web3, one workflow at a time.

Built With

  • apis
  • asgi
  • cloud-services
  • databases
  • eslint
  • evm
  • fastapi-0.115
  • frameworks
  • hardhat
  • lucide
  • moonbase-alpha
  • moonbase-alpha-rpc
  • moonbeam
  • moonbeam-faucet
  • moonscan-explorer
  • next.js-15
  • node.js-20
  • npm
  • openzeppelin-contracts
  • platforms
  • polkadot-parachain
  • postcss
  • pydantic-2.10
  • python
  • python-3.10
  • radix-ui
  • rainbowkit
  • react-19
  • redis-5.2
  • shadcn/ui
  • solidity-0.8.20
  • tailwind-css-3.4
  • turbopack
  • typescript-5.6
  • uvicorn
  • venv
  • viem-2.39
  • wagmi
  • web3
  • web3.py-7.6
Share this project:

Updates