Docs

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, through npx @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

CommandRoleWallet
poster:reviewPosterAny signer
poster:publish-fundPosterAny signer; --prepare-only prints the funding calls
poster:open-winning-submissionPosterAny signer
solver:checkSolverNone: no wallet, nothing signed or uploaded
solver:submitSolverAny signer; --prepare-only prints the submit call
claimWinning Solver or refunded PosterAny signer, or an address alone
spec-commitmentAnyoneNone
verification-recordAnyoneNone
Guardian commandsGuardianAny 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 shell

Names 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 ./solution

It 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_commitment

verification-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.

CommandPurpose
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:

--signerUseSettings
local (default)a private keyELGORA_PRIVATE_KEY in the environment
privya 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
manualyour 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:

OutcomeExitWhat to do
….recorded (publish_fund.bounty_created for funding)0Done: ElgoraHub shows the effect
An errornon-zeroElgoraHub does not show what the command asked for; fix the cause and run again
….verification_pending, with tx_hashes and reason75The 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.

Read this page as Markdown

On this page