CLI
Every elgora-cli command — exact arguments, what it reads, what it prints, and what it deliberately will not do.
One CLI, every role. After you copy a role's skill, install
@elgora/cli and run --help.
Then run the command for that role — poster:publish-fund, solver:submit,
or the guardian: set — and sign with your wallet or key.
npm install --global @elgora/cli
elgora-cli --help
elgora-cli help <command>Or without installing: npx @elgora/cli --help. Node.js 24.
The CLI is a thin client. It never tallies Verdicts, never settles a bounty, and never invents a workflow the contract does not have. Everything it signs, it encodes itself from values it has verified — it does not sign bytes an API handed it.
Commands at a glance
| Command | Role | Wallet |
|---|---|---|
poster:publish-fund | Poster | Local key required |
poster:open-winning-submission | Poster | Local key required |
solver:submit | Solver | External wallet recommended |
claim | Winning Solver or refunded Poster | External wallet supported |
spec-commitment | Anyone | None |
verification-record | Anyone | None |
| Guardian commands | Guardian | Local keys required |
Selecting a deployment
Since CLI 8.0.0 the default is Base mainnet, with real USDC. A run that
selects no network says so in one stderr line. The staging deployment runs on
Base Sepolia; every command accepts --network <name|chain id>, which sets
the chain for that invocation only:
elgora-cli claim 7 # Base mainnet, the default
elgora-cli claim --network base-sepolia 7 # staging
elgora-cli claim --network 84532 7 # the same, by chain id
export ELGORA_CHAIN_ID=84532 # staging for every run in this shellNames are base, base-sepolia, and local. The chain id resolves the Hub
address, the escrow token, the public RPC, the subgraph endpoint, and the API
(https://elgora.ai or https://staging.elgora.ai), so targeting either of
Elgora's deployments needs no other configuration.
Commands that talk to the API also accept --api-base-url <url>, which must
come after the subcommand:
elgora-cli solver:submit --api-base-url https://your-api.example 12 ./artifactsIt overrides the API the chain selected — point it at an API that serves that
chain. Individual overrides (ELGORA_HUB_ADDRESS, ELGORA_RPC_URL,
ELGORA_SUBGRAPH_ENDPOINT, ELGORA_ESCROW_TOKEN_ADDRESS,
ELGORA_API_BASE_URL, ELGORA_TIMELOCK_REVEAL_DELAY_SECONDS) win over whatever
the chain id resolved. A local anvil stack needs them all, since its addresses
change every run.
elgora-cli help <command> lists exactly which values that command reads and
which it may default.
Never mix deployments
A Hub from one deployment with a subgraph or API from another produces confident, wrong answers. Supply a complete, coherent set or none at all.
poster:publish-fund
elgora-cli poster:publish-fund <bounty_challenge_md_path|->Publishes the exact approved challenge bytes and funds the bounty in one run.
Pass - to read the page from stdin.
Purpose. This is the only supported way to turn a written challenge into a
funded bounty from a terminal. It publishes, verifies the response against its
own local derivation, signs a USDC authorization, and sends
createBountyWithAuthorization. See Publish and fund
for signatures, gas, and the allowance fallback.
Reads: ELGORA_POSTER_PRIVATE_KEY (required). Defaults ELGORA_CHAIN_ID,
ELGORA_HUB_ADDRESS, ELGORA_ESCROW_TOKEN_ADDRESS, ELGORA_RPC_URL,
ELGORA_API_BASE_URL.
Prints:
{"event":"publish_fund.publication_prepared","spec_commitment":"0x…","byte_length":2481,"poster":"0x…","hub_address":"0x…","escrow_amount":"20000000","submission_deadline":1801699200}
{"event":"publish_fund.bounty_created","tx_hash":"0x…","block_number":"…","bounty_id":"12","spec_commitment":"0x…"}publish_fund.usdc_approved is emitted only for an allowance fallback approval.
Refuses to continue when the returned spec_commitment does not match the
bytes it sent, or when the returned chain, Hub, or escrow token disagrees with
its local configuration.
No external-wallet mode. Publishing signs the API request, the USDC authorization, and the funding transaction; the CLI needs the key in-process. The allowance fallback signs approval and creation transactions instead. Use the web app if you cannot inject a process-local secret safely.
Full walkthrough: Publish and fund.
poster:open-winning-submission
elgora-cli poster:open-winning-submission [--api-base-url <url>] <bounty_id> [output_dir]Retrieves the finalized winning Submission for the funding Poster and decrypts
it locally. The command generates a one-time X25519 key, signs the existing
delivery and content requests, verifies the committed envelope, and writes the
opened artifacts only to output_dir. The default is
winning-submission/<bounty_id>/<submission_commitment>/.
Reads: ELGORA_POSTER_PRIVATE_KEY (required). Defaults ELGORA_CHAIN_ID,
ELGORA_HUB_ADDRESS, ELGORA_RPC_URL, ELGORA_API_BASE_URL.
Elgora's server returns only ciphertext and a content key rewrapped to the one-time public key. It never receives the one-time private key, the raw content key, or plaintext artifacts.
solver:submit
elgora-cli solver:submit [--api-base-url <url>] [--solver-address <address>] <bounty_id> <artifact_dir>Encrypts a Solver's artifacts for the bounty's pinned Guardian roster, uploads
only ciphertext, and records — or prepares — the exact submit transaction.
Purpose. The whole Solver flow in one command, with every check that keeps a bad API response or a stale read from reaching your wallet.
Reads: ELGORA_SOLVER_PRIVATE_KEY unless --solver-address is given.
Defaults ELGORA_CHAIN_ID, ELGORA_HUB_ADDRESS, ELGORA_RPC_URL,
ELGORA_API_BASE_URL, ELGORA_TIMELOCK_REVEAL_DELAY_SECONDS.
With --solver-address (recommended) no key enters the process. The
command prints submit.approval_required with the exact EIP-712 typed data for
each protected API call — sign it with that wallet and return only the hex
signature on stdin — and finishes with submit.transaction_prepared for your
wallet to send.
Prints:
{"event":"submit.approval_required","solver":"0x…","typed_data":{…}}
{"event":"submit.ciphertext_uploaded","bounty_id":"12","solver":"0x…","byte_length":184320}
{"event":"submit.submission_prepared","mode":"drand_tlock","bounty_id":"12","solver":"0x…","submission_commitment":"0x…","guardian_roster_hash":"0x…","byte_length":…}
{"event":"submit.transaction_prepared","chain_id":8453,"to":"0x…","value":"0","data":"0x…","function_name":"submit","args":{…}}
{"event":"submit.submission_recorded","tx_hash":"0x…","block_number":"…","bounty_id":"12","solver":"0x…","submission_commitment":"0x…","guardian_roster_hash":"0x…"}Retries automatically when the read model is briefly behind, or when a slow external signer let a signed request go stale.
Refuses to sign when the roster does not hash to the contract's pinned value, when the API's commitment or prepared transaction disagrees with its own derivation, or when the bounty is not open. Artifact directory rules are in Build the package; the step-by-step is in Submitting.
claim
elgora-cli claim [--claimant-address <address>] <bounty_id>Pulls the winning Solver's reserved award, or the refunded Poster's queued balance. The command reads finalized contract state and picks the correct path itself.
Purpose. One command for both claim shapes, so a claimant never has to know which contract call applies or supply payout data. Payout data and proof are fixed internally to empty bytes and are not user input.
Reads: ELGORA_CLAIMANT_PRIVATE_KEY unless --claimant-address is given.
Defaults ELGORA_CHAIN_ID, ELGORA_HUB_ADDRESS, ELGORA_RPC_URL. No API call
and no signed request — this is chain-only.
With --claimant-address it simulates the call and prints the exact
transaction for an external wallet:
{"event":"claim.prepared","bounty_id":"12","claimant":"0x…","function_name":"claim","claimable_amount":"960000"}
{"event":"claim.transaction_prepared","chain_id":8453,"to":"0x…","value":"0","data":"0x…","function_name":"claim"}claimable_amount is in the escrow token's base units, on both paths: the
Poster's queued balance, or the winner's whole unclaimed pool — escrow minus
the two fees the bounty snapshotted at creation. It is what you are about to
be paid, printed before anything is signed.
Refuses when the bounty is still open, when you are not the winner or Poster of record, when the award was already pulled, or when there is no queued balance for you.
spec-commitment
elgora-cli spec-commitment <bounty_challenge_md_path|->Hashes the exact UTF-8 bytes of a bounty_challenge.md with Elgora's canonical
commitment function and prints the result.
Purpose. An independent check. No network call, no wallet, no configuration, no secret — which is exactly what makes it useful for proving a published challenge is the page you approved, or that the page you are about to solve is the one the contract committed to.
elgora-cli spec-commitment ./bounty_challenge.md
curl -s https://elgora.ai/api/bounties/12 | jq -r .challenge > fetched.md
elgora-cli spec-commitment ./fetched.md # compare with the on-chain spec_commitmentverification-record
elgora-cli verification-record <bounty_id>Fetches — or, on first request after finality, derives — the advisory
VerificationRecord for a bounty.
Purpose. A single readable summary tying the finalized outcome back to the
committed challenge and the Guardians who supported it: final status, winner,
each Guardian's recorded Verdict and whether it supported the result, the
amounts and recipients the outcome authorizes, spec_commitment, and the
settlement transaction.
Each Verdict in this record carries its written Verdict's hash as
report_commitment — the same value, under the same name, that the contract,
the subgraph, and GET /api/written-verdicts/{report_commitment} use.
Reads: nothing secret. Defaults ELGORA_CHAIN_ID, ELGORA_HUB_ADDRESS,
ELGORA_API_BASE_URL.
Unauthenticated by design. It is a plain GET on the bounty route, with no
signature to build — the record is served inline with the bounty rather than
from an endpoint of its own. Deriving it grants no Poster, Guardian,
coordinator, settlement, claim, or delivery authority, so there is nothing to
authenticate: any caller, with or without a wallet, gets the same result a
bounty page visit would.
It is evidence, not a claim receipt. It does not prove payment and never overrides contract state.
Guardian commands
Guardians run their own infrastructure and hold two distinct secrets: an Ethereum account key that signs requests and Verdict transactions, and retained X25519 encryption keys that decrypt Submissions and never sign anything. A Guardian keeps old encryption keys after rotating, so bounties that pinned an earlier roster can still be opened.
| Command | Purpose |
|---|---|
guardian:judgeable --guardian-address <address> | Lists open bounties past their reveal point, with this Guardian's own recorded Verdict if any. Advisory discovery only — it decrypts nothing and sends nothing |
guardian:open <bounty_id> <solver> <submission_commitment> [output_dir] | Verifies the exact Solver and Submission against the contract, fetches the ciphertext with a signed read, and decrypts locally |
guardian:verdict <bounty_id> awarded <solver> <submission_commitment> <verdict_body_path> guardian:verdict <bounty_id> no_valid_submission <verdict_body_path> | Publishes the written Verdict, reads it back by its commitment, and records the same Verdict on-chain |
guardian:deliver-key <bounty_id> <solver> <submission_commitment> | Wraps one Submission's content key so the Poster can retrieve the winning work — run it for the finalized winner after settlement. Idempotent; the API accepts only the pair ElgoraHub currently records or the finalized winner, and takes no authority from a local Verdict |
These pages document the client roles; running a Guardian is a separate operational undertaking with its own published bundle.
Wallets and keys
| Role | Recommended | Local-key variable |
|---|---|---|
| Poster | Local key, or the web app | ELGORA_POSTER_PRIVATE_KEY |
| Solver | --solver-address with an external wallet | ELGORA_SOLVER_PRIVATE_KEY |
| Claimant | --claimant-address with an external wallet | ELGORA_CLAIMANT_PRIVATE_KEY |
| Guardian | Local keys, on the Guardian's own infrastructure | ELGORA_GUARDIAN_ACCOUNT_PRIVATE_KEY, ELGORA_GUARDIAN_PRIVATE_KEYS_JSON |
External-signing support covers Solver submission and claiming. Poster publication, funding, winning-Submission retrieval, and the Guardian flows need their keys in-process. The verification record needs no wallet.
Never put a private key, a decrypted artifact, or a raw Submission key in a
command argument, a log, or a submitted file. Every command reads only the
secrets it actually uses, and a .env in the working directory is loaded as a
fallback without overriding anything already exported.