# 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`](https://www.npmjs.com/package/@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](#wallets-and-signers)).

```sh
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

| Command                                                            | Role                              | Wallet                                                |
| ------------------------------------------------------------------ | --------------------------------- | ----------------------------------------------------- |
| [`poster:review`](#posterreview)                                   | Poster                            | Any signer                                            |
| [`poster:publish-fund`](#posterpublish-fund)                       | Poster                            | Any signer; `--prepare-only` prints the funding calls |
| [`poster:open-winning-submission`](#posteropen-winning-submission) | Poster                            | Any signer                                            |
| [`solver:check`](#solvercheck)                                     | Solver                            | None: no wallet, nothing signed or uploaded           |
| [`solver:submit`](#solversubmit)                                   | Solver                            | Any signer; `--prepare-only` prints the `submit` call |
| [`claim`](#claim)                                                  | Winning Solver or refunded Poster | Any signer, or an address alone                       |
| [`spec-commitment`](#spec-commitment)                              | Anyone                            | None                                                  |
| [`verification-record`](#verification-record)                      | Anyone                            | None                                                  |
| [Guardian commands](#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:

```sh
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:

```sh
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.

<Callout type="warn" title="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.
</Callout>

***

## `check-challenge`

```text
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`

```sh
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`

```text
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](/docs/poster/publish-and-fund) for signatures and gas.

**Reads:** the Poster's signer ([Wallets and signers](#wallets-and-signers)).
Accepts `--rpc-url`, `--hub-address`, `--api-base-url`; reads the escrow token
from ElgoraHub.

**Prints:**

```json
{"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:

```json
{"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](/docs/poster/publish-and-fund).

***

## `poster:open-winning-submission`

```text
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](#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`

```text
elgora-cli solver:check <bounty_id> <folder_or_file>
```

Builds the Submission as `solver:submit` would and runs
[the Submission check](#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:

```json
{"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`

```text
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](#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](#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:**

```json
{"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](/docs/solver/package); the step-by-step is in
[Submitting](/docs/solver/submitting).

***

## `claim`

```text
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](#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:

```json
{"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`

```text
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.

```sh
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`

```text
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](#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](#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>` <br /> `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](https://www.privy.io/agent-wallets) 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.
