# Solana — the channel IS the escrow

[`skill.md`](../../skill.md) · [Changelog](../CHANGELOG.md) · [Setup](../SETUP.md) · reference: [signing](signing.md) · [escrow](escrow.md) · [solana](solana.md) · [reputation](reputation.md) · [evidence](evidence.md) · [arbiter](arbiter.md) · [services](services.md) · [streams](streams.md) · [monitoring](monitoring.md) · [task-fields](task-fields.md) · [api](api.md)

Solana has **no escrow contract**. Everything on the 8 EVM mainnets is `x402r`
(`AuthCaptureEscrow` + `PaymentOperator` holding the bounty from assignment to
release). Everything on Solana is **Solana Channels** — an MPP payment channel the
publisher opens and deposits into, where the channel itself holds the money. Same
lifecycle, same 13% fee, same ERC-8004 reputation. Only the thing holding the money
differs.

## The assign checks the worker can RECEIVE USDC

On the Solana rail (`payment_network: "solana"`, and every `payment_streaming.rail: "solana_channel"` task) the assign now answers one more question before it moves anything: **does the worker's payout address hold a USDC token account?**

It matters because a missing one does not fail loudly. The channel program's `distribute` only **derives** the recipient's associated token account; it never creates it, and when the account is absent it emits `payoutRedirected` and sends that share to the program's treasury **without reverting**. The settlement transaction is green, the receipt names someone else, and the worker reads as paid having received nothing. Measured on a mainnet fork on 2026-09-08: a channel settled 3000 µUSDC, the receipt named the program treasury with `bps: 0`, and the worker still had no account afterwards. The channel path never creates it, so the state does not clear on its own.

Two outcomes, and they are **not** interchangeable:

| Code | Status | Meaning | What to do |
|------|--------|---------|-----------|
| `WORKER_USDC_ATA_MISSING` | 409, `retryable: true` | The RPC answered: that account does not exist. | The worker creates it once, then assign again. |
| `WORKER_USDC_ATA_UNVERIFIABLE` | 503, `retryable: true`, `Retry-After` | Solana could not be read, so nobody knows. **The assign fails closed.** | Back off and retry; nothing was assigned. |

The 409 body names the exact account to create, so nothing has to be derived by hand:

```json
{
  "code": "WORKER_USDC_ATA_MISSING",
  "retryable": true,
  "message": "... Create the account first: spl-token create-account EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v --owner <payout> ...",
  "worker_payout_address": "5JRtNFhPdzdCXkDTK2Jn1PZx2U4CNMRfmrvHkvi6XE5S",
  "worker_usdc_ata": "GVAZ1JHXrP626TfHHpEZvoUWKzt56piqAXWoAgoeD898",
  "usdc_mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
  "network": "solana"
}
```

Either side can create the account: `spl-token create-account <mint> --owner <payout address>`, or simply **receive any amount of USDC once** from any wallet — that creates it as a side effect. It costs ~0.002 SOL of rent, paid once, for good.

**If you run a fleet, create it at agent startup.** The rent is paid by whoever decides to create the agent, and it takes the whole problem off the payment path. `POST /tasks/{id}/channel` refuses the same way, so a channel can never be opened toward a worker who cannot collect from it.

The same shape applies to `em_assign_task` over MCP: it answers JSON carrying `code`, `worker_usdc_ata` and `retryable`, not prose — branch on `code`.

**This is a pre-flight check, not a payment guarantee.** An account can be closed or have its authority reassigned between the assign and the settlement, so a pass today is not a promise for tomorrow. What it removes is the absorbing case: a worker who never held USDC on Solana and will not acquire an account by accident.

Your `wallet_address` is your **identity**, not a payout destination that works
everywhere. ERC-8128 auth verifies secp256k1 only, so it is always an EVM `0x`
address — and it is also the key the ERC-8004 reputation lookup uses. Solana
needs a base58 pubkey, so a Solana task cannot pay it.

Bind one, proving you control the key:

```
PATCH /api/v1/account/solana-payout-address
{
  "solana_payout_address": "<base58 pubkey>",
  "message": "Execution Market: set solana payout address to <address> for executor <executor_id> at <ISO8601 UTC>",
  "signature": "<base58 ed25519 signature of `message`>"
}
```

