# Compose Bitcoin Oracle

> Build and deploy the Goldsky Compose bitcoin-oracle example under the user's own account — a cron task that fetches BTC/USD from CoinGecko and writes `(timestamp, price)` as `bytes32` values to an on-chain `PriceOracle` contract via a Compose-managed wallet, appending each price to a durable collection. Triggers on: 'build a bitcoin price oracle', 'BTC/USD oracle onchain', 'push a price feed onchain', 'cron price oracle', 'set up / deploy the bitcoin-oracle example', 'compose price oracle'. Recommends the shared fully-unpermissioned oracle on Base Sepolia so there's nothing to deploy. Scaffolds the example from goldsky-io/documentation-examples, walks CLI install, contract choice (reuse shared / deploy own), wiring, optional GitHub publish, and a log-tailing smoke test. For a custom/novel Compose app that isn't this oracle, use /compose. For debugging an already-deployed app, use /compose-doctor. For manifest/CLI/API field lookups, use /compose-reference.

- Skill: `goldsky-io/compose-bitcoin-oracle` (Agent Skill)
- Install (CLI): `npx skillmds@latest add goldsky-io/compose-bitcoin-oracle`
- Raw SKILL.md: https://api.skillmd.com/api/skills/goldsky-io/compose-bitcoin-oracle/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: goldsky-io (https://skillmd.com/u/goldsky-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/goldsky-io/compose-bitcoin-oracle

---


# Build: Compose bitcoin-oracle

Stand up the bitcoin-oracle example under the user's own Goldsky account. A cron task fetches BTC/USD from CoinGecko and writes `(timestamp, price * 100)` as two `bytes32` values to a `PriceOracle` contract via a Compose-managed wallet. It also appends the price to a Compose `collection` for historical queries.

This template supplies only what's specific to the bitcoin-oracle app — how it works and its source. The recommended path uses a **shared, fully-unpermissioned `PriceOracle` on Base Sepolia**, so the user deploys nothing and the Compose smart wallet is auto-created and gas-sponsored.

## Step 0a — Load the base skills first

**Before anything else — before you answer, ask a question, scaffold a file, or call `deployComposeApp` — load the two base skills this template depends on:**

1. **`Skill(compose)`** — the always-on Compose guide: the golden rules (never assume anything about the app on the user's behalf; ask when unsure) and general build guidance.
2. **`Skill(compose-reference)`** — the manifest / field / API reference; consult before writing any `compose.yaml` or task file.

This template deliberately omits those rules and that reference — they are **required** to build correctly and are not repeated here. Do not proceed until both are loaded.

## Mode Detection

Pick the mode from the tools available to you:

- **A `deployComposeApp` tool is available (Goldsky webapp chatbot) — this is the preferred in-app flow.** Do NOT emit `goldsky` terminal commands or `cliCommand` cards, and do NOT use Step 0b / `degit` / `forge` / `goldsky compose deploy`. Give a 2-3 sentence plain explanation, then ask with `askUser` (tag the recommended option with `recommendedIndex`):
  1. **App name** — ask first, before anything else: *"What should the app be called? (suggest `bitcoin-oracle`)"*. The name is hard to change later — it scopes named wallets and participates in the CREATE2 salt for every `deployContract` — so it must be settled before any wallet or contract step. Accept the default `bitcoin-oracle` on a shrug, and set the chosen name as the top-level `name:` in the scaffolded `compose.yaml`.
  2. **Contract** — ask this explicitly, do not assume: *"Do you have your own `PriceOracle` contract, or should we use a shared demo oracle on Base Sepolia to get running quickly?"* Options: **"Use the shared demo oracle on Base Sepolia (recommended — nothing to deploy)"** and **"I'll use my own contract."** On the shared path, `ORACLE_CONTRACT` is the **HARDCODED** address `0x53deB3fF6E6e82A3b5E96f14E185e3Fe66BF5113` on `baseSepolia` — copy it character-for-character; mention in prose it's demos-only, not production. On the own path, ask the user to paste their contract address and chain and use exactly what they paste (their `PriceOracle` must let the Compose wallet write).
  3. **Update frequency** (recommend every minute, `* * * * *`).

  The Compose smart wallet is auto-created at runtime and gas-sponsored — never tell the user to create or fund a wallet. After the questions, scaffold the files in-memory (do NOT degit): `compose.yaml` (a single cron task on the chosen schedule), `src/contracts/PriceOracle.json` (the verbatim ABI in Step 3), and `src/tasks/bitcoin-oracle.ts` (fetch BTC/USD from CoinGecko, then `evm.contracts.PriceOracle.write(toBytes32(timestamp), toBytes32(Math.round(price*100)))` via the gas-sponsored smart wallet, appending each price to a `bitcoin_prices` collection). `ORACLE_CONTRACT` is a **hardcoded `const` at the top of the task file, not an env var** (for this example we keep it inline; `/compose` permits manifest `env:` instead — either is fine, just don't put a plain address in `secrets:`). Follow `/compose-reference` for the manifest shape and the sandbox import rule before emitting the files (per the golden rules in `/compose` — don't synthesize the manifest from memory). Then **call `deployComposeApp` in the SAME turn**; don't ask the user to confirm first or emit any `goldsky` command. **In this mode, ignore Steps 0–8 below** — they are the CLI/local procedure.
- **`Bash` is available (local CLI / coding agent):** execute the steps below directly, parsing output into later commands.
- **Neither (pure reference Q&A):** explain what the app does; for step-by-step help point them at `npx skills add goldsky-io/goldsky-agent` to run it locally with Bash.

## Non-negotiables

- **The shared oracle at `0x53deB3fF6E6e82A3b5E96f14E185e3Fe66BF5113` on Base Sepolia is fully unpermissioned — anyone can write to it.** It exists for getting started and demos only. Tell the user, in prose, that it must NOT be used in production. It only exists on Base Sepolia.
- **Never run `goldsky compose deployContract`, `goldsky compose deploy`, `git push`, or `gh repo create` without showing the exact command first and getting explicit confirmation.**
- **Deploy-your-own path only:** the contract's authorized writer must be the Compose wallet, or every `write()` reverts. On the shared-oracle path there is no writer restriction, so this does not apply.
- **The example ships only `src/contracts/PriceOracle.json` (the ABI), not Solidity source.** If deploying fresh, use the reference contract in this skill. Write the ABI verbatim — see Step 3.
- **Do not touch `src/lib/utils.ts`.** `toBytes32` is coupled to how the contract stores the value.

> **Steps 0–8 below are the Bash / local-CLI procedure. If a `deployComposeApp` tool is available (webapp chatbot), do NOT follow them — use the deploy-tool flow in Mode Detection above.**

## Step 0b — Scaffold the example

Pull just the bitcoin-oracle example into a fresh directory (no git history):

```bash
npx -y degit goldsky-io/documentation-examples/compose/bitcoin-oracle#6abe62878cb92f2569538ba9572b049ed5949a01 bitcoin-oracle
cd bitcoin-oracle
```

If `npx degit` is unavailable, fall back to a sparse clone:

```bash
git clone --filter=blob:none --sparse https://github.com/goldsky-io/documentation-examples.git
cd documentation-examples && git checkout 6abe62878cb92f2569538ba9572b049ed5949a01 && git sparse-checkout set compose/bitcoin-oracle && cd compose/bitcoin-oracle
```

If the user already cloned the example, skip this step and `cd` into it.

## Preflight

If `goldsky compose --version` prints `compose CLI is not installed`, run `goldsky compose install` (idempotent on the stable channel), then re-check.

**Version (compose 0.8.1).** Run `goldsky compose --version` — it prints `goldsky compose 0.8.1`. If the version is older than `0.8.1`, or `deployContract` / `writeContract` are unknown commands, the deploy-your-own path (Branch B in Step 3) won't work — OFFER to run `goldsky compose update` for the user and re-check `--version`. (Branch A — the shared oracle — only needs `compose deploy`, so an older CLI is fine there.)

The `goldsky` CLI, auth, and `deno` checks are the standard Compose preflight — see `/compose` and `/auth-setup`. Bitcoin-oracle-specific: the deploy-your-own path (Step 3, Branch B) uses **`goldsky compose deployContract`**, which bundles its own compiler — **Foundry is not required.**

## Step 1 — Configuration

Per the golden rules in `/compose`, ask only what you can't derive — and ask the app name first:

1. **"What should the app be called? (suggest `bitcoin-oracle`)"** — ask FIRST, before any wallet (Step 2) or contract (Step 3) step. The name is hard to change later: it scopes named wallets and participates in the CREATE2 salt for every `deployContract`, so it must be settled now. Accept the default `bitcoin-oracle` on a shrug, and set it as the top-level `name:` in `compose.yaml`.
2. **"Which chain?"** — **Base Sepolia (recommended)** because it has the ready, fully-unpermissioned shared oracle (nothing to deploy). Other options (Base, Arbitrum, Polygon Amoy, etc.) require deploying your own oracle. Use the camelCase form in TS (`baseSepolia`).
3. **"PriceOracle contract?"** (ask immediately after the chain) — two options:
   - **Reuse the shared oracle on Base Sepolia (recommended)** — nothing to deploy. Wire `0x53deB3fF6E6e82A3b5E96f14E185e3Fe66BF5113` (mention the address in prose, not in any option label). Demos/getting-started only, not production.
   - **Deploy my own** — see Step 3, Branch B. (Required on any chain other than Base Sepolia.)
4. **"How often should the cron run?"** — Every minute (recommended, `* * * * *`), every 5 minutes (`*/5 * * * *`), or every hour. Set the `expression:` under the `cron` trigger in `compose.yaml`.

## Step 2 — Wallet

- **Shared-oracle path (recommended):** nothing to do. The Compose smart wallet is auto-created at runtime and fully gas-sponsored on Base Sepolia. Do NOT tell the user to create or fund a wallet.
- **Deploy-your-own path (Branch B):** the wallet is created first, before any deploy, as step (1) of the Branch B ordering in Step 3; capture its address as `$COMPOSE_WALLET`. The named wallet `bitcoin-oracle-wallet` matches `evm.wallet({ name: "bitcoin-oracle-wallet" })` in `src/tasks/bitcoin-oracle.ts`.

## Step 3 — Contract

**Branch A — Reuse shared oracle (recommended).** `$CONTRACT_ADDRESS = 0x53deB3fF6E6e82A3b5E96f14E185e3Fe66BF5113` on Base Sepolia. No deploy, no writer authorization — but "nothing to deploy" is **not** "nothing to wire." The scaffold ships stale defaults (Polygon Amoy + a different oracle address), so the Step 4 wiring is **mandatory even on Branch A**. Skip to Step 4.

**Branch B — Deploy your own.** Branch B has a fixed ordering: **create the wallet → deploy the contract → codegen → wire (Step 4) → deploy the app (Step 7).** Follow steps (1)–(5) below in order. `deployContract` writes the full compiled ABI to `src/contracts/PriceOracle.json` for you, so there's nothing to confirm by hand. The JSON below is the subset the task actually touches — `write`, the two view getters, and the `PriceUpdated` event (the compiled output also exposes the constructor, `writer`, `setWriter`, and `OnlyWriter`). On the in-app / scaffold-inline path this subset is enough for codegen; never invent ABI fields:

```json
[{"inputs":[{"internalType":"bytes32","name":"timestamp","type":"bytes32"},{"internalType":"bytes32","name":"price","type":"bytes32"}],"name":"write","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"latestTimestamp","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"latestPrice","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"timestamp","type":"bytes32"},{"indexed":false,"internalType":"bytes32","name":"price","type":"bytes32"}],"name":"PriceUpdated","type":"event"}]
```

**(1) Create the wallet** (works before any deploy) and capture its address as `$COMPOSE_WALLET`. It matches `evm.wallet({ name: "bitcoin-oracle-wallet" })` in `src/tasks/bitcoin-oracle.ts`:

```bash
COMPOSE_WALLET=$(goldsky compose wallet create bitcoin-oracle-wallet --json | jq -r .address)
```

> ⚠ **Do NOT use `wallet create --env local`**: that's a LOCAL tevm wallet the cloud runtime never signs with, producing a bricked oracle the Troubleshooting section below will not diagnose. Use the default (cloud) wallet.

**(2) Write the source, then deploy the contract.** Run `mkdir -p contracts` and write this reference Solidity to `contracts/PriceOracle.sol`:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract PriceOracle {
    address public writer;
    bytes32 public latestTimestamp;
    bytes32 public latestPrice;

    event PriceUpdated(bytes32 indexed timestamp, bytes32 price);

    error OnlyWriter();

    constructor(address _writer) { writer = _writer; }

    function setWriter(address newWriter) external {
        if (msg.sender != writer) revert OnlyWriter();
        writer = newWriter;
    }

    function write(bytes32 timestamp, bytes32 price) external {
        if (msg.sender != writer) revert OnlyWriter();
        latestTimestamp = timestamp;
        latestPrice = price;
        emit PriceUpdated(timestamp, price);
    }
}
```

Then deploy through the Compose wallet. `deployContract` compiles in-CLI and deploys via a CREATE2 proxy signed by the gas-sponsored Compose wallet, so on Base / Base Sepolia it needs no funded EOA and no RPC URL. Every `deployContract` / `writeContract` needs a project API key. Auth: export `GOLDSKY_API_TOKEN=<project API key>` once in the shell (preferred: keeps the key out of argv and shell history), or pass `-t "$GOLDSKY_PROJECT_KEY"` per command, or use an active `goldsky login` session. Precedence: `--token` > `GOLDSKY_API_TOKEN` > `~/.goldsky/auth_token`. Make a key at Settings > API Keys in the dashboard. The constructor arg authorizes the wallet from step 1 as the writer:

```bash
ADDR=$(goldsky compose deployContract contracts/PriceOracle.sol \
  --chain-id <CHAIN_ID> \
  --constructor-args $COMPOSE_WALLET \
  --wallet bitcoin-oracle-wallet \
  --json | jq -r .address)
```

Chain IDs: `baseSepolia` → `84532`, `base` → `8453`, `polygonAmoy` → `80002`, `polygon` → `137`, `arbitrum` → `42161`, `optimism` → `10`. It auto-saves the ABI to `src/contracts/PriceOracle.json`. `--json` suppresses ascii art; on failure the command exits 1 with `{"error":true,"code":"...","message":"..."}` on stderr, so an empty capture means check stderr. Set `CONTRACT_ADDRESS=$ADDR`.

**Non-Base chains (forge fallback).** `deployContract` routes through the cloud's Alchemy bundler, which covers Ethereum, Sepolia, Polygon, Polygon Amoy, Arbitrum, Arbitrum Sepolia, Optimism, Optimism Sepolia, Base and Base Sepolia (chain IDs 1, 11155111, 137, 80002, 42161, 421614, 10, 11155420, 8453, 84532). On a chain outside that set the deploy fails with `No Alchemy bundler URL for chain <id>`, so use the `forge create` fallback there. Multi-provider coverage is tracked as FOU-991. On the forge path, the writer is still `$COMPOSE_WALLET`, so the task code is unchanged; extract the bare ABI afterward (see the note under the command). Don't ask the user for a private key; OFFER to generate a throwaway funded EOA: generate it straight into a chmod-600 file the model never reads, showing only the address:

```bash
cast wallet new --json > /tmp/k.json
umask 077 && printf 'FUNDED_EOA_KEY=%s\n' "$(jq -r '.[0].private_key' /tmp/k.json)" > .eoa.env
cast wallet address "$(jq -r '.[0].private_key' /tmp/k.json)"   # the ONLY thing ever printed
shred -u /tmp/k.json
```

Point the user at the right-chain faucet for the chosen chain, then OFFER to check funding with `cast balance <address> --rpc-url <RPC>` before deploying. If they decline, tell them plainly the alternative is deploying with their own funded wallet:

```bash
source .eoa.env   # provides FUNDED_EOA_KEY; keep this in the SAME shell invocation as the command
forge create contracts/PriceOracle.sol:PriceOracle \
  --rpc-url <RPC_URL> \
  --private-key "$FUNDED_EOA_KEY" \
  --broadcast \
  --constructor-args $COMPOSE_WALLET
```
`--broadcast` is required (forge ≥1.0 dry-runs by default and deploys nothing while looking successful), and `--constructor-args` MUST be the last flag — it is greedy variadic, so placing it before `--rpc-url` / `--private-key` makes forge swallow them and dial localhost. The deployed address is on the `Deployed to:` line — capture it as `$CONTRACT_ADDRESS`. Then extract the **bare ABI** (feeding the full `{abi, bytecode, ...}` artifact breaks codegen silently):

```bash
jq .abi out/PriceOracle.sol/PriceOracle.json > src/contracts/PriceOracle.json
```

**(3) Run codegen.** `deployContract` saved the ABI above (or the `jq .abi` extract did, on the forge path); `codegen` generates the typed `evm.contracts.PriceOracle` class the task imports (the CLI's own success output says to):

```bash
goldsky compose codegen
```

**(4) Wire** the address and chain into the task (Step 4). **(5) Deploy the app** so the wired task goes live (Step 7). A single deploy, not a redeploy: the wallet was created and contract deployed first, so this takes the wired task live.

Either way (`deployContract` or the `forge create` fallback), `$COMPOSE_WALLET` — the constructor arg — is what authorizes the writer, so there's nothing else to do. (Already have a pre-existing `PriceOracle`-shaped contract? Grant the Compose wallet write permission via `setWriter($COMPOSE_WALLET)` from the owner EOA and use its address instead.)

## Step 4 — Wire the contract address and chain into the task

Edit `src/tasks/bitcoin-oracle.ts` — use grep anchors:
- Find `const ORACLE_CONTRACT = "0x..."` near the top and replace the address with `$CONTRACT_ADDRESS`.
- Find the `evm.chains.*` reference inside the `new evm.contracts.PriceOracle(...)` call and set it to `evm.chains.<chosen chain in camelCase>` (e.g. `baseSepolia`).
- If you wired a different chain, also update the stale scaffold comment block that still says "Polygon Amoy" so the file's comments match.

If the user changed the cron cadence, edit the `expression:` under the `cron` trigger in `compose.yaml`.
- Set the top-level `name:` in `compose.yaml` to the chosen app name from Step 1 (the degit scaffold may ship a different default — overwrite it so `compose deploy` and the CREATE2 salt match).

## Step 5 — Gas (deploy-your-own, non-sponsored chains only)

Compose-managed wallets default to `sponsorGas: true` on sponsored chains (Base, Base Sepolia, Polygon, Polygon Amoy, and others). On those chains the wallet needs no funding; skip this step. On a non-sponsored chain, send native gas token to `$COMPOSE_WALLET` (testnet faucet, or budget for the cron cadence on mainnet: every-minute writes ≈ 1,440 tx/day). Note that this **runtime** task-gas sponsorship covers far more chains than the `deployContract` / `writeContract` cloud *deploy* path, which routes through the cloud's Alchemy bundler covering Ethereum, Sepolia, Polygon, Polygon Amoy, Arbitrum, Arbitrum Sepolia, Optimism, Optimism Sepolia, Base and Base Sepolia (chain IDs 1, 11155111, 137, 80002, 42161, 421614, 10, 11155420, 8453, 84532). On a chain outside that set the deploy fails with `No Alchemy bundler URL for chain <id>`, so use the `forge create` fallback there (Step 3). Multi-provider coverage is tracked as FOU-991. Then let runtime sponsorship (or a funded wallet) cover the cron writes.

## Step 6 — Optional: publish to a new GitHub repo

```bash
git init
git add .
if git ls-files --cached | grep -qiE '(keypair\.json|\.env|private[._-]?key|\.pem|id_rsa)'; then
  echo "⚠ Secret-shaped file is staged — remove it before committing. Aborting publish."
else
  git commit -m "Initial commit: Compose bitcoin-oracle"
  gh repo create <user's repo name> --<public|private> --source=. --push
fi
```

## Step 7 — Deploy to Goldsky

```bash
goldsky compose deploy
```

For Branch A (shared oracle) this is your first and only deploy. For Branch B this is also your only deploy: the wallet was created and the contract deployed first (Step 3), so this single deploy takes the wired task live.

## Step 8 — Smoke test

Stream logs and wait for the next cron fire (up to 1 minute):

```bash
goldsky compose logs -f
```

`-f` / `--follow` streams live so you catch the next cron fire; plain `goldsky compose logs` is a one-shot dump of the last 100 lines (`--tail`, default 100) and returns.

Also check runs (agents run non-interactively, so `runs` is more reliable than watching a live stream):

```bash
goldsky compose runs --task bitcoin_oracle --since 5m --json
```

Good output is a return payload with `success: true` and an `oracleHash` 0x-prefixed tx hash, repeating on cadence with no retries.

Verify on-chain (Base Sepolia explorer: `https://sepolia.basescan.org/address/$CONTRACT_ADDRESS#events`): you should see a `PriceUpdated` event per cron fire, and `latestPrice()` / `latestTimestamp()` should return recent `bytes32` values.

## Troubleshooting

- **Edits to `compose.yaml` or source files don't take effect after redeploy.** The local `.compose/` bundle cache is stale. Run `rm -rf .compose/` and redeploy.
- **Every cron run reverts (deploy-your-own only).** The Compose wallet isn't the authorized writer. Re-run `setWriter($COMPOSE_WALLET)` (Branch A of your own contract) or re-check the `deployContract` constructor arg. (Shared oracle has no writer restriction, so this can't be the cause there.)
- **`insufficient funds for gas`.** Only possible on a non-sponsored chain with deploy-your-own. Fund `$COMPOSE_WALLET`.
- **CoinGecko 429 / rate-limited.** The default retry config (3 attempts, backoff) handles transient rate-limits. If persistent, reduce cron cadence or switch to a paid API.
- **Task runs but no events on-chain.** Confirm the `evm.chains.*` reference matches the chain where the contract lives. A wallet on the wrong chain signs a tx that never appears on the intended chain.
- **`error: Unknown command "deployContract". Did you mean command "deploy"?` (or the `writeContract` variant).** Cause: the CLI is too old (older than 0.8.1). Fix: offer `goldsky compose update`, then re-check `goldsky compose --version`.
- **`This contract has already been deployed with this app` (re-running `deployContract`).** CREATE2 dedupes on contract + constructor args + app name, so an identical re-run is refused *before* the tx and the `--json` output has no `deployBlock`. A rerun can't recover the block. Levers: change a constructor arg, change the source, or use a different app name. The new name must not differ only by case or by `-`/`_`: names canonicalize (lowercase, `[-_]+` -> `-`), so `my-app`, `My_App`, and `my__app` collide and the deploy returns 409. PriceOracle takes a constructor arg, so prefer the **constructor-arg lever**. ⚠ Renaming the app changes *every* future deploy address and conflicts with the app name you `compose deploy` under, so avoid it unless intended.

## What you should NOT do

- Do not change the `toBytes32` helper in `src/lib/utils.ts`. The contract reads `price` as `bytes32` and the example scales by 100 (cents); changing either side silently breaks the stored value.
- Do not use the shared Base Sepolia oracle as a production target — it's open for anyone to write.
- Do not invent the `PriceOracle` ABI. Use the ABI from Step 3 (the JSON there is the subset the task touches; `deployContract` writes the full version).

## Related

- **`/compose`** — Build a new/custom Compose app from scratch, or explain what Compose is.
- **`/compose-reference`** — Manifest, CLI, TaskContext API, wallets, gas sponsorship, codegen.
- **`/compose-doctor`** — Diagnose and fix a broken Compose app.
- **`/auth-setup`** — `goldsky login` walkthrough.

