diff --git a/content/build/run-a-gateway/manage/environment-variables.mdx b/content/build/run-a-gateway/manage/environment-variables.mdx
index 7d612e39c..4663b815c 100644
--- a/content/build/run-a-gateway/manage/environment-variables.mdx
+++ b/content/build/run-a-gateway/manage/environment-variables.mdx
@@ -179,7 +179,9 @@ The gateway talks to four Solana programs that together implement the ar.io prot
| -------------------------- | ------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `AR_IO_WALLET` | string | - | Operator Solana public key (base58). Display label surfaced on `/ar-io/info` |
| `SOLANA_RPC_URL` | string | `https://api.mainnet-beta.solana.com`| Solana JSON-RPC endpoint. Public defaults throttle hard — use a premium provider (QuickNode, Helius, Triton) in production |
-| `SOLANA_KEYPAIR_PATH` | string | - | Path to the operator's 64-byte Solana keypair JSON file (signs cranker instructions when `ENABLE_EPOCH_CRANKING=true`) |
+| `SOLANA_KEYPAIR_PATH` | string | - | Path to the operator's 64-byte Solana keypair JSON file. Signs `join_network`, `update_gateway_settings`, and cranker instructions. Inside the container the path must start with `/app/wallets/` |
+| `SOLANA_PRIVATE_KEY` | string | - | Alternative to `SOLANA_KEYPAIR_PATH`: base58-encoded 64-byte secret (Phantom export format). Mutually exclusive with the file form |
+| `ENABLE_EPOCH_CRANKING` | boolean | unset (= off) | When `true`, the observer runs permissionless epoch instructions (`close_observation`, `tick_epoch`, etc.). "When unset, observer skips cranking." Set `false` to make the off-state explicit |
| `ARIO_CORE_PROGRAM_ID` | string | - | `ario-core` program ID (token, staking, epoch state) |
| `ARIO_GAR_PROGRAM_ID` | string | - | `ario-gar` program ID (Gateway Registry; joins, observations, distributions) |
| `ARIO_ARNS_PROGRAM_ID` | string | - | `ario-arns` program ID (ArNS name registry) |
@@ -374,7 +376,22 @@ The default public Solana RPC is rate-limited and may block `getProgramAccounts`
| Variable | Type | Default | Description |
| ------------------------------ | ------- | ------- | ------------------------------------- |
-| `SUBMIT_CONTRACT_INTERACTIONS` | boolean | `true` | Submit observations to Solana programs |
+| `SUBMIT_CONTRACT_INTERACTIONS` | boolean | `true` | Submit observations to Solana programs. Pre-flight no-ops unless your pubkey is in `epoch.prescribed_observers` — harmless to leave at default before `join_network` |
+
+### Upload Wallet Identities
+
+The observer uploads report bundles to Turbo. The upload signer is resolved from the first matching env in the [precedence chain](/build/run-a-gateway/manage/solana-migration#upload-signing-precedence). Setting envs from more than one chain group at once is rejected at startup.
+
+| Variable | Type | Default | Description |
+| --------------------------------- | ------ | ------- | --------------------------------------------------------------------------- |
+| `ARWEAVE_UPLOAD_KEY_FILE` | string | - | Path to an Arweave JWK file. Highest priority for upload signing |
+| `ARWEAVE_UPLOAD_JWK` | string | - | Inline Arweave JWK JSON. Lower priority than the file form |
+| `ETHEREUM_UPLOAD_PRIVATE_KEY_FILE`| string | - | Path to a 32-byte hex private key (with or without `0x` prefix) |
+| `ETHEREUM_UPLOAD_PRIVATE_KEY` | string | - | Inline hex private key. Lower priority than the file form |
+| `SOLANA_UPLOAD_KEYPAIR_PATH` | string | - | Path to a separate Solana keypair JSON for uploads. Ignored when any `ARWEAVE_UPLOAD_*` or `ETHEREUM_UPLOAD_*` is set |
+| `SOLANA_UPLOAD_PRIVATE_KEY` | string | - | Alternative to above: base58 secret. Mutually exclusive with the file form |
+
+When none of the above are set, uploads fall back to the observer key, then the operator key.
### Offset Observation
diff --git a/content/build/run-a-gateway/manage/solana-migration.mdx b/content/build/run-a-gateway/manage/solana-migration.mdx
index 31cc8c022..672281458 100644
--- a/content/build/run-a-gateway/manage/solana-migration.mdx
+++ b/content/build/run-a-gateway/manage/solana-migration.mdx
@@ -135,6 +135,14 @@ Complete these steps before the cutover date to ensure uninterrupted reward elig
chmod 600 wallets/*.json
```
+
+ Env vars must use the **in-container** path (`/app/wallets/...`), not the host path (`./wallets/...`). Docker Compose bind-mounts `${WALLETS_PATH:-./wallets}` to `/app/wallets` inside the container.
+
+ | Wrong (host path) | Right (container path) |
+ |---|---|
+ | `SOLANA_KEYPAIR_PATH=./wallets/operator-keypair.json` | `SOLANA_KEYPAIR_PATH=/app/wallets/operator-keypair.json` |
+
+
Skip this step entirely if you set `OBSERVER_PRIVATE_KEY` / `SOLANA_PRIVATE_KEY` env vars (base58 strings) instead.
@@ -207,6 +215,85 @@ Complete these steps before the cutover date to ensure uninterrupted reward elig
+## Wallet Roles and Configuration Patterns
+
+The gateway uses up to four distinct wallet roles. Understanding these helps you pick the right configuration for your setup.
+
+| Role | What it signs | Env vars | Fallback |
+|---|---|---|---|
+| **Operator** (+ cranker) | `join_network`, `update_gateway_settings`, permissionless cranker instructions | `SOLANA_KEYPAIR_PATH` or `SOLANA_PRIVATE_KEY` | — (required) |
+| **Observer** | `save_observations` transactions | `OBSERVER_KEYPAIR_PATH` or `OBSERVER_PRIVATE_KEY` | Falls back to operator key |
+| **Upload** | Observer report bundles sent to Turbo | See [upload precedence](#upload-signing-precedence) below | Falls back to observer → operator Solana key |
+| **HTTPSIG signer** | RFC 9421 response headers | Uses observer Solana key when set | Auto-generated standalone Ed25519 key |
+
+
+Setting both the file-path and inline forms for the same role (e.g. `SOLANA_KEYPAIR_PATH` **and** `SOLANA_PRIVATE_KEY`) is rejected at startup as ambiguous. Pick one.
+
+
+### Supported Configurations
+
+These are the five supported wallet setups. **Pattern 1 is the recommended default** — one key does everything. Pattern 2 is the most common migration path for operators who already have an Arweave JWK.
+
+| # | Operator | Observer | Upload | Required envs |
+|---|---|---|---|---|
+| **1** | Solana | = operator | = operator (Solana) | `SOLANA_KEYPAIR_PATH` |
+| **2** | Solana | = operator | Arweave JWK | `SOLANA_KEYPAIR_PATH` + `ARWEAVE_UPLOAD_KEY_FILE` |
+| **3** | Solana A | Solana B | Solana C | `SOLANA_KEYPAIR_PATH` + `OBSERVER_KEYPAIR_PATH` + `SOLANA_UPLOAD_KEYPAIR_PATH` |
+| **4** | Solana A | Solana B | Arweave JWK | `SOLANA_KEYPAIR_PATH` + `OBSERVER_KEYPAIR_PATH` + `ARWEAVE_UPLOAD_KEY_FILE` |
+| **5** | Solana A | Solana B | Ethereum | `SOLANA_KEYPAIR_PATH` + `OBSERVER_KEYPAIR_PATH` + `ETHEREUM_UPLOAD_PRIVATE_KEY_FILE` |
+
+#### Pattern 1 — Single Solana keypair (recommended)
+
+```bash
+# One key for operator + observer + uploads
+SOLANA_KEYPAIR_PATH=/app/wallets/operator-keypair.json
+SOLANA_RPC_URL=
+AR_IO_WALLET=
+OBSERVER_WALLET=
+ENABLE_EPOCH_CRANKING=false # flip to true when ready
+```
+
+#### Pattern 2 — Keep existing Arweave JWK for uploads
+
+The most common path for operators migrating from a pre-Solana setup. Your existing Arweave JWK continues signing report bundles while the Solana keypair handles protocol interactions.
+
+```bash
+SOLANA_KEYPAIR_PATH=/app/wallets/operator-keypair.json
+ARWEAVE_UPLOAD_KEY_FILE=/app/wallets/.json
+SOLANA_RPC_URL=
+AR_IO_WALLET=
+OBSERVER_WALLET=
+ENABLE_EPOCH_CRANKING=false
+```
+
+### Upload Signing Precedence
+
+The gateway picks the first matching upload signer from this list:
+
+```
+1. ARWEAVE_UPLOAD_KEY_FILE (file) → ArweaveSigner
+2. ARWEAVE_UPLOAD_JWK (inline) → ArweaveSigner
+3. ETHEREUM_UPLOAD_PRIVATE_KEY_FILE (file) → EthereumSigner
+4. ETHEREUM_UPLOAD_PRIVATE_KEY (inline) → EthereumSigner
+5. SOLANA_UPLOAD_KEYPAIR_PATH (explicit) → SolanaSigner
+6. Fallback: OBSERVER_KEYPAIR_PATH ?? SOLANA_KEYPAIR_PATH → SolanaSigner
+```
+
+
+Setting upload envs from more than one chain at once (e.g. `ARWEAVE_UPLOAD_KEY_FILE` **plus** `ETHEREUM_UPLOAD_PRIVATE_KEY`) raises a startup error listing every conflicting env. Pick exactly one upload chain.
+
+
+### Key Formats
+
+Solana keypairs come in two common formats. Both encode the same 64-byte secret (`seed(32) || pubkey(32)`):
+
+| Format | Example | Source |
+|---|---|---|
+| **JSON array** (Solana CLI standard) | `[12,34,56,...]` — 64 uint8 integers | `solana-keygen new --outfile keypair.json` |
+| **base58 secret** | 87–88 character base58 string | Phantom "export private key", browser wallets |
+
+Use the JSON file with `*_KEYPAIR_PATH` env vars, or the base58 string with `*_PRIVATE_KEY` env vars — never both for the same role.
+
## Troubleshooting
### "Observer is restart-looping with 'Epoch 0 PDA not found'"
@@ -248,6 +335,19 @@ The gateway hydrates an ArNS names cache at boot, paginating through the on-chai
Fix: bump `@ar.io/sdk` in your gateway's image (`package.json`) to the latest `^4.0.0-solana.*` and rebuild.
+### Wallet Configuration Startup Errors
+
+The gateway validates wallet configuration at startup. Errors are loud and name the offending env:
+
+| Error pattern | Cause | Fix |
+|---|---|---|
+| `multiple chain groups configured for upload role` | Upload envs from more than one chain are set (e.g. `ARWEAVE_UPLOAD_*` and `ETHEREUM_UPLOAD_*`) | Pick one upload chain and remove the others |
+| `ambiguous: both ... set for role` | File-path and inline forms for the same role are both set (e.g. `SOLANA_KEYPAIR_PATH` + `SOLANA_PRIVATE_KEY`) | Use one form per role |
+| `material at SOLANA_KEYPAIR_PATH does not look like a Solana keypair` | An Arweave JWK or other JSON was placed at the Solana keypair path | Check you copied the right file — Solana keypairs are a JSON array of 64 integers, not a JWK object |
+| `material at ARWEAVE_UPLOAD_KEY_FILE does not look like an Arweave JWK` | A Solana keypair (JSON array) was placed at the Arweave upload slot | Swap the file for your Arweave JWK |
+| `SOLANA_KEYPAIR_PATH not set in Solana mode` | The operator key is missing entirely | Set `SOLANA_KEYPAIR_PATH` or `SOLANA_PRIVATE_KEY` |
+| `OBSERVER_KEYPAIR_PATH does not match on-chain Gateway.observer_address` | The observer key doesn't match what was registered at `join_network` | Update the key to match, or call `update_observer_address` on-chain |
+
## New Risks to Be Aware Of
### Gateway Pruning
diff --git a/content/build/run-a-gateway/manage/verification-headers.mdx b/content/build/run-a-gateway/manage/verification-headers.mdx
index 38f34cb1a..dab7c8ec1 100644
--- a/content/build/run-a-gateway/manage/verification-headers.mdx
+++ b/content/build/run-a-gateway/manage/verification-headers.mdx
@@ -113,36 +113,25 @@ Add the following to your gateway's `.env` file:
HTTPSIG_ENABLED=true
```
-On startup, the gateway automatically generates an Ed25519 key at `data/keys/httpsig.pem` if one doesn't exist. The key also functions as a valid Solana address.
+#### Signing Key Selection
-#### Optional: Attestation
+When `OBSERVER_KEYPAIR_PATH` or `OBSERVER_PRIVATE_KEY` is set, the gateway uses the observer's Solana keypair directly as the HTTPSIG signing key. Verifiers derive the Solana address from the public key in the `keyId` and look it up in the on-chain Gateway Registry — no separate attestation is needed.
-To link your signing key to your on-chain identity, configure an observer wallet. The gateway will create an RSA-signed attestation linking the Ed25519 key to your Arweave wallet and upload it to Arweave:
+When neither observer env is set, the gateway auto-generates a standalone Ed25519 key at `data/keys/httpsig.pem`. Responses are still signed, but the signer can't be tied back to the on-chain registry.
-```bash
-# Observer wallet for attestation (Arweave address)
-OBSERVER_WALLET=your-arweave-address
-WALLETS_PATH=wallets
-
-# Automatically upload attestation to Arweave (default: true)
-HTTPSIG_UPLOAD_ATTESTATION=true
-
-# Optional: include staked gateway address in attestation
-AR_IO_WALLET=your-gateway-address
-```
-
-Place your Arweave wallet JWK file in the `WALLETS_PATH` directory. The attestation is cached to `data/keys/httpsig-attestation.json` and only regenerated if the key or wallet changes.
+
+If you use a single Solana key for both operator and observer (Pattern 1 in the [migration guide](/build/run-a-gateway/manage/solana-migration#supported-configurations)), HTTPSIG falls back to the auto-generated key because the gateway only piggybacks on `OBSERVER_KEYPAIR_PATH`/`OBSERVER_PRIVATE_KEY` when they're **explicitly set**. To get on-chain-verifiable HTTPSIG with a single key, explicitly set `OBSERVER_KEYPAIR_PATH` (or `OBSERVER_PRIVATE_KEY`) to the same value as your operator key.
+
### Configuration Reference
| Variable | Default | Description |
|----------|---------|-------------|
| `HTTPSIG_ENABLED` | `false` | Enable RFC 9421 response signing |
-| `HTTPSIG_KEY_FILE` | `data/keys/httpsig.pem` | Path to Ed25519 private key (auto-generated if missing) |
+| `HTTPSIG_KEY_FILE` | `data/keys/httpsig.pem` | Path to standalone Ed25519 private key (auto-generated if missing). Ignored when `OBSERVER_KEYPAIR_PATH` or `OBSERVER_PRIVATE_KEY` is set |
| `HTTPSIG_BIND_REQUEST` | `true` | Include request method and path in signature (prevents replay) |
-| `OBSERVER_WALLET` | - | Arweave wallet address for attestation signing |
-| `WALLETS_PATH` | `wallets` | Directory containing Arweave wallet JWK files |
-| `HTTPSIG_UPLOAD_ATTESTATION` | `true` | Upload attestation to Arweave automatically |
+| `OBSERVER_KEYPAIR_PATH` | - | Path to a 64-byte Solana keypair JSON. When set, used as the HTTPSIG signing key — verifiable against the on-chain GAR |
+| `OBSERVER_PRIVATE_KEY` | - | Alternative: base58-encoded 64-byte secret. Mutually exclusive with the file form |
### Verifying It's Working
@@ -173,9 +162,9 @@ Only responses containing at least one trust-relevant header are signed. Non-dat
Clients can verify signed responses through the following chain:
-1. **Signature verification** - The public key is embedded in the `Signature-Input` header's `keyid` parameter and is verifiable via the Web Crypto API in modern browsers.
-2. **Identity verification** - If attestation is configured, follow the attestation chain from the Ed25519 signing key to the operator's Arweave RSA wallet to confirm the signer is a registered gateway. The attestation is available at `/ar-io/info` and on Arweave.
-3. **Body integrity** - Compare the `Content-Digest` header against a locally computed hash, or walk the Arweave signature chain from the signed `X-AR-IO-Data-Id`.
+1. **Signature verification** — The public key is embedded in the `Signature-Input` header's `keyid` parameter and is verifiable via the Web Crypto API in modern browsers.
+2. **Identity verification** — When the observer Solana key is used for signing, derive the Solana address from the public key and look it up in the on-chain Gateway Registry (GAR). A match confirms the signer is a registered gateway operator.
+3. **Body integrity** — Compare the `Content-Digest` header against a locally computed hash, or walk the Arweave signature chain from the signed `X-AR-IO-Data-Id`.
## Related