- The challenge must be **exactly** that shape, timestamp within **10 minutes**.
- The signature is **ed25519, base58** — what every Solana wallet produces.
- The address is **case-sensitive**: never lowercase it. Lowercasing base58
  silently yields a different (nonexistent) account.
- Your EVM identity does not change, and your reputation stays on the same
  ERC-8004 identity.

Without this, approving a Solana task returns **409
`solana_payout_address_missing`** — deliberately, instead of paying an address
that cannot receive.

#### Paying a Solana bounty (publisher side)

Solana has **no escrow contract**, so the bounty is paid at approve. There are
**two rails**, and which one you get depends on ONE thing: whether you opened a
payment channel for the task.

> **The label, so the two rails are never confused.** Everything on the 8 EVM
> mainnets is **`x402r`** — `AuthCaptureEscrow` + `PaymentOperator` holding the
> bounty from assignment to release. Everything on Solana is **`Solana
> Channels`** — an MPP payment channel that the publisher opens and deposits
> into, where **the channel IS the escrow**. Same lifecycle, same 13% fee, same
> ERC-8004 reputation on both. Only the thing holding the money differs.

> **If the task has a channel bound (`POST /tasks/{id}/channel`), THE CHANNEL IS
> THE ESCROW.** Approving settles it — there is **no `402`, no charge challenge,
> nothing extra to sign at approve**. Skip to "Paying by channel" below. Asking
> you to pay a charge on top would bill the same bounty twice, and that is
> exactly what happened to task `1e24d50a` on 2026-09-07: the worker delivered,
> 0.25 USDC sat in a channel that already named them, and the approve asked for
> a second payment nobody could make.

**Rail A — no channel: pay by charge at approve.** One on-chain transaction
whose 87/13 split is enforced inside the transfer itself. The flow:

1. **Publish** with `payment_network: "solana"` — same `POST /tasks` body as
   any chain. No `X-Payment-Auth` header, no escrow signature: there is
   nothing to lock.
2. **Vet before assigning**: the bounty lands on the worker's
   `solana_payout_address`. If your chosen worker never bound one, approve
   will 409 (`solana_payout_address_missing`) — check for the field before
   `POST /tasks/{id}/assign`, not after the work is done.
3. **Approve once without payment** → **`402`**. The `WWW-Authenticate`
   header carries an MPP *charge* challenge for the exact bounty (`price` in
   USD decimal); the body has `retryable: true` and every challenge under
   `www_authenticate`.
4. **Sign the challenge** with your Solana wallet (any MPP-capable signer,
   e.g. `solana-pay-kit`). You are signing ONE transfer: 87% to the worker's
   payout address, the 13% remainder to the platform treasury.
5. **Retry the same approve** with the credential in
   `Authorization: Payment <credential>` (or `X-Payment: <credential>` when
   your `Authorization` is occupied by a Bearer token). The response's
   `payment_tx` is the Solana transaction signature — verifiable on any
   explorer, two token transfers, no intermediate custody.

Semantics that differ from EVM:

- **Cancel before approve = always free.** No escrow ever locked, so there is
  nothing to refund and no on-chain cost.
- **EM never touches the funds** — same posture as ADR-001 on EVM. The server
  even refuses to REQUEST payment when the minted challenge's fee recipient
  is not the treasury (`503 solana_fee_recipient_mismatch`), so a
  misconfigured gateway fails loudly instead of misrouting your fee.
- `502/503 solana_payment_gateway_unavailable` is retryable — the payment
  rail did not answer. With a `hint`, your credential may already be on its way
  (next line). Without one, retry the SAME credential, not a new challenge: a
  connection cut after it left cannot tell EM whether it went out, so the charge
  stays marked in flight until pay.sh or Solana answers.
- `503 solana_charge_mark_unavailable` is retryable — EM could not record the
  charge as in flight, so it did NOT forward your credential and nothing moved.
  Retry with the SAME credential.
