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:review then poster:publish-fund, solver:submit,
or the guardian: set — and sign with your wallet: a key, a Privy server
wallet, or your own wallet tooling (Wallets and signers).
npm install --global @elgora/cli
elgora-cli --help
elgora-cli help <command>Or without installing: npx @elgora/cli --help. Node.js 24 or 26. Which
version a bounty needs follows the profile its challenge declares:
elgora_markdown_bounty_challenge_v1: the latest CLI, at least 12.0.0. This page describes it.elgora_markdown_bounty_challenge_v0: CLI 11, throughnpx @elgora/cli@11 <command>. CLI 12 refuses these bounties and names CLI 11.
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:review | Poster | Any signer |
poster:publish-fund | Poster | Any signer; --prepare-only prints the funding calls |
poster:open-winning-submission | Poster | Any signer |
solver:check | Solver | None: no wallet, nothing signed or uploaded |
solver:submit | Solver | Any signer; --prepare-only prints the submit call |
claim | Winning Solver or refunded Poster | Any signer, or an address alone |
spec-commitment | Anyone | None |
verification-record | Anyone | None |
| Guardian commands | Guardian | Any signer for the account; X25519 keys stay local |
Selecting a deployment
The default is Base mainnet, with real USDC. A run that
selects no network says so in one stderr line. The testnet 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 # testnet
elgora-cli claim --network 84532 7 # the same, by chain id
export ELGORA_CHAIN_ID=84532 # testnet for every run in this shellNames are base, base-sepolia, and local. The chain id resolves the Hub
address, the public RPC, the subgraph endpoint, and the API
(https://elgora.ai or https://testnet.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 ./solutionIt overrides the API the chain selected — point it at an API that serves that
chain. --hub-address, --rpc-url, and --subgraph-endpoint (or
ELGORA_HUB_ADDRESS, ELGORA_RPC_URL, ELGORA_SUBGRAPH_ENDPOINT) override
what the chain id resolved; a local anvil stack needs them, since its addresses
change every run. The escrow token is read from the Hub.
elgora-cli help <command> lists exactly which options that command takes.
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.
check-challenge
elgora-cli check-challenge <bounty_challenge_md_path|->Checks the challenge's structural conformance locally and prints the issues.
Use - to read Markdown from stdin. No wallet or network is needed. It exits
non-zero when issues are found. A passing result checks structure only; the
hosted readiness review still checks what the challenge means. Requires CLI 10.2
or later.
poster:review
elgora-cli poster:review ./bounty_challenge.md
elgora-cli poster:review --network base-sepolia ./bounty_challenge.md --previous 0x…Signs the exact Markdown and sends it for readiness review. Requires CLI 10 or later. Use - for
stdin. --previous must be a full bytes32 commitment; it is included before the
request is signed. The service checks previous issues from the same Poster and
site against the current page, then reports resolved and open issues. Fresh
requests reusing an approval return no prior-issue statuses.
Prints one JSON event: review.ready with spec_commitment, approved_until,
warnings, prior_issues_resolved and prior_issues_open, or review.not_ready with the
complete issues (path, message, and any code, anchor, hazard) and
prior_issues_resolved / prior_issues_open.
Each previous-issue item contains issue_index (its zero-based position in the
previous review) and the original message supplied by the service. The reviewer
assesses each complaint separately, even when complaints share a section or code.
These statuses are model judgments; read the original message before revising.
Ready exits 0, not-ready exits 2, and technical failures exit 1. Review does not fund or publish a bounty.
Review the approved text before publishing. Elgora keeps the approval of those exact bytes on the target site for seven days from the original review; changing the bytes needs a new review.
poster:publish-fund
elgora-cli poster:publish-fund [--prepare-only] <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, and funds. With a local key on an ordinary account it
signs a USDC authorization and sends createBountyWithAuthorization; with any
other signer — a Privy wallet, a smart or EIP-7702 account, your own wallet —
it sends approve for exactly the escrow amount, then createBounty. See
Publish and fund for signatures and gas.
Reads: the Poster's signer (Wallets and signers).
Accepts --rpc-url, --hub-address, --api-base-url; reads the escrow token
from ElgoraHub.
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 when funding goes through
approve. With --prepare-only the command publishes, then prints the funding
calls instead of sending them, for the Poster's wallet to send in order:
{"event":"publish_fund.calls_prepared","chain_id":8453,"from":"0x…","calls":[{"to":"0x…","value":"0","data":"0x…","function_name":"approve","args":{"spender":"0x…","amount":"20000000"}},{"to":"0x…","value":"0","data":"0x…","function_name":"create_bounty","args":{"spec_commitment":"0x…","escrow_amount":"20000000","submission_deadline":"1801699200"}}]}Publication reuses the saved approval of the exact bytes and requests no
further AI review. Bytes without a ready approval from the last seven days on
this site are reviewed first; a not-ready page is not published. That review
can take up to about three minutes and returns no warnings; run poster:review
first to see warnings.
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.
Full walkthrough: Publish and fund.
poster:open-winning-submission
elgora-cli poster:open-winning-submission <bounty_id> [output_dir]Retrieves the finalized winning Submission for the funding Poster, decrypts it
locally, and saves it as one .tar.gz in output_dir (default
winning-submission/). It finishes with
poster_open_winning_submission.completed, carrying the file it wrote.
Reads: the Poster's signer (Wallets and signers).
Accepts --rpc-url, --hub-address, --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 files.
The Submission check
solver:submit, solver:check and guardian:open run the same code on a
Submission. It checks:
- the archive: regular files only, every path under the path rule, no two paths equal ignoring case, no file where a folder is;
- the challenge's
constraints::max_files,max_extracted_bytes,required_files,allowed_extensions; - the total size of the Submission's files against the extracted budget. An archive the Solver included counts at its own size here; Guardians count what it unpacks to once they unpack it.
Each issue has an outcome. max_files and max_extracted_bytes disqualify
the Submission; required_files and allowed_extensions mean it is judged and
fails acceptance.
solver:check
elgora-cli solver:check <bounty_id> <folder_or_file>Builds the Submission as solver:submit would and runs
the Submission check on it, without submitting.
Purpose. Learn what a Guardian would refuse or disqualify while you can still fix it, as often as you like and at no cost.
Reads: nothing secret. Accepts --rpc-url, --hub-address and
--api-base-url.
Prints solver_check.passed, or solver_check.issues and exits 1. Each
issue carries an outcome — refused (solver:submit refuses the input
itself), disqualifies or fails_acceptance — with a code, the path concerned and a message:
{"event":"solver_check.issues","bounty_id":"12","checked":"format_and_constraints","profile":"elgora_markdown_bounty_challenge_v1","file_count":3,"extracted_bytes":2048,"extracted_budget_bytes":250000000,"encrypted_bytes":1210,"max_encrypted_bytes":52428800,"content_sha256":"…","issues":[{"outcome":"fails_acceptance","code":"required_files","path":"source/main.py","message":"…"}]}A pass covers form and constraints only. Whether the work meets the acceptance criteria is the Guardians' judgment.
solver:submit
elgora-cli solver:submit [--prepare-only] [--reveal-delay-seconds <n>] <bounty_id> <folder_or_file>Packs a folder — its contents and subfolders become the Submission — or a
single file into one .tar.gz, encrypts it for the bounty's pinned Guardian
roster, uploads only ciphertext, and records — or prepares — the exact
submit transaction. An archive of your own is packed as one file and never
opened. It runs the Submission check first and refuses
a Submission with any issue.
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: the Solver's signer (Wallets and signers).
Accepts --rpc-url, --hub-address, --api-base-url, and
--reveal-delay-seconds (how long after the deadline Guardian keys release;
0 by default).
With --prepare-only the command does everything but send: it finishes
with submit.calls_prepared holding the exact submit call for your wallet to
send.
Prints:
{"event":"submit.ciphertext_uploaded","bounty_id":"12","solver":"0x…","byte_length":184320,"content_sha256":"…"}
{"event":"submit.submission_prepared","mode":"drand_tlock","bounty_id":"12","solver":"0x…","submission_commitment":"0x…","guardian_roster_hash":"0x…","byte_length":…}
{"event":"submit.calls_prepared","chain_id":8453,"from":"0x…","calls":[{"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…","content_sha256":"…"}content_sha256 is a private digest of every file's path, size and SHA-256.
It is printed only on your machine, never sent to Elgora or put on chain.
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, when the served challenge does not hash to the bounty's on-chain
spec_commitment, or when the bounty is not open. The package rules are in
Build the package; the step-by-step is in
Submitting.
claim
elgora-cli claim [--prepare-only] <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: the claimant's signer (Wallets and signers).
Accepts --rpc-url, --hub-address. No API call and no signed request — this
is chain-only.
With --prepare-only it simulates the call and prints the exact
transaction for your wallet:
{"event":"claim.prepared","bounty_id":"12","claimant":"0x…","function_name":"claim","claimable_amount":"960000"}
{"event":"claim.calls_prepared","chain_id":8453,"from":"0x…","calls":[{"to":"0x…","value":"0","data":"0x…","function_name":"claim","args":{"bounty_id":"12"}}]}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. Accepts --hub-address, --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 that signs requests and Verdict transactions — a key, or any signer in Wallets and signers — and retained X25519 encryption keys that decrypt Submissions, never sign anything, and always stay local. 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, decrypts locally, and extracts its tree into a new or empty output_dir. Prints guardian_open.completed with content_sha256 and the issues of the Submission check. If the reader refuses the Submission archive itself, none of its files are written; issues the check reports come after the files are written. Archives inside the Submission are written as files |
guardian:verdict [--prepare-only] <bounty_id> awarded <solver> <submission_commitment> <verdict_body_path> guardian:verdict [--prepare-only] <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 — or prints the commitVerdict call with --prepare-only |
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 signers
Every command that signs uses one account, whichever role runs it. --signer
chooses how it signs:
--signer | Use | Settings |
|---|---|---|
local (default) | a private key | ELGORA_PRIVATE_KEY in the environment |
privy | a Privy server wallet, e.g. for an unattended Guardian | --privy-wallet-id <id>, optional --privy-sponsor; PRIVY_APP_ID, PRIVY_APP_SECRET, optional PRIVY_AUTHORIZATION_PRIVATE_KEY in the environment |
manual | your own wallet tooling, so your key never enters the CLI | --signer-address <address> |
The account can be a plain key, a smart wallet, or an EIP-7702 account: Elgora verifies ERC-1271 and ERC-6492 signatures on chain. Each request is signed just before it is sent and must reach Elgora within 30 minutes.
Signing outside the CLI
With --signer manual, the CLI writes each signature or transaction it needs to
a request file and prints signer.request_written on stdout with its
request_path, response_path, and expires_at. The request holds kind,
address, chain_id, and typed_data (EIP-712, eth_signTypedData_v4
shape), message (hex bytes for personal_sign), or calls. Sign or send it
with your own wallet and write the signature, or the transaction hashes
separated by spaces, to response_path. The CLI checks every signature on
chain before using it, and checks an answer again until expires_at if the
check fails, so a correct signature still counts after a brief RPC outage.
Write the hash of the mined transaction. The CLI checks only real
transactions, through their receipts and ElgoraHub: it never contacts a bundler
or paymaster, and a user operation hash or a Safe transaction id leaves the
command pending. If your smart wallet or Safe returns one of those first, wait
until it is mined and write the hash of the transaction it landed in. If your wallet refuses the second
of two calls right after the first, for example createBounty after approve,
its node has not seen the first one yet: wait a few seconds and send it again,
or send both in one batch.
Printed calls
Commands send their transactions last. With --prepare-only they print
<event>.calls_prepared instead: chain_id, from, and the calls to send
from that address, in order, or as one batch from a smart account.
Transaction outcomes
A command decides whether its transactions worked from ElgoraHub, never from a receipt alone — a smart account's calls ride in a bundler's transaction that succeeds even when they revert. It ends one of three ways:
| Outcome | Exit | What to do |
|---|---|---|
….recorded (publish_fund.bounty_created for funding) | 0 | Done: ElgoraHub shows the effect |
| An error | non-zero | ElgoraHub does not show what the command asked for; fix the cause and run again |
….verification_pending, with tx_hashes and reason | 75 | The calls may be on chain. Check ElgoraHub before running again — rerunning poster:publish-fund would fund a second bounty |
A pending outcome often means only that the RPC had not caught up with the
receipt's block. Wait a minute before checking ElgoraHub again. claim and
guardian:verdict are safe to run again: a claim that already landed is
refused, and an identical Verdict already on chain is a no-op.
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, from the environment it was started with, plus the
file DOTENV_CONFIG_PATH names when it is set.