Skip to main content
This guide walks you through building a Bitcoin price oracle that fetches the BTC/USD price every minute and writes it on-chain. It demonstrates cron triggers, contract codegen, external API calls, and collection storage.

How it works

  1. Cron trigger fires every minute
  2. CoinGecko API provides the current BTC/USD price
  3. On-chain contract receives the price via a typed contract class
  4. Collection stores the price for historical queries

Prerequisites

Project structure

bitcoin-oracle/
├── compose.yaml                    # Compose configuration
├── tsconfig.json                   # TypeScript config
├── src/
│   ├── contracts/
│   │   └── PriceOracle.json        # Contract ABI (generates typed class)
│   ├── lib/
│   │   └── utils.ts                # toBytes32 helper
│   └── tasks/
│       └── bitcoin-oracle.ts       # Main task

Step 1: Set up the project

Clone the example repository:
git clone https://github.com/goldsky-io/documentation-examples.git
cd documentation-examples/compose/bitcoin-oracle

Step 2: Generate contract types

The project includes a PriceOracle.json ABI in src/contracts/. Generate the typed contract class:
goldsky compose codegen
This creates a typed PriceOracle class in .compose/generated/ that provides type-safe contract interaction.

Step 3: Understand the task

The bitcoin-oracle.ts task handles everything:
import { TaskContext } from "compose";
import { toBytes32 } from "../lib/utils";

const ORACLE_CONTRACT = "0x34a264BCD26e114eD6C46a15d0A3Ba1873CaA708";

export async function main(context: TaskContext) {
  const { fetch, evm, collection } = context;

  const wallet = await evm.wallet({ name: "bitcoin-oracle-wallet" });

  // Instantiate the typed contract (generated from src/contracts/PriceOracle.json)
  const oracle = new evm.contracts.PriceOracle(
    ORACLE_CONTRACT,
    evm.chains.polygonAmoy,
    wallet
  );

  // Fetch Bitcoin price from CoinGecko API
  const response = await fetch<{ bitcoin: { usd: number } }>(
    "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
    {
      max_attempts: 3,
      initial_interval_ms: 1000,
      backoff_factor: 2,
    }
  );

  if (!response) {
    throw new Error("Failed to fetch Bitcoin price");
  }

  const bitcoinPrice = response.bitcoin.usd;
  const timestamp = Date.now();

  // Convert to bytes32 format and write on-chain
  const timestampAsBytes32 = toBytes32(timestamp);
  const priceAsBytes32 = toBytes32(Math.round(bitcoinPrice * 100));

  const { hash, receipt } = await oracle.write(timestampAsBytes32, priceAsBytes32);

  // Store in a collection for historical queries
  const priceHistory = await collection("bitcoin_prices");
  const { id } = await priceHistory.insertOne({
    price: bitcoinPrice,
    timestamp: timestamp,
  });

  return {
    success: true,
    oracleHash: hash,
    price: bitcoinPrice,
    timestamp,
    priceId: id,
  };
}

Key Compose features used

  • context.fetch — HTTP requests with built-in retry and backoff
  • evm.wallet — managed wallet with gas sponsorship
  • evm.contracts.PriceOracle — typed contract class generated from ABI JSON via compose codegen
  • collection — persistent document storage for price history

Step 4: Configure the Compose app

The compose.yaml uses a cron trigger to run every minute:
name: "bitcoin-oracle"
api_version: "stable"
tasks:
  - path: "./src/tasks/bitcoin-oracle.ts"
    name: "bitcoin_oracle"
    triggers:
      - type: "cron"
        expression: "* * * * *"
    retry_config:
      max_attempts: 2
      initial_interval_ms: 1000
      backoff_factor: 1

Step 5: Run locally

goldsky compose start
The task runs immediately and then every minute. You should see logs showing the fetched price and transaction hash.

Step 6: Deploy to Goldsky

goldsky compose deploy

Customization

Change the price source

Replace the CoinGecko URL with any API that returns a JSON price:
const response = await fetch<{ price: number }>(
  "https://your-api.com/price",
  { max_attempts: 3, initial_interval_ms: 1000, backoff_factor: 2 }
);

Use your own contract

  1. Drop your contract’s ABI JSON into src/contracts/MyContract.json
  2. Run goldsky compose codegen to generate the typed class
  3. Use it in your task:
const myContract = new evm.contracts.MyContract(
  "0xYOUR_CONTRACT_ADDRESS",
  evm.chains.baseSepolia,
  wallet
);
await myContract.yourMethod(arg1, arg2);

Change the cron schedule

triggers:
  - type: "cron"
    expression: "*/5 * * * *"  # every 5 minutes

Resources