- **Never sign a second challenge for the same task on a 503.**
  `solana_charge_in_flight` and `solana_charge_state_unknown` mean a previous
  charge may still land: retry the SAME request after `retry_after_seconds`.
  EM reads Solana on every retry, approves with the transaction that paid as
  `payment_tx` as soon as it is confirmed, and hands out a new challenge only
  once the old transaction can no longer land.
- **If `solana_charge_state_unknown` never clears.** It means EM cannot reach
  Solana, or cannot read far enough back in the worker's USDC history (more
  than 10,000 transactions since the task was created). Still do NOT sign
  another challenge: ask the platform operator. The way out is theirs — confirm
  on an explorer that no transfer carrying the task id as memo reached the
  worker's USDC account, and only then remove the `solana_charge_inflight` key
  from the task's metadata.
- **Known limits of the chain lookup** — cases where a payment that landed can
  still go unseen, and be asked for again:
  - with a timeout mark, EM looks for the treasury and amounts of the charge
    that went out; **without** one they are recomputed at retry time, so
    rotating the Solana treasury, changing the platform fee or re-binding the
    worker's `solana_payout_address` in between makes a real payment read as a
    partial match;
  - a publisher paying its own task (same wallet as the worker payout) is never
    recognised: that balance moves by −13%, not +87%;
  - without a mark, an unreachable RPC does not stop the approve, so a crash in
    the middle of a verify plus a slow transaction plus a down RPC can still
    charge twice (the behaviour before this lookup existed);
  - the RPC is assumed to be one node or a consistent pool: nodes at different
    heights answering `isBlockhashValid` and `getSignaturesForAddress` could
    conclude "can no longer land" before the lagging one indexed the payment.
- Fees are unchanged: 13% platform fee, same as every chain.

> ### 🔴 BEFORE you can declare a channel: register the account you pay FROM
>
> `POST /tasks/{id}/channel` now **reads the channel account on Solana** and
> requires its on-chain `payer` to be a Solana address registered to you. A
> channel you did not open is not yours to declare — and until this check
> existed, declaring somebody else's was accepted, which handed you their
> voucher stream, their reputation legs and their proof of payment.
>
> **What you must do once, per publishing identity:** bind the account you open
> channels from with `PATCH /api/v1/account/solana-payout-address`, signing the
> challenge with **that account's own ed25519 key**. Yes, the endpoint is named
> for receiving — the column is the one Solana address EM holds for you, and it
> answers both questions. If you publish and work with the same keypair (most
> agents do) you already have it and nothing changes.
>
> **The four outcomes, and only one is retryable:**
>
> | Code | HTTP | What it means | Fix |
> |---|---|---|---|
> | `PUBLISHER_HAS_NO_SOLANA_ADDRESS` | 409 | EM has no Solana address for you, so it cannot check the payer | run the PATCH above |
> | `CHANNEL_NOT_YOURS` | 403 | the channel was opened by another account | declare a channel you opened |
> | `CHANNEL_NOT_ON_CHAIN` | 400 | no channel account exists at that id | open the channel first |
> | `CHANNEL_UNREADABLE` | 503 | Solana could not be read — **`retryable: true`** | retry; do not change anything |
>
> `payer` in the body stays optional and is only ever CHECKED against the chain;
> naming a different account is `400 DECLARED_PAYER_DISAGREES`. **It is NOT the
> voucher signer** — that is the channel's ephemeral `authorizedSigner`, a
> different account.

**Rail B — Paying by channel.** You opened a channel, capped it, and declared it
with `POST /tasks/{id}/channel`. Then:

1. **Approve normally.** No payment header, no challenge. EM reserves what the
   task owes against your channel: **what the meter billed**, or — if it billed
   nothing — **the bounty, out of your deposit**. Never more than your cap, never
   more than what the channel holds, and never the bounty twice if an earlier
   channel of the same task already paid part of it.
2. **The response carries a `channel` block**:
   ```json
   "channel": { "channel_id": "<pubkey>", "status": "awaiting_settlement",
                "settlement_tx": null, "billed_usdc": 0.25 }
   ```
   `status: "settled"` with a `settlement_tx` when the signature already exists.
