Goldsky Compose Reference
Reference for the compose.yaml manifest, the full goldsky compose CLI surface, the TaskContext API, wallets, gas sponsorship, contract codegen, the dashboard, and pricing. For interactive build flows use /compose; for debugging use /compose-doctor.
This is the reference layer of the Compose skill family.
/compose(loaded first) carries the general build rules and concepts; the template skills (/compose-bitcoin-oracle,/compose-vrf,/compose-dividend-distribution,/compose-compliance-oracle) carry example app source. Load this skill for any concrete field, flag, manifest shape, or API signature — and always before writing acompose.yamlor task file, rather than synthesizing the shape from memory.
Always validate the manifest before deploying.
goldsky compose startcatches schema errors fast.
Sandbox import rule — get this wrong and the task fails to bundle or crashes at runtime. Two things are NEVER imported: the Compose runtime capabilities and the EVM SDK.
env,fetch,callTask,logEvent,evm(wallets, chains, contracts,decodeEventLog), andcollectionall come from the injectedcontextargument — there is no@goldsky/compose-evm(or similar) package to import; reach chains viacontext.evm.chains.<name>, never by importingviemfor them. Beyond that, what you may import depends on whether the app has apackage.json:
- No
package.json(Deno-style app, e.g. bitcoin-oracle): import ONLY thecomposemodule (for types,import type { TaskContext } from "compose") and sibling project files (./lib/utils,../contracts/Foo). Any other bare import is rejected by the bundler.- Has a
package.json(esbuild-bundled, e.g. copy-trader withviem/@ethersproject/wallet, solana withgill): npm deps declared there ARE bundled and importable for local/pure use (crypto, signing, encoding). The hard limit is the network: Compose tasks run in a sandbox with no outbound socket of their own, so any package that does its own HTTP at runtime (axios,node-fetch, an SDK's built-in HTTP client) fails — route every network call throughcontext.fetchand use only the SDK's pure utilities.So: match the example you're scaffolding from. If it ships a
package.json, keep its npm imports; if it doesn't, don't introduce any.
Quick Reference
Most common lookups:
- Manifest top-level / task / trigger fields → compose.yaml Manifest
- CLI flags → CLI Commands
- TaskContext shape, IWallet, Collection → TaskContext API
- Smart wallet vs BYO EOA, gas sponsorship defaults → Wallets — Deep Dive
- Contract codegen workflow → Contract Codegen
--jsonoutput shapes → CLI JSON Schemas- Dashboard URL → Dashboard
compose.yaml Manifest
Top-level fields
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | /^[a-zA-Z0-9]([a-zA-Z0-9_\-]*[a-zA-Z0-9])?$/, starts and ends with a letter or number; letters, numbers, underscores, hyphens. Uppercase and leading digits are allowed. The platform additionally rejects a name that canonicalizes (lowercase, [-_]+->-) onto an existing app's, with a 409, so my-app, My_App, and my__app cannot coexist |
api_version |
string | deploy-only | semver (e.g. 0.1.0) or stable / preview / canary (any internal-* prefix is also accepted) |
tasks |
array | yes | Non-empty |
secrets |
string[] | no | Names only — values set via compose secret set |
env |
{ local?, cloud? } |
no | env's only valid children are local and cloud — each a Record<string, string> flattened into context.env. A bare env.MY_VAR (a var name directly under env) is rejected: "not a valid key". A hardcoded per-app constant belongs in the task file, not here. |
Task fields
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | /^[a-zA-Z0-9][a-zA-Z0-9_.\-]*$/, starts with a letter or number; letters, numbers, underscores, hyphens, dots. A leading underscore is no longer allowed (_internal_task is now rejected) |
path |
string | yes | Relative path to the .ts task file |
triggers |
array | yes | One or more; at most one per type |
retry_config |
object | no | { max_attempts, initial_interval_ms, backoff_factor } - all three required when set; the manifest validator rejects a task with any field outside name, path, retry_config, triggers |
Trigger types
cron
- type: cron
expression: "*/15 * * * *" # 5-field cron
http
- type: http
authentication: auth_token # or "none"
ip_whitelist: ["1.2.3.4", "10.0.0.0/8"] # optional, IPv4/IPv6/CIDR
onchain_event
- type: onchain_event
network: polygon_amoy # snake_case required
contract: "0xYourContractAddress" # 0x + 40 hex
events:
- "Transfer(address,address,uint256)" # viem signature strings, optional
dataset_version: "..." # optional
Full manifest example
name: my-app
api_version: stable
secrets:
- COINGECKO_API_KEY
- ORACLE_SIGNER_KEY
env:
cloud:
LOG_LEVEL: info
local:
LOG_LEVEL: debug
tasks:
- name: update_oracle
path: src/tasks/update-oracle.ts
retry_config:
max_attempts: 3
initial_interval_ms: 1000
backoff_factor: 2
triggers:
- type: cron
expression: "*/5 * * * *"
- name: manual_trigger
path: src/tasks/manual-trigger.ts
triggers:
- type: http
authentication: auth_token
CLI Commands
All commands accept -t/--token and --api-server; the -n/--name flag selects the app by name (falls back to -m/--manifest, then ./compose.yaml). Token precedence is --token > the GOLDSKY_API_TOKEN env var > ~/.goldsky/auth_token (written by goldsky login). With none of the three the CLI errors with "Please run goldsky login, set GOLDSKY_API_TOKEN, or pass --token to the command."
Non-interactive guards.
initwithout a name,deploywith a majorapi_versionmismatch (needs--force, message "Refusing to deploy with a major api_version mismatch in non-interactive mode. Pass --force to override."), andcleanwithout-f("Use --force for non-interactive cleanup.") all abort in a non-TTY. Agents always run non-TTY.
Lifecycle
| Command | Purpose | Key flags |
|---|---|---|
compose init |
Scaffold new app | [name] (prompts only when a TTY and no name) |
compose start |
Run locally (there is no dev command) |
--fork-chains, --cloud, --impersonate, -p/--port |
compose deploy |
Bundle + upload to cloud | -m, -t, -f (Skip version compatibility prompts (required to deploy with a major version mismatch when not running in a terminal)), --sync-env, --json |
compose status |
Show runtime status | -n, --json |
compose list |
List all apps | --json |
compose history |
Show deployment history for an app | -n, --limit (default 20, server caps at 100), --offset (default 0), --include-failures, --json |
compose pause |
Pause | -n, --json |
compose resume |
Resume | -n, --json |
compose delete |
Delete (type-to-confirm; --force for CI) |
-n, --force, --delete-database, --json |
compose logs |
View / tail logs | -f, --tail (default 100), --level, --search, --since, --max-lines, --timeout <duration>, --json |
compose clean |
Wipe local .compose/stage.db |
-f, -c/--config <config> |
compose update [version] |
Re-download the compose binary | [version] (stable/preview/semver), --preview |
compose callTask <task_name> <payload> |
Invoke a task with a JSON payload. Defaults to the deployed app | --env <local|cloud> (default cloud), -p/--port <port> (implies local; --port with an explicit --env cloud is an error), -n, -m, -t, --api-server, --json |
Local port resolution for callTask --env local: --port flag > .compose/.port > 4000. Connection refused, an unknown task name, and a stale port file each produce a distinct message and exit 1 (codes CONNECTION_REFUSED, TASK_NOT_FOUND, INVALID_RESPONSE, INVALID_PAYLOAD, INVALID_ENV, INVALID_FLAGS).
Secrets
| Command | Purpose |
|---|---|
compose secret set <SECRET_NAME> --value <value> [-n <app>] [--env local|cloud] [--redeploy] |
Set a secret (name is positional; -n = app) |
compose secret delete <secret_name> [-n <app>] [--env local|cloud] |
Delete |
compose secret list [-n <app>] |
List (no --env flag) |
compose deploy --sync-env |
Upload all of .env to cloud at deploy time (there is no secret sync) |
Wallets
| Command | Purpose |
|---|---|
compose wallet create <wallet_name> [-n <app>] [--env local|cloud] [--json] |
Create managed wallet; prints address (name is positional; -n = app) |
compose wallet list [-n <app>] [--env local|cloud] [--json] |
Table: name, address, type (privy / private_key / tevm), created_at |
Codegen
compose codegen — parse all src/contracts/*.json ABIs, write .compose/generated/index.ts and .compose/types.d.ts. Runs automatically inside compose init, compose start, and during deploy.
Contracts
Compile and deploy a contract, or submit a write call, straight from the CLI — no Foundry needed (deployContract bundles solc). Added in 0.8.0; the forge-style multi-arg/array constructor syntax needs ≥ 0.8.1 — run goldsky compose update if you're older.
compose deployContract <file.sol> — compiles in-CLI and deploys via a CREATE2 proxy through the app's Compose wallet. The forge-style multi-arg/array constructor syntax needs ≥ 0.8.1.
| Flag | Purpose |
|---|---|
--chain-id <id> |
Target chain (required). e.g. Base Sepolia 84532, Base 8453, Polygon 137, Polygon Amoy 80002, Arbitrum 42161, Optimism 10. |
--constructor-args <tokens...> |
Forge-style: space-separated, one token per param; arrays "[a,b]", tuples "(a,b)", nesting allowed, negatives " -5" (quoted leading space). Coerced against the compiled ABI. |
--wallet <name> |
App wallet that deploys (default default). Match the evm.wallet({ name }) the task code uses when the contract must authorize that wallet. |
--verify |
Verify the contract on the block explorer. |
--force |
Bypass the msg.sender-in-constructor guard (the sender is the CREATE2 proxy, not the wallet). |
-m / -t |
Manifest path / project token (see Lifecycle). |
--api-server |
API server URL (global flag). |
--json |
JSON output (see CLI JSON Schemas). |
Run from the app directory - it reads compose.yaml for the app name; app name + project id derive the deterministic CREATE2 salt. On gas-sponsored chains (Base 8453, Base Sepolia 84532) it needs no funded key and no RPC URL. 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 (the constructor args stay the same; supply the ABI to src/contracts/ yourself). Multi-provider coverage is tracked as FOU-991. Runtime task-gas sponsorship (see Gas Sponsorship) covers many more chains than this deploy endpoint.
Wallet lifecycle. wallet create and wallet list now work even before the app is deployed (they provision the hosted store on demand, like deployContract/writeContract); wallet create returns the wallet's address pre-deploy. To authorize a wallet inside a constructor: wallet create <name> → deployContract --constructor-args <address> → wire the address into the task → compose deploy.
CREATE2 collisions. Re-deploying the same contract source + constructor args from the same app hits a CREATE2 collision: the platform returns HTTP 400 CONTRACT_ALREADY_DEPLOYED and the CLI reports "This contract has already been deployed with this app. The same contract + app name + project produces the same address via CREATE2." with exit 1 and no Deploy Block (--json code ALREADY_DEPLOYED; any other deploy failure is DEPLOY_CONTRACT_FAILED). A changed constructor arg, changed source, or a different app name produces a fresh address. Note the app name participates in the CREATE2 salt - renaming the app changes every future deploy address. On success it prints the contract address, tx hash, and Deploy Block, and auto-saves the ABI to src/contracts/<Name>.json - then compose codegen gives typed bindings.
compose writeContract — submit a write call to a deployed contract.
| Flag | Purpose |
|---|---|
--chain-id <id> |
Target chain (required). |
--to <address> |
Target contract address (required). |
--function "sig(types)" |
Function signature, e.g. "setValue(uint256)". |
--args <tokens...> |
Same forge-style grammar as --constructor-args. |
--data <hex> |
Raw calldata alternative to --function / --args. |
--value <amount> |
Native value to send; suffix a unit (wei, gwei, ether), e.g. --value 1ether. |
--wallet <name> |
App wallet that signs (default default). |
--api-server |
API server URL (global flag). |
--json |
JSON output (see CLI JSON Schemas). |
Read-back
| Command | Purpose | Key flags |
|---|---|---|
compose runs [runId] |
List runs, or show one run's detail | --limit, --offset, --task <name>, --status <success|error|pending>, --since <1h|30m|7d>, --until <duration>, --json, -n/-m, -t, --api-server |
compose collections list |
Table of the app's collection names | targeting + --json |
compose collections query <collectionName> |
Query one collection | --filter <json> (must be a JSON object), --limit (default 100, backend max 1000), --offset, --json, targeting |
compose source [taskName] |
Print the deployed app's source file list, or one task's source. Never writes to disk | targeting + --json |
compose download |
Download the deployed app's source archive | -o/--output <path> (default <app>.zip, refuses to overwrite an existing file), --json, targeting |
TaskContext API
main(context: TaskContext, params?: Record<string, unknown>): Promise<unknown> receives:
type TaskContext = {
env: Record<string, string>;
logger: {
info(message: string, data?: Record<string, unknown>): void;
warn(message: string, data?: Record<string, unknown>): void;
error(message: string, data?: Record<string, unknown>): void;
};
fetch: FetchFn;
callTask: <Args, T>(name: string, args: Args, retryConfig?: RetryConfig) => Promise<T>;
logEvent: (event: { code: string; message: string; data?: unknown }) => Promise<void>;
evm: {
chains: Record<string, Chain>; // re-exported from viem internally — access via context.evm.chains.<name>, do NOT import viem
wallet: (config: WalletConfig) => Promise<IWallet>;
decodeEventLog: <T>(abi: AbiItem[], log: OnchainEvent) => Promise<T>;
contracts: Record<string, ContractClass>; // populated by codegen
};
collection: <T>(name: string, indexes?: CollectionIndexSpec[]) => Promise<Collection<T>>;
sideEffect: <T>(fn: () => T | Promise<T>) => Promise<T>;
};
No secrets namespace. Secrets flatten into context.env. For output: context.logger.info/warn/error(message, data?) is the structured, run-correlated logger (each line carries taskName, runId, appId, level, timestamp). console.log is fine for free-form output. logEvent still works but is marked @deprecated in the runtime types and will be removed in a future major version.
fetch (overloads)
type FetchConfig = {
method?: string; // defaults to "GET"
headers?: Record<string, string>;
body?: Record<string, unknown> | string; // objects are JSON.stringify'd
};
interface FetchFn {
<T>(url: string, retryConfig?: RetryConfig): Promise<T | undefined>;
<T>(url: string, config?: FetchConfig, retryConfig?: RetryConfig): Promise<T | undefined>;
}
- The second argument is a config object, not a bare body:
ctx.fetch(url, { method: "POST", body: { a: 1 }, headers: { "X-Key": k } }). Passing a raw payload object as the second argument does not send a body. Unrecognized keys are dropped and the request goes out as a GET. - A non-2xx response throws
Fetch failed with status <code> <statusText>: <body>. - The response is JSON-parsed and returns
undefinedwhen the body is not JSON. - This is not
window.fetch. Use this, not nativefetch.
callTask
callTask<Args, T>(name: string, args: Args, retryConfig?: RetryConfig): Promise<T>
Tis whatever the callee returns. Avoid-returning task resolves toundefined.- Use for task-to-task invocation (parent/child patterns).
sideEffect
sideEffect<T>(fn: () => T | Promise<T>): Promise<T>
Wraps a non-deterministic value (timestamp, UUID, random) so it is cached like any other context call. Durable resumption replays a task from the start, so an unwrapped Date.now() changes on replay. The callback runs once. On replay the host returns the cached value and the callback never runs.
RetryConfig
type RetryConfig = {
max_attempts: number; // ≥0
initial_interval_ms: number; // >0
backoff_factor: number; // >0
};
All three fields are required when you pass a retryConfig explicitly (the manifest validator enforces this for retry_config too). Omitting it does not mean one attempt:
| Scope | Default |
|---|---|
A task with no retry_config |
{ max_attempts: 3, initial_interval_ms: 1000, backoff_factor: 2 } |
Safe/read-only context calls: readContract, simulate, getBalance, and ctx.fetch with GET/HEAD/OPTIONS |
{ max_attempts: 3, initial_interval_ms: 500, backoff_factor: 2 } |
Everything else (callTask, writeContract, sendTransaction, prepareUserOperation, submitSignedUserOperation, wallet create/save, and ctx.fetch with POST/PUT/PATCH/DELETE) |
{ max_attempts: 1, initial_interval_ms: 500, backoff_factor: 2 }, held at 1 deliberately to avoid blind retries of non-idempotent calls |
OnchainEvent (for decodeEventLog and onchain_event triggers)
type
blockNumber: number;
blockHash: string;
transactionIndex: number;
removed: boolean;
address: string;
data: Hex;
topics: Hex[];
transactionHash: string;
logIndex: number;
};
For onchain_event-triggered tasks, params contains { log: OnchainEvent } plus chain-specific metadata. decodeEventLog(abi, params.log) returns the decoded struct.
IWallet
interface IWallet {
readonly name: string;
readonly address: Address;
writeContract(
chain: Chain,
contractAddress: Address,
functionSig: string, // signature string only, no ABI item
args: unknown[],
confirmation?: TransactionConfirmation,
retryConfig?: RetryConfig,
): Promise<{ hash: string; receipt: TransactionReceipt; userOpHash?: string }>;
sendTransaction(
config: {
to: Address; data: Hex; chain: Chain;
value?: bigint; maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint;
gas?: bigint; nonce?: number;
},
confirmation?: TransactionConfirmation,
retryConfig?: RetryConfig,
): Promise<{ hash: string; receipt: TransactionReceipt; userOpHash?: string }>;
readContract<T = unknown>(
chain: Chain, contractAddress: Address, functionSig: string,
args: unknown[], retryConfig?: RetryConfig,
): Promise<T>;
simulate( // throws on revert
chain: Chain, contractAddress: Address, functionSig: string,
args: unknown[], retryConfig?: RetryConfig,
): Promise<unknown>; // viem simulateContract result
getBalance(chain: Chain, retryConfig?: RetryConfig): Promise<string>; // decimal wei string
}
type TransactionConfirmation = {
confirmations?: number;
onReorg?: {
action:
| { type: "replay" }
| { type: "log"; logLevel?: "error" | "info" | "warn" } // default "error"
| { type: "task"; task: string };
depth: number;
};
};
TransactionReceipt carries status: "success" | "reverted", blockNumber: bigint, blockHash, gasUsed: bigint, effectiveGasPrice: bigint, cumulativeGasUsed: bigint, from, to, contractAddress: Address | null, logs: Log[], logsBloom, transactionHash, transactionIndex, type.
Specifically: there is no string | AbiItem overload; retryConfig is a separate 6th positional arg, not a key in an options bag; there are no gas/gasPrice options on writeContract; the return has no chainId and no top-level blockNumber (the TxResult type as documented does not exist); sendTransaction is object-first, the positional (chain, to, value, data?, options?) form does not exist; simulate returns { result, request } and throws on revert, so SimulateResult { success, ... } and if (!sim.success) are dead code; getBalance returns a decimal wei string, so arithmetic without BigInt(...) string-concatenates; onReorg.action.type is "replay" | "log" | "task", there is no "skip".
Collection
type CollectionIndexSpec = {
path: string;
type: "text" | "numeric" | "boolean" | "timestamptz";
unique?: boolean;
};
interface Collection<T> {
readonly name: string;
insertOne(doc: T, opts?: { id?: string }): Promise<{ id: string }>;
findOne(filter: Filter): Promise<(T & { id: string }) | null>;
findMany(filter: Filter, options?: { limit?: number; offset?: number }): Promise<Array<T & { id: string }>>;
getById(id: string): Promise<(T & { id: string }) | null>;
setById(id: string, doc: T, opts?: { upsert?: boolean }): Promise<{ id: string; upserted?: boolean; matched?: number }>; // upsert defaults true; false throws if absent
deleteById(id: string): Promise<{ deletedCount: number }>;
drop(): Promise<void>;
}
collection<T>(name, indexes?) takes CollectionIndexSpec[], not string[]. findMany options are { limit?, offset? }: skip does not exist and is silently ignored, so paging written against it always returns page 1. Reads return T & { id: string }. A filter is flat Record<string, string | number | boolean | HelperValue>, so nested-path filters are not supported.
Filter operators: $gt, $gte, $lt, $lte, $in, $ne, $nin, $exists. Equality: { field: value }.
CLI JSON Schemas
For agents parsing --json output:
compose status -n <app> --json
{
"name": "my-app",
"status": "RUNNING",
"created_at": 1771630350411,
"updated_at": 1774473580871
}
status is one of RUNNING, PAUSED, STARTING, STOPPING, ERROR, NOT_FOUND (the value comes from the API, so treat the list as non-exhaustive). Timestamps are ms epoch.
compose list --json
[
{ "name": "my-app", "status": "RUNNING", "created_at": 1771630350411, "updated_at": 1774473580871 }
]
compose logs --json
NDJSON (one object per line):
{"timestamp":"2026-04-20T10:00:00Z","level":"info","message":"..."}
Exactly three fields. There is no dashboard_url in CLI log output. To link a user to a specific run, get the run id from compose runs and build https://app.goldsky.com/<project_id>/dashboard/compose/<app-name>/runs/<run_id> yourself. The CLI's logs command also has no --run-id filter (the underlying API accepts one, the CLI does not pass it), so use compose runs <runId> for per-run detail.
compose secret list -n <app> --json
[{ "name": "MY_SECRET", "created_at": 1771630350411 }]
Values are never returned.
compose wallet list --json
[{ "name": "updater", "address": "0x...", "type": "privy", "created_at": 1771630350411 }]
type is one of privy (smart wallet), private_key (BYO EOA), tevm (local forked).
Errors in --json mode
In --json mode stdout carries only the result document (ascii art and progress bars are suppressed). Failures go to stderr as {"error": true, "code": "<CODE>", "message": "..."} with exit 1. Codes present in the CLI: VALIDATION_FAILED, SECRET_MISSING, DEPLOY_FAILED, DEPLOY_CONTRACT_FAILED, ALREADY_DEPLOYED, WRITE_CONTRACT_FAILED, WALLET_CREATE_FAILED, WALLET_LIST_FAILED, NOT_FOUND, TASK_NOT_FOUND, CONNECTION_REFUSED, INVALID_ENV, INVALID_FLAGS, INVALID_PAYLOAD, INVALID_RESPONSE, INVALID_FILTER, UNKNOWN.
Wallets — Deep Dive
Smart wallet (managed, Privy-backed)
const w = await evm.wallet({ name: "my-oracle" }); // sponsorGas defaults TRUE
Created cloud-side by Privy. Address is persisted. Gas-sponsored by default. Cannot be used in plain local dev - throws "You cannot use a named wallet without a private key in local dev. Start with "goldsky compose start --fork-chains" for full wallet support, or use a private key wallet: const wallet = await evm.wallet({ privateKey: MY_SECRET }); See https://docs.goldsky.com/compose/secrets for more info on private key wallets" Use compose start --fork-chains or switch to a BYO EOA for local iteration.
BYO EOA (private key)
const w = await evm.wallet({
privateKey: env.MY_KEY,
name: "my-pk-wallet", // optional; defaults to the derived address
sponsorGas: true, // DEFAULTS TO FALSE — opt in explicitly
});
Works in both cloud and local. When sponsorGas: true, the wallet configures EIP-7702 delegation per chain on first use, then submits UserOperations through a sponsored bundler.
Gas Sponsorship
Bundler fallback order: Alchemy → Pimlico → Gelato. Override via BUNDLER_PROVIDER=<alchemy|pimlico|gelato> env var.
Supported chains
A chain is runtime-sponsorable if any of the three bundler providers covers it (tried in fallback order Alchemy → Pimlico → Gelato, each gated on its API keys being set). In the 0.8.1 source that union spans 112 chains. This is the runtime task-gas sponsorship set - far broader than the deployContract / writeContract cloud deploy path, which covers the 10-chain Alchemy set (1, 11155111, 137, 80002, 42161, 421614, 10, 11155420, 8453, 84532; FOU-991 tracks broader coverage); runtime sponsorship also covers the Arbitrum, Optimism (incl. Arbitrum Sepolia (421614) / Optimism Sepolia (11155420)), Polygon, Ethereum and BNB families, among many others. Don't hardcode the list - it changes; confirm current coverage on the Goldsky docs chains page.
Error on unsupported chain
No bundler provider available for chain <id>. Providers: alchemy: chain not supported; pimlico: missing keys (PIMLICO_API_KEY); gelato: …
Either use a supported chain or set sponsorGas: false and fund the EOA manually.
Caveats
onReorgis not supported for gas-sponsored transactions (warning logged, not fatal).- Passing a custom
nonceto a sponsoredsendTransactionis ignored (ERC-4337 smart wallets use a different nonce structure).
Contract Codegen (full example)
Input
Drop ABI JSON files into src/contracts/:
src/contracts/
├── ERC20.json
└── PriceFeed.json
Accepted ABI shapes: bare ABI array ([{ "type": "function", ... }, ...]), or wrapped object ({ "abi": [...] }), or a Foundry/Hardhat artifact (the generator extracts the abi field). The filename (without extension) becomes the generated class name.
Generate
goldsky compose codegen
(Also runs automatically during init, start, and deploy.)
Output
.compose/generated/index.ts exports a class per ABI. .compose/types.d.ts declares ambient types under the compose path alias (referenced in the scaffolded tsconfig.json).
Consume in a task
import type { TaskContext } from "compose";
export async function main({ evm, env }: TaskContext) {
const wallet = await evm.wallet({ name: "oracle" });
const PriceFeed = evm.contracts.PriceFeed;
// Read — generated view methods call wallet.readContract under the hood
const feed = new PriceFeed(env.FEED_ADDRESS, evm.chains.ethereum, wallet);
const price = await feed.latestAnswer();
// Write — generated state-changing methods call wallet.writeContract under the hood
const tx = await feed.setPrice(1234n);
return { price: price.toString(), hash: tx.hash };
}
Classes are exposed under context.evm.contracts.<Name>. Codegen names ending in Class (e.g. ERC20Class) are exposed as ERC20 at runtime. The generated constructor is new <Name>(address, chain, wallet) — pass an IWallet from evm.wallet(...); view methods read through it, state-changing methods write through it, both subject to the wallet's gas-sponsorship setting.
Supported Chains
context.evm.chains is re-exported from viem/chains. Any chain viem knows, you can address as evm.chains.<name> (e.g. evm.chains.polygonAmoy, evm.chains.monadTestnet, evm.chains.baseSepolia). For gas sponsorship specifically, see the Gas Sponsorship section — sponsorship is a subset of viem's chain list.
Dashboard
URL pattern:
https://app.goldsky.com/<project_id>/dashboard/compose/<app-name>
https://app.goldsky.com/<project_id>/dashboard/compose/<app-name>/runs/<run_id>
The dashboard shows status, secret names, logs, a Code tab file browser, a Download app button, and per-run traces. Build run URLs from a run id returned by compose runs.
Pricing
Pricing is not published. Usage is metered on three dimensions: function calls (compose_function_calls), worker hours (compose_worker_hours), and gas spend (compose_gas_spend). Gas spent by writeContract and deployContract is billed the same as runtime task gas. Per-unit prices are set per contract, so point the user at https://goldsky.com/pricing rather than quoting a tier.
Related
/compose— Build a new app or explain what Compose is./compose-doctor— Diagnose and fix broken apps./auth-setup—goldsky loginhelp./secrets— General secret management.