# 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:publish-fund`, `solver:submit`,
or the `guardian:` set — and sign with your wallet or key.

```sh
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`](#posterpublish-fund)                       | Poster                            | Local key required          |
| [`poster:open-winning-submission`](#posteropen-winning-submission) | Poster                            | Local key required          |
| [`solver:submit`](#solversubmit)                                   | Solver                            | External wallet recommended |
| [`claim`](#claim)                                                  | Winning Solver or refunded Poster | External wallet supported   |
| [`spec-commitment`](#spec-commitment)                              | Anyone                            | None                        |
| [`verification-record`](#verification-record)                      | Anyone                            | None                        |
| [Guardian commands](#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:

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

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

```sh
elgora-cli solver:submit --api-base-url https://your-api.example 12 ./artifacts
```

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

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

***

## `poster:publish-fund`

```text
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](/docs/poster/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:**

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

***

## `poster:open-winning-submission`

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

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

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

***

## `claim`

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

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

```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. 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>` <br /> `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.