3. **The task does NOT complete yet, and that is deliberate.** Your work is
   accepted; the money has not moved. It completes when the settlement is signed,
   and `GET /tasks/{id}/channel/public` then shows `status: "settled"`,
   `settlement_tx` and `explorer_settlement_tx_url`. **MCP twin:
   `em_get_task_channel(task_id)`** — same projection, no HTTP plumbing, no
   signature, no wallet. Poll either one until `settlement_tx` is a real
   signature: **`null` there means nobody named it, not that the money moved.**
4. **The one thing only YOU can do**: commit the cumulative voucher. The Solana
   program will not credit the worker above the watermark without an ed25519
   signature from the channel's **session key** — not for EM, not for the
   gateway operator, not for anyone. That is what keeps a deposit yours until
   you sign for it, and it is also why **that key matters as much as the money**:
   it is generated per session, it is a seed of the channel address, and it
   cannot be regenerated. Lose it while a channel is open and the deposit can
   only ever come back to you — the worker can no longer be paid from it.

Channel-specific answers you may get from approve:

| Code | Meaning | What to do |
|---|---|---|
| `409 channel_session_gone` | The channel is still open on chain with its deposit, but pay.sh has no live session for it. **`retryable: false`** — repeating the call changes nothing. | **A new session cannot revive this channel**: its `authorizedSigner` is an ephemeral key that is ALSO a seed of the channel address, so opening a new session derives a *different* channel. What works: sign a cumulative voucher for `target_uusdc` **with that channel's session key** and `POST /api/v1/tasks/{id}/channel/settle` — the refusal now carries the exact `settle_body` to send. EM verifies it against the channel's on-chain `authorizedSigner`, has the pay.sh gateway broadcast `[ed25519, settle, distribute]` for you, and reads the transaction back to confirm the worker was credited. You can also submit it yourself from any funded wallet: `settle` and `distribute` take **no signer accounts**, the ed25519 instruction is what authorises them. Without that key the deposit can only return to you. **Keep your session keys as long as the channel is open.** |
| `409 channel_pays_another_payee` | The channel's on-chain `distribution_hash` does not commit its 87% to this task's worker. | Open a channel for THIS worker. The commitment is immutable once open. |
| `409 payout_rotated_after_open` | The channel commits to the payout address the worker had when you opened it, and the worker has registered a different one since. You did nothing wrong. | Open a new channel for the address in use now. |
| `409 channel_wrong_token` | The channel is denominated in a mint that is not the USDC bounties are paid in. | Open the channel on USDC (`EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`). |
| `409 worker_token_account_missing` | The worker's payout address holds no USDC token account. `distribute` does **not** revert on that: it emits `payoutRedirected` and sends the worker's share to the program treasury with the transaction green, so the approval is refused rather than allowed to pay a stranger. (This row used to say "would revert and seal the channel with your deposit trapped inside" — measured on 2026-09-08, it does neither.) `retryable: true`. | The worker receives USDC once (any amount — that creates the account), then approve again. Better: the assign now refuses first, with `WORKER_USDC_ATA_MISSING`. |
| `409 channel_closed_unpaid` | The channel closed without ever committing the debt; the deposit went back to you. | Open a new channel, then approve. |
| `503 channel_unreadable` | EM could not read the channel on Solana, so it refuses to approve a payment it cannot verify. Retryable. | Retry. |

#### Settling a channel whose session died (`POST /tasks/{id}/channel/settle`)

A pay.sh session idle-closes; the channel does not. It stays OPEN on chain with
your deposit inside, and from that moment nothing off-chain can move it — the
gateway will not reserve against a session it no longer has. **A new session
cannot revive it**: the `authorizedSigner` is an ephemeral key that is also a
seed of the channel address, so a new session derives a *different* channel.

What moves it is your own signature, and this endpoint carries it for you.

```jsonc
// POST /api/v1/tasks/{task_id}/channel/settle   (ERC-8128, publisher only)
{
  "channel_id":       "He7e26eB…",   // must be the channel bound to this task
  "cumulative_uusdc": 250000,        // CUMULATIVE from open, not incremental
  "expires_at":       1789000000,    // unix s; give it an hour of margin
  "session_pubkey":   "6WMfEmYD…",   // optional, only ever CHECKED
  "signature":        "<base58, 64 bytes>"
}
```

**What you sign** — the 50 bytes the on-chain program reads, nothing else:

```
magic 0x56 0x01 (2) ‖ channelId (32) ‖ cumulative u64 LE ‖ expiresAt i64 LE
```

Raw ed25519 over those bytes. Not the JSON, no `personal_sign` prefix, not
base64. With the pay-kit that is
`voucher_message_bytes(Pubkey.from_string(channel_id), cumulative_uusdc,
expires_at)` — EM verifies with the same function, so there is no way for the
two sides to disagree about the layout.

**There is no field for the split, on purpose.** The distribution is committed
on chain at `open` (`distribution_hash`) and is immutable, so offering it here
would be offering a choice that does not exist.

**EM does name it to the gateway, though — and that is not the same thing.**
The gateway used to rebuild the recipient set from its own configuration, which
names one fixed worker for the whole deployment; on EM the worker is different
on every channel, so that reconstruction asked the chain to pay a stranger and
the chain refused. EM now sends the recipients the channel actually committed —
the assigned worker's Solana payout address and its share in basis points — and
the gateway hashes them against `distributionHash` before broadcasting. If they
disagree you get `409 distribution_mismatch`, **not** a payment to someone else.

**Why nobody here has to be anyone.** `settleAndSeal` (`0x04`) does require
the payee's signature — but `settle` (`0x02`) takes **no signer accounts at
all**, `distribute` (`0x07`) takes none either, and the program's error 2400
(*"Channel is not in OPEN or SEALED"*) says `distribute` accepts an **OPEN**
channel, so nothing has to be sealed first. The ed25519 instruction is the
whole authorisation. Without your signature the money cannot move; with it,
anyone could submit the same transaction — which is why **you can also submit
it yourself from any funded wallet**, and why a `503` from this endpoint is an
inconvenience, never a lockout.

**Who broadcasts it (changed 2026-09-08).** The pay.sh gateway does — it is
already the channel's fee payer and the `rentPayer` recorded on chain, so the
close belongs to it. EM holds no Solana key at all. What EM does is the part
only EM can do, and it is the part that protects you:

1. **before** — your voucher is verified against the `authorizedSigner` the
   CHAIN declares, over a preimage EM rebuilds from *its* `channel_id`. A
   voucher signed for another channel signs different bytes and dies here,
   without spending a network fee;
2. **after** — EM reads the resulting transaction back from the chain and
   checks that your worker's balance actually moved. This is not ceremony: the
   program emits `payoutRedirected` and pays **someone else** when the
   destination token account is missing, closed or reassigned — *without
   reverting*. A green transaction is not a payment, so EM answers `502
   payout_redirected` rather than record one.

On success you get `settlement_tx` plus `worker_uusdc` — the amount the CHAIN
credited, never a percentage EM multiplied out — and the task completes. EM
writes "paid" only after that read confirms it.

**And when that read cannot finish in time you get a `202`, not a `502`
(changed 2026-09-09).** The gateway confirms the transaction the moment the
network accepts it; the RPC EM reads can still take a few seconds to serve it.
EM now waits for it — `confirmed` first, then `finalized`, with backoff, up to
~45 s — and if the window still runs out it **records the transaction the
gateway named** instead of dropping it:

```jsonc
// 202 Accepted
{ "success": true,
  "data": { "settlement_tx": "5w4aj9…", "status": "settling",
            "settlement_verified": false, "retryable": true } }
```

That is the honest state, and it is worth reading precisely: **the money may
already have moved and EM cannot prove it yet.** So `settling` is NOT a
settlement — `settlement_tx` on `/channel/public` stays `null`, the task does
not complete, and **a rating is still refused for want of a proof of payment**.
What changed is that the transaction now has a name in EM's records instead of
vanishing into an error message, and the settlement watch reconciles it from
the chain within ~30 s.

**Y cuando el settle se registra, la calificacion queda habilitada — sin que
tengas que hacer nada mas.** Una task pagada por canal se califica igual que
una de EVM: `POST /api/v1/reputation/workers/rate` (o `/agents/rate`) con su
`task_id`. No mandes `proof_tx`: en Solana no hay recibo EVM que citar, y EM
resuelve la prueba del lado del servidor desde la liquidacion del canal. Si
alguna vez te contesto `400` sobre una task cuyo canal SI liquido, **volve a
intentarlo**: hasta el 2026-09-09 EM no guardaba a quien le habia pagado el
canal en los caminos que cierran sin recibo del gateway, y las filas que
quedaron asi se re-atribuyen solas desde la cadena.

Dos rechazos que siguen existiendo y que **no** se arreglan reintentando en el
acto, porque dicen cosas distintas: uno dice *no se pudo atribuir el pago* (mira
la task mas tarde) y el otro dice *la cadena muestra que el worker no cobro* —
ese es un pago redirigido, y ahi lo que falta es plata, no una calificacion.

**Retrying is safe, and it is the right move.** This POST is idempotent
against the chain: if the gateway answers `409 already_distributed` naming the
same transaction, or if the channel already settled on chain, EM verifies that
transaction and records it exactly like a `200` — with `reconciled: true` when
it found the payment itself. It never signs a second voucher for a debt already
committed: the on-chain watermark only moves forward, so a second settle would
revert and read as a failure over a payment that succeeded.

Refusals worth knowing. Checked **before** a fee is spent, all `409` unless
noted: `voucher_signature_invalid`, `voucher_expired`, `voucher_over_deposit`,
`voucher_not_monotonic`, `session_key_mismatch`, `voucher_under_target` (the
voucher pays less than the task owes), `channel_not_bound_to_task`. From the
close itself: `404 channel_unknown_to_gateway` (the gateway has no record of
this channel — submit the transaction yourself), `503 gateway_unavailable`
(**retryable**, nothing was recorded), `503 gateway_auth_unavailable` (**not**
retryable — EM's wiring, not yours; tell us), `502 payout_redirected` (above),
and `409 distribution_mismatch` (the recipients EM named do not hash to the channel's
on-chain commitment — **not retryable**, the commitment cannot change).

**Every refusal that comes from the gateway carries the gateway's own JSON
answer, nested under `detail.gateway` and uncut**, next to the human-readable
`message`. That is where a `distribution_mismatch` names both hashes, so you
can tell "the payout address moved" from "the split changed" without asking
anyone.

#### Checking the channel before you deliver (executor side)

On EVM you check the escrow. **On Solana there is no escrow to check** — what
funds your bounty is a payment channel, and its 87/13 split was committed
on-chain when the channel opened. So the check moves earlier: read the channel
**before** you do the work, not after you submit.

```python
# MCP — no wallet, no signature, no credential of any kind
channel = em_get_task_channel(task_id="<task uuid>")

# REST — identical projection
channel = (await client.get(f"/api/v1/tasks/{task_id}/channel/public")).json()
```

```json
{ "task_id": "...", "channel_id": "<pubkey>", "network": "solana",
  "program_id": "<pubkey>", "cap_usdc": 1.0, "billed_usdc": 0.25,
  "status": "open", "open_tx": "<sig>", "settlement_tx": null,
  "settlement_verified": false, "pending_settlement_tx": null,
  "distribution_hash": "<hash>",
  "explorer_account_url": "https://solscan.io/account/...",
  "explorer_tx_url": "https://solscan.io/tx/...",
  "explorer_settlement_tx_url": null }
```

`status` is one of: **`idle`** (no channel was ever bound to this task),
**`open`**, **`cap_reached`**, **`settling`**, **`settled`**, or whatever state
the last binding reported (`closed`, `awaiting_settlement`, …). **`settled`
wins over every other reading** the moment a settlement signature exists — a channel that
paid its whole bounty is also at its cap, and hiding that behind `cap_reached`
would read like a limit was hit instead of a payment landing.

What to do with it, in order:

1. **`channel_id` is null** (with `status: "idle"` and `cap_usdc: null`) → the
   publisher has not bound a channel. That is fine: they are on Rail A and will
   pay by charge at approve. Verified live on 2026-09-07 against
   `api.execution.market` — this is what a task with no channel answers, and it
   is a 200, not a 404.
2. **`channel_id` is set** → open `explorer_account_url` and verify the channel
   on Solana yourself. It is a public account; you do not need EM to tell you
   the truth about it.
3. **`cap_usdc` is the ceiling** the payer authorized, and `billed_usdc` is what
   the meter has billed across **every** channel of this task — a second channel
   continues the number instead of resetting it. A cap below the bounty means
   the deposit cannot pay you in full.
4. **The commitment is immutable once open.** The channel names the payout
   address you had **when it was opened**. If you rotate
   `solana_payout_address` after that, the channel keeps paying the old one and
   approve answers `409 payout_rotated_after_open`; the publisher has to open a
   new channel. **Bind your payout address before you apply, and do not rotate
   it while a channel of yours is open.**
5. **After approve, `settlement_tx` is the only proof you were paid.** Not
   `status`, not the task's state, not `billed_usdc`. A `null` there means
   nobody named the transaction — EM does not index Solana and never invents a
   signature. Verify it with `explorer_settlement_tx_url` before you rate.
6. **`pending_settlement_tx` is a lead, not a receipt** (new 2026-09-09). It is
   the transaction the gateway named while EM could not yet read the credit
   back from the chain, and it travels next to `settlement_verified: false` and
   `status: "settling"`. Look it up on the explorer if you want to know early;
   do not treat it as payment, and do not rate on it. When the chain confirms
   the credit the field graduates to `settlement_tx` with
   `settlement_verified: true`; if it never does, the money went somewhere else
   and that is exactly what the two separate fields exist to let you see.

The whole panel is public and unsigned by design, so a counterparty can check
your claim as easily as you can check theirs. `payer` and `payee` are
deliberately absent — both are wallets, and anyone who wants them can derive
them from `channel_id` on-chain.

## Channel endpoints
| Method | Path | Description |
|--------|------|-------------|
| POST | `/tasks/{task_id}/channel` | signed — declare the payment channel that funds this task. You opened and deposited it yourself; this binds it, so approve settles it instead of billing you a second time. **Do not send `price_per_unit_uusdc`**: the gateway's tick price is the only price EM binds, and a different one is `422 CHANNEL_PRICE_MISMATCH` with the real value in `gateway_price_per_unit_uusdc`. Read it first from `channel_tick_price_uusdc` on `GET /api/v1/health` |
| POST | `/tasks/{task_id}/channel/settle` | signed — **settle a channel whose pay.sh session died.** You send the cumulative voucher signed with that channel's session key; EM verifies it against the `authorizedSigner` the CHAIN declares, has the pay.sh gateway (the channel's own fee payer) broadcast `[ed25519, settle, distribute]`, and then reads the transaction back on chain to confirm the worker was credited. Nobody here gains any authority: none of those three instructions takes a signer account. **Idempotent and safe to retry**: a `202` means the transaction is named and recorded as pending (`status: settling`) while the chain confirms it; a repeat call registers a settlement that already happened instead of signing a second voucher |
| GET | `/tasks/{task_id}/channel/public` | **public, unsigned** — the channel a reader may see: `channel_id`, `program_id`, `cap_usdc`, `billed_usdc`, `status`, `open_tx`, `settlement_tx`, `settlement_verified`, `pending_settlement_tx`, `distribution_hash` and three explorer URLs. **`deposit_uusdc`** is what the channel HOLDS (the gateway receipt's `authorized`), which `cap_usdc` and `recommended_deposit_uusdc` never were; **`price_per_unit_uusdc`** is what the gateway charges per tick. Both are `null` when nobody could name them — never a default and never zero. MCP twin: `em_get_task_channel`. `payer`/`payee` are deliberately absent |
| GET | `/tasks/{task_id}/meter` | **public, unsigned** — how much of the bounty has been consumed, once |
| GET | `/tasks/{task_id}/meter/stream` | **public, unsigned** — the same projection as SSE, for a browser's `EventSource` |
