GovNet (EMG Protocol) Skill
A natural-language interface for the EMG (Epistemic Market Gauge) prediction-market
protocol — also known as GovNet. The skill exposes every user-facing operation:
list and watch markets, place and cancel orders, cast private votes during the
voting window, split chips into shares (or merge them back), monitor live order
books and fills, and read settlement results.
The skill is signing-aware. State-changing requests are gated by an EIP-712
signature produced via awp-wallet sign-typed-data — the skill itself never
reads, writes, or stores a private key.
Quick start
- Install awp-wallet (one-time):
awp-wallet --version. If missing, the
harness will install it from https://github.com/awp-core/awp-wallet.
- First-run handshake: the first invocation of any signed script runs
GET /v1/auth/info to learn the EIP-712 chain id and verifying contract,
then caches both under ~/.govnet/auth-info.json.
- Public reads work without a wallet:
python3 scripts/public/markets.py
python3 scripts/public/book.py --market 6 --worknet 11
- Signed reads/writes auto-resolve principal via
awp-wallet receive:python3 scripts/private/state.py
python3 scripts/trade/submit-order.py --market 6 --worknet 11 \
--side buy --kind limit --price 0.22 --quantity 100
End-to-end demo:
$ python3 scripts/helpers/what-can-i-do.py
{
"phase": "voting_and_trading",
"epoch_id": 6,
"next_transition_at": "2026-05-03T12:00:00Z",
"available": ["list-markets", "vote", "submit-order", "split", "merge", …]
}
API mapping
| Script |
Method |
Path |
Auth |
public/auth-info.py |
GET |
/v1/auth/info |
— |
public/auth-time.py |
GET |
/v1/auth/time (clock probe + drift check) |
— |
public/markets.py |
GET |
/v1/markets, /v1/markets/{id} |
— |
public/book.py |
GET |
/v1/markets/{m}/worknets/{wn}/book |
— |
public/klines.py |
GET |
/v1/markets/{m}/worknets/{wn}/klines |
— |
public/worknets.py |
GET |
/v1/worknets |
— |
public/epochs.py |
GET |
/v1/epochs/current, /v1/epochs/{id}, …/phase, …/results, …/voters†, …/votes/{principal}/history† |
— |
public/leaderboard.py† |
GET |
/v1/leaderboard/epistemic |
— |
public/merkle.py |
GET |
/v1/epochs/{id}/merkle-root, …/votes/{principal}/proof |
— |
private/state.py |
GET |
/v1/principals/{me}/state |
sig‡ |
private/power.py |
GET |
/v1/principals/{me}/power |
sig‡ |
private/managers.py |
GET |
/v1/principals/{me}/managers |
sig‡ |
private/recipient.py |
GET |
/v1/principals/{me}/recipient |
sig‡ |
private/orders-list.py† |
GET |
/v1/orders |
sig |
private/orders-get.py |
GET |
/v1/orders/{id} |
sig |
private/fills-list.py† |
GET |
/v1/fills (cursor-paginated, self-only) |
sig |
private/votes-mine.py |
GET |
/v1/principals/{me}/votes/{market_id} (post-reveal only) |
sig |
trade/submit-order.py |
POST |
/v1/orders |
sig |
trade/synthesize.py |
POST |
/v1/orders/synthesize (Smart Order Router; R8-D) |
sig |
trade/cancel-order.py |
DELETE |
/v1/orders/{id} |
sig |
trade/cancel-all.py |
POST |
/v1/orders/cancel-all |
sig |
trade/cancel-batch.py |
POST |
/v1/orders/cancel-batch |
sig |
vote/submit-vote.py |
POST |
/v1/epochs/{market_id}/votes |
sig |
vote/verify-proof.py |
local |
(Merkle proof reconstruction) |
— |
positions/split.py |
POST |
/v1/positions/split |
sig |
positions/merge.py |
POST |
/v1/positions/merge |
sig |
content/post-comment.py |
POST |
/v1/comments |
sig |
content/post-report.py |
POST |
/v1/reports |
sig |
content/endorse.py |
POST |
/v1/comments/{id}/endorse |
sig |
stream/watch-book.py |
WS |
book.{m}.{wn} |
— |
stream/watch-klines.py |
WS |
klines.{m}.{wn}.{interval} |
— |
stream/watch-phase.py |
WS |
phase |
— |
stream/watch-private.py |
WS |
fills.me, orders.me (with auth.hello) |
sig |
helpers/what-can-i-do.py |
local |
(phase-aware operation listing) |
— |
helpers/countdown.py |
local |
(time until next phase boundary) |
— |
helpers/show-receipt.py |
local |
(pretty-print a fill / settlement result) |
— |
Every script emits a single JSON object (or one JSON-Lines stream for stream/*)
to stdout so the calling agent can parse it directly.
Scripts marked † support a --all-pages flag that walks
pagination.next_cursor until exhausted and concatenates all data[] arrays
into one response. has_more === false is the authoritative stop signal
(see references/api-shapes.md). Default cap is 100 pages — when hit, the
output carries truncated_at_max_pages: true plus next_cursor for resume.
Private listings still cost one nonce per page (each page is a separately-signed
request) — don't blindly enable --all-pages on huge listings.
Endpoints marked sig‡ carry security: [] in OpenAPI (documented as
public reads) but production actually requires EMG-SIG-V1 — the skill
signs them anyway. The OpenAPI lag is a known doc/server drift; reproduce
the auth-required behavior in any reimplementation. The query parameter
on /principals/.../state, /principals/.../power, /principals/.../recipient
is canonical market_id; epoch_id is accepted as an alias post-9387e78.
Signing (EMG-SIG-V1) — the load-bearing summary
Every authenticated request carries five headers:
| Header |
Value |
X-EMG-Principal |
0x-hex 20-byte Staker address |
X-EMG-Actor |
0x-hex 20-byte signer (omit when actor == principal) |
X-EMG-Nonce |
strictly-greater unsigned integer |
X-EMG-Timestamp |
Unix seconds UTC (server enforces ±30s window) |
X-EMG-Signature |
0x-hex 65 bytes (r‖s‖v); over the EIP-712 EMGRequest digest |
Critical contract gotchas — these have all bitten implementers before:
- Strip the
/v1 prefix when populating the path field of the EMGRequest
typed data. The production server is mounted under /v1 via axum's
Router::nest, which strips the prefix BEFORE the auth middleware sees the
URI. Sign /orders, not /v1/orders.
- Exception: WebSocket
auth.hello signs path: "/v1/ws" because the WS
handler reads the full URI via a hardcoded literal.
- WebSocket subscribe param is
channels, NOT topics. Server returns
INVALID_PARAMS and silently drops every push if you send topics.
- WS notification field is
params.channel, NOT params.topic.
- BookDelta
new_quantity is ABSOLUTE, not a diff. 0 removes the level.
- Side enum: book channel uses
bid/ask; orders use buy/sell.
- Decimals are strings at scale 18. Carry as strings on the wire; coerce
to
decimal.Decimal for arithmetic; never use float.
- Nonce is strictly greater than the server's stored value, tracked
per-principal under
~/.govnet/nonces/<principal>.json with atomic rename.
AUTH_NONCE_TOO_LOW / NONCE_TOO_LOW retry: re-fetch /v1/auth/info
to read the server's stored value, bump local +1, retry once.
- 5xx with
X-EMG-Nonce-Burned: true means the server consumed the nonce
even though the request failed. Bump local nonce floor BEFORE retrying.
- Idempotency keys (
X-Idempotency-Key): same key + same body returns
cached response within 24 h. Same key + different body → 503 / 409
STATE_IDEMPOTENCY_KEY_MISMATCH (client bug). Generate a fresh key per
logical action; reuse on retry.
Full implementation reference: references/signing.md.
Phase awareness
Refuse signed writes early when the current phase doesn't allow them. Fetch
/v1/epochs/current (or /v1/epochs/{id}/phase) and check this matrix:
|
pending |
voting_and_trading |
trading_only |
settling |
completed |
| Read public data |
✓ |
✓ |
✓ |
✓ |
✓ |
| Read private state |
✓ |
✓ |
✓ |
✓ |
✓ |
| Submit order |
✗ |
✓ |
✓ |
✗ |
✗ |
| Cancel order |
✗ |
✓ |
✓ |
✗ |
✗ |
| Submit vote |
✗ |
✓ |
✗ |
✗ |
✗ |
| Split / merge position |
✗ |
✓ |
✓ |
✗ |
✗ |
| Read settlement results |
✗ |
✗ |
✗ |
✗ |
✓ |
Server-side phase enums use both snake_case (voting_and_trading) and
CamelCase (VotingAndTrading); helpers in scripts/lib/govnet_lib.py
normalize either form. references/status-state-machine.md has the full map.
Composition with awp-wallet / awp-skill
awp-wallet is a hard dependency. Every signed request goes through
awp-wallet sign-typed-data --data '<json>'. The skill never sees a private
key directly. See https://github.com/awp-core/awp-wallet.
awp-skill is a soft dependency. EMG snapshots veAWP-derived AWP Power
at every Wednesday 12:00 UTC. When STATE_PRINCIPAL_NOT_IN_EPOCH fires, the
skill should hint at awp-skill rather than try to handle staking itself.
Error handling discipline
Map server code → user-facing action. Every signed-write script has the same
retry policy:
| Code |
Action |
AUTH_MISSING_HEADER |
Log and abort — skill bug. |
AUTH_SIGNATURE_INVALID |
Refresh /v1/auth/info, retry once. Else surface as a domain-mismatch. |
AUTH_NONCE_TOO_LOW / NONCE_TOO_LOW |
signed_request auto: refresh auth-info, bump_to(server_stored), retry once. |
AUTH_TIMESTAMP_OUT_OF_WINDOW |
signed_request auto: fetch /v1/auth/time, set local clock-offset, retry once. If still skewed, surface "NTP daemon may be stuck". |
AUTH_EIP712_DOMAIN_MISMATCH |
signed_request auto: force-refresh /v1/auth/info (chainId / verifyingContract changed), retry once. |
AUTH_UNAUTHORIZED_DELEGATE |
Surface "this manager is not authorized for this principal" (likely stale awp-skill config). |
BUSINESS_PHASE_MISMATCH / BUSINESS_TRADING_ONLY_PHASE |
Surface phase + countdown to when the op opens again. |
BUSINESS_INSUFFICIENT_BALANCE |
Surface chips_available. Do NOT auto-retry. |
BUSINESS_INSUFFICIENT_SHARES |
Surface available shares per worknet. (Triggered by merge or sell when out of stock.) |
BUSINESS_REPORT_ALREADY_SUBMITTED |
One report per (market, worknet) per epoch — do NOT auto-retry. Show user the existing report id. |
BUSINESS_VOTE_ALREADY_FINAL |
Phase 1 closed; show phase_closed_at. |
BUSINESS_NOT_WORKNET_OPERATOR |
Only the worknet's configured operator may submit reports. |
BUSINESS_ORDER_NOT_FOUND / BUSINESS_ORDER_NOT_OWNED |
404 vs 403 — distinguish "doesn't exist" from "exists but yours". |
STATE_PRINCIPAL_NOT_IN_EPOCH |
Three causes (indistinguishable from response): no veAWP at all (stake via awp-skill), or lock_end too close to epoch settlement (extend via veAWP.addToPosition), or snapshot indexer miss (escalate). Cross-check on-chain veAWP.getVotingPower before suggesting "go stake". |
STATE_VOTES_NOT_REVEALED |
Phase 2→3 boundary hasn't fired; tell user to retry after settlement starts. |
STATE_RESULTS_NOT_FOUND |
Tell user to retry after settlement window. |
IDEMPOTENCY_KEY_REUSE (post-H3, 422) |
Same X-Idempotency-Key reused with different body. Generate fresh key, do NOT auto-retry. Replaces pre-2026-05 STATE_IDEMPOTENCY_KEY_MISMATCH (409). |
RATE_LIMIT_EXCEEDED / RATE_LIMIT_BACKPRESSURE |
signed_request auto: parse Retry-After (delta-seconds OR HTTP-date), sleep ≤ 60s, retry once. |
VALIDATION_SIMPLEX_CONSTRAINT_VIOLATED (422) |
Vote vector |Σ − 1| > 1e-9 (DB-layer + app-layer check). Re-normalize and retry. |
INTERNAL_MATCHER_UNAVAILABLE (503) |
Per-worknet matcher down; backoff (250ms × 2^attempt, max 5s). Surface congestion if persists > 1m. |
INTERNAL_WAL_DISK_FULL (503) |
Operator-paging condition; never auto-retry — surface and bail. |
INTERNAL_* (5xx) with X-EMG-Nonce-Burned |
Bump nonce, surface error to caller (no auto-retry; idempotency unclear). |
INSECURE_TRANSPORT (client) |
Set GOVNET_API_BASE / GOVNET_WS_URL to https:// / wss://. Skill refuses plaintext. |
INSECURE_REDIRECT (client) |
Server returned 30x. Skill refuses to follow (signed headers would leak). Fix DNS / config upstream. |
Full code → message map: references/error-codes.md (incl. client-emitted codes + post-H3 idempotency section).
Confirm-before-irreversible
Every signed-write script writes a confirmation block to stderr and waits for
y on stdin (matching awp-wallet send's pattern):
[TX] about to submit order:
market: №6 (aMINE / aGOV / aPRED / aKYA / aARDI / aTMR / aCOM)
worknet: aGOV (id 11)
side: buy
kind: limit @ 0.2200
quantity: 100
post_only: false
reduce_only: false
stp_mode: cancel_both
idem-key: 018f-…
nonce: 43 (was 42)
proceed? (y/n)
If stdin is not a tty, the script aborts unless --yes was supplied. NEVER
auto-execute a signed write without explicit consent.
Bundled references
Load on demand:
references/api-shapes.md — request/response shape per endpoint.
references/signing.md — EMG-SIG-V1 walkthrough with worked examples.
references/status-state-machine.md — phase transitions + countdown logic.
references/error-codes.md — full code → user-text map.
Layout
gov-skill/ # repo root === skill root
├── SKILL.md
├── README.md
├── LICENSE
├── scripts/
│ ├── lib/ # canonical, sign, nonce, ws, govnet_lib
│ ├── public/ # 8 unauthenticated readers
│ ├── private/ # signed reads
│ ├── trade/ # signed writes — orders
│ ├── vote/ # signed writes — votes
│ ├── positions/ # signed writes — split/merge
│ ├── content/ # signed writes — comments / reports
│ ├── stream/ # JSON-Lines WebSocket subscribers
│ └── helpers/ # phase-aware local helpers
├── references/ # markdown loaded on demand by the agent
└── tests/ # pytest — known-answer canonical + EIP-712 digest
1---2name: govnet3description: EMG protocol (a.k.a. GovNet) — list/watch prediction markets, place/cancel limit/market orders, cast private votes during the Wed-Thu voting window, split chips into worknet shares (or merge), watch live order books + fills.me/orders.me, read settlement results. Use this skill whenever the user mentions: GovNet, gov.works, EMG, emission market, worknet (aMINE/aGOV/aPRED/aKYA/aARDI/aTMR/aCOM), "chips this epoch", "split into shares", voting Wednesday, settlement Tuesday, V_j / W_j / Σ Pⱼ, AWP Power, "this week's market", "trading closes", "market phase". Trigger even when the user does not type "govnet" — any of these phrases (chips, worknet, weekly emission, per-Principal voting) means this skill is the right tool. Composes with awp-wallet (every signed request goes through it) and awp-skill (veAWP / AWP Power). NOT for: Polymarket, Augur, Hyperliquid, Binance, Uniswap, Aave, Lido, generic DAO proposals (Compound, Snapshot), veAWP staking (awp-skill), raw token transfers, NFT trading.4---56# GovNet (EMG Protocol) Skill78A natural-language interface for the EMG (Epistemic Market Gauge) prediction-market9protocol — also known as GovNet. The skill exposes every user-facing operation:10list and watch markets, place and cancel orders, cast private votes during the11voting window, split chips into shares (or merge them back), monitor live order12books and fills, and read settlement results.1314The skill is signing-aware. State-changing requests are gated by an EIP-71215signature produced via `awp-wallet sign-typed-data` — the skill itself never16reads, writes, or stores a private key.1718---1920## Quick start21221. **Install awp-wallet** (one-time): `awp-wallet --version`. If missing, the23 harness will install it from <https://github.com/awp-core/awp-wallet>.242. **First-run handshake**: the first invocation of any signed script runs25 `GET /v1/auth/info` to learn the EIP-712 chain id and verifying contract,26 then caches both under `~/.govnet/auth-info.json`.273. **Public reads** work without a wallet:28 ```29 python3 scripts/public/markets.py30 python3 scripts/public/book.py --market 6 --worknet 1131 ```324. **Signed reads/writes** auto-resolve principal via `awp-wallet receive`:33 ```34 python3 scripts/private/state.py35 python3 scripts/trade/submit-order.py --market 6 --worknet 11 \36 --side buy --kind limit --price 0.22 --quantity 10037 ```3839End-to-end demo:4041```42$ python3 scripts/helpers/what-can-i-do.py43{44 "phase": "voting_and_trading",45 "epoch_id": 6,46 "next_transition_at": "2026-05-03T12:00:00Z",47 "available": ["list-markets", "vote", "submit-order", "split", "merge", …]48}49```5051---5253## API mapping5455| Script | Method | Path | Auth |56|-----------------------------------------|--------|-----------------------------------------------------------------|------|57| `public/auth-info.py` | GET | `/v1/auth/info` | — |58| `public/auth-time.py` | GET | `/v1/auth/time` (clock probe + drift check) | — |59| `public/markets.py` | GET | `/v1/markets`, `/v1/markets/{id}` | — |60| `public/book.py` | GET | `/v1/markets/{m}/worknets/{wn}/book` | — |61| `public/klines.py` | GET | `/v1/markets/{m}/worknets/{wn}/klines` | — |62| `public/worknets.py` | GET | `/v1/worknets` | — |63| `public/epochs.py` | GET | `/v1/epochs/current`, `/v1/epochs/{id}`, `…/phase`, `…/results`, `…/voters`†, `…/votes/{principal}/history`† | — |64| `public/leaderboard.py`† | GET | `/v1/leaderboard/epistemic` | — |65| `public/merkle.py` | GET | `/v1/epochs/{id}/merkle-root`, `…/votes/{principal}/proof` | — |66| `private/state.py` | GET | `/v1/principals/{me}/state` | sig‡ |67| `private/power.py` | GET | `/v1/principals/{me}/power` | sig‡ |68| `private/managers.py` | GET | `/v1/principals/{me}/managers` | sig‡ |69| `private/recipient.py` | GET | `/v1/principals/{me}/recipient` | sig‡ |70| `private/orders-list.py`† | GET | `/v1/orders` | sig |71| `private/orders-get.py` | GET | `/v1/orders/{id}` | sig |72| `private/fills-list.py`† | GET | `/v1/fills` (cursor-paginated, self-only) | sig |73| `private/votes-mine.py` | GET | `/v1/principals/{me}/votes/{market_id}` (post-reveal only) | sig |74| `trade/submit-order.py` | POST | `/v1/orders` | sig |75| `trade/synthesize.py` | POST | `/v1/orders/synthesize` (Smart Order Router; R8-D) | sig |76| `trade/cancel-order.py` | DELETE | `/v1/orders/{id}` | sig |77| `trade/cancel-all.py` | POST | `/v1/orders/cancel-all` | sig |78| `trade/cancel-batch.py` | POST | `/v1/orders/cancel-batch` | sig |79| `vote/submit-vote.py` | POST | `/v1/epochs/{market_id}/votes` | sig |80| `vote/verify-proof.py` | local | (Merkle proof reconstruction) | — |81| `positions/split.py` | POST | `/v1/positions/split` | sig |82| `positions/merge.py` | POST | `/v1/positions/merge` | sig |83| `content/post-comment.py` | POST | `/v1/comments` | sig |84| `content/post-report.py` | POST | `/v1/reports` | sig |85| `content/endorse.py` | POST | `/v1/comments/{id}/endorse` | sig |86| `stream/watch-book.py` | WS | `book.{m}.{wn}` | — |87| `stream/watch-klines.py` | WS | `klines.{m}.{wn}.{interval}` | — |88| `stream/watch-phase.py` | WS | `phase` | — |89| `stream/watch-private.py` | WS | `fills.me`, `orders.me` (with `auth.hello`) | sig |90| `helpers/what-can-i-do.py` | local | (phase-aware operation listing) | — |91| `helpers/countdown.py` | local | (time until next phase boundary) | — |92| `helpers/show-receipt.py` | local | (pretty-print a fill / settlement result) | — |9394Every script emits a single JSON object (or one JSON-Lines stream for `stream/*`)95to stdout so the calling agent can parse it directly.9697Scripts marked **†** support a `--all-pages` flag that walks98`pagination.next_cursor` until exhausted and concatenates all `data[]` arrays99into one response. `has_more === false` is the authoritative stop signal100(see `references/api-shapes.md`). Default cap is 100 pages — when hit, the101output carries `truncated_at_max_pages: true` plus `next_cursor` for resume.102Private listings still cost one nonce per page (each page is a separately-signed103request) — don't blindly enable `--all-pages` on huge listings.104105Endpoints marked **sig‡** carry `security: []` in OpenAPI (documented as106public reads) but production actually requires EMG-SIG-V1 — the skill107signs them anyway. The OpenAPI lag is a known doc/server drift; reproduce108the auth-required behavior in any reimplementation. The query parameter109on `/principals/.../state`, `/principals/.../power`, `/principals/.../recipient`110is canonical `market_id`; `epoch_id` is accepted as an alias post-9387e78.111112---113114## Signing (EMG-SIG-V1) — the load-bearing summary115116Every authenticated request carries five headers:117118| Header | Value |119|---------------------|-------------------------------------------------------------|120| `X-EMG-Principal` | 0x-hex 20-byte Staker address |121| `X-EMG-Actor` | 0x-hex 20-byte signer (omit when actor == principal) |122| `X-EMG-Nonce` | strictly-greater unsigned integer |123| `X-EMG-Timestamp` | Unix seconds UTC (server enforces ±30s window) |124| `X-EMG-Signature` | 0x-hex 65 bytes (r‖s‖v); over the EIP-712 `EMGRequest` digest|125126Critical contract gotchas — these have all bitten implementers before:1271281. **Strip the `/v1` prefix** when populating the `path` field of the `EMGRequest`129 typed data. The production server is mounted under `/v1` via axum's130 `Router::nest`, which strips the prefix BEFORE the auth middleware sees the131 URI. Sign `/orders`, not `/v1/orders`.132 - Exception: WebSocket `auth.hello` signs `path: "/v1/ws"` because the WS133 handler reads the full URI via a hardcoded literal.1342. **WebSocket subscribe param is `channels`, NOT `topics`.** Server returns135 `INVALID_PARAMS` and silently drops every push if you send `topics`.1363. **WS notification field is `params.channel`, NOT `params.topic`.**1374. **BookDelta `new_quantity` is ABSOLUTE**, not a diff. `0` removes the level.1385. **Side enum**: book channel uses `bid`/`ask`; orders use `buy`/`sell`.1396. **Decimals are strings at scale 18.** Carry as strings on the wire; coerce140 to `decimal.Decimal` for arithmetic; never use `float`.1417. **Nonce is strictly greater than the server's stored value**, tracked142 per-principal under `~/.govnet/nonces/<principal>.json` with atomic rename.1438. **`AUTH_NONCE_TOO_LOW` / `NONCE_TOO_LOW` retry**: re-fetch `/v1/auth/info`144 to read the server's stored value, bump local +1, retry once.1459. **5xx with `X-EMG-Nonce-Burned: true`** means the server consumed the nonce146 even though the request failed. Bump local nonce floor BEFORE retrying.14710. **Idempotency keys** (`X-Idempotency-Key`): same key + same body returns148 cached response within 24 h. Same key + different body → 503 / 409149 `STATE_IDEMPOTENCY_KEY_MISMATCH` (client bug). Generate a fresh key per150 logical action; reuse on retry.151152Full implementation reference: `references/signing.md`.153154---155156## Phase awareness157158Refuse signed writes early when the current phase doesn't allow them. Fetch159`/v1/epochs/current` (or `/v1/epochs/{id}/phase`) and check this matrix:160161| | pending | voting_and_trading | trading_only | settling | completed |162|------------------------------|:-------:|:------------------:|:------------:|:--------:|:---------:|163| Read public data | ✓ | ✓ | ✓ | ✓ | ✓ |164| Read private state | ✓ | ✓ | ✓ | ✓ | ✓ |165| Submit order | ✗ | ✓ | ✓ | ✗ | ✗ |166| Cancel order | ✗ | ✓ | ✓ | ✗ | ✗ |167| Submit vote | ✗ | ✓ | ✗ | ✗ | ✗ |168| Split / merge position | ✗ | ✓ | ✓ | ✗ | ✗ |169| Read settlement results | ✗ | ✗ | ✗ | ✗ | ✓ |170171Server-side phase enums use both snake_case (`voting_and_trading`) and172CamelCase (`VotingAndTrading`); helpers in `scripts/lib/govnet_lib.py`173normalize either form. `references/status-state-machine.md` has the full map.174175---176177## Composition with awp-wallet / awp-skill178179- **`awp-wallet`** is a hard dependency. Every signed request goes through180 `awp-wallet sign-typed-data --data '<json>'`. The skill never sees a private181 key directly. See <https://github.com/awp-core/awp-wallet>.182- **`awp-skill`** is a soft dependency. EMG snapshots veAWP-derived AWP Power183 at every Wednesday 12:00 UTC. When `STATE_PRINCIPAL_NOT_IN_EPOCH` fires, the184 skill should hint at `awp-skill` rather than try to handle staking itself.185186---187188## Error handling discipline189190Map server `code` → user-facing action. Every signed-write script has the same191retry policy:192193| Code | Action |194|---------------------------------------------------|-----------------------------------------------------------------------------------------------------|195| `AUTH_MISSING_HEADER` | Log and abort — skill bug. |196| `AUTH_SIGNATURE_INVALID` | Refresh `/v1/auth/info`, retry once. Else surface as a domain-mismatch. |197| `AUTH_NONCE_TOO_LOW` / `NONCE_TOO_LOW` | `signed_request` auto: refresh auth-info, `bump_to(server_stored)`, retry once. |198| `AUTH_TIMESTAMP_OUT_OF_WINDOW` | `signed_request` auto: fetch `/v1/auth/time`, set local clock-offset, retry once. If still skewed, surface "NTP daemon may be stuck". |199| `AUTH_EIP712_DOMAIN_MISMATCH` | `signed_request` auto: force-refresh `/v1/auth/info` (chainId / verifyingContract changed), retry once. |200| `AUTH_UNAUTHORIZED_DELEGATE` | Surface "this manager is not authorized for this principal" (likely stale `awp-skill` config). |201| `BUSINESS_PHASE_MISMATCH` / `BUSINESS_TRADING_ONLY_PHASE` | Surface phase + countdown to when the op opens again. |202| `BUSINESS_INSUFFICIENT_BALANCE` | Surface `chips_available`. Do NOT auto-retry. |203| `BUSINESS_INSUFFICIENT_SHARES` | Surface available shares per worknet. (Triggered by `merge` or sell when out of stock.) |204| `BUSINESS_REPORT_ALREADY_SUBMITTED` | One report per (market, worknet) per epoch — do NOT auto-retry. Show user the existing report id. |205| `BUSINESS_VOTE_ALREADY_FINAL` | Phase 1 closed; show `phase_closed_at`. |206| `BUSINESS_NOT_WORKNET_OPERATOR` | Only the worknet's configured operator may submit reports. |207| `BUSINESS_ORDER_NOT_FOUND` / `BUSINESS_ORDER_NOT_OWNED` | 404 vs 403 — distinguish "doesn't exist" from "exists but yours". |208| `STATE_PRINCIPAL_NOT_IN_EPOCH` | Three causes (indistinguishable from response): no veAWP at all (stake via awp-skill), or lock_end too close to epoch settlement (extend via veAWP.addToPosition), or snapshot indexer miss (escalate). Cross-check on-chain veAWP.getVotingPower before suggesting "go stake". |209| `STATE_VOTES_NOT_REVEALED` | Phase 2→3 boundary hasn't fired; tell user to retry after settlement starts. |210| `STATE_RESULTS_NOT_FOUND` | Tell user to retry after settlement window. |211| `IDEMPOTENCY_KEY_REUSE` *(post-H3, 422)* | Same `X-Idempotency-Key` reused with different body. Generate fresh key, do NOT auto-retry. Replaces pre-2026-05 `STATE_IDEMPOTENCY_KEY_MISMATCH` (409). |212| `RATE_LIMIT_EXCEEDED` / `RATE_LIMIT_BACKPRESSURE` | `signed_request` auto: parse `Retry-After` (delta-seconds OR HTTP-date), sleep ≤ 60s, retry once. |213| `VALIDATION_SIMPLEX_CONSTRAINT_VIOLATED` (422) | Vote vector \|Σ − 1\| > 1e-9 (DB-layer + app-layer check). Re-normalize and retry. |214| `INTERNAL_MATCHER_UNAVAILABLE` (503) | Per-worknet matcher down; backoff (250ms × 2^attempt, max 5s). Surface congestion if persists > 1m. |215| `INTERNAL_WAL_DISK_FULL` (503) | Operator-paging condition; never auto-retry — surface and bail. |216| `INTERNAL_*` (5xx) with `X-EMG-Nonce-Burned` | Bump nonce, surface error to caller (no auto-retry; idempotency unclear). |217| `INSECURE_TRANSPORT` *(client)* | Set `GOVNET_API_BASE` / `GOVNET_WS_URL` to `https://` / `wss://`. Skill refuses plaintext. |218| `INSECURE_REDIRECT` *(client)* | Server returned 30x. Skill refuses to follow (signed headers would leak). Fix DNS / config upstream.|219220Full code → message map: `references/error-codes.md` (incl. client-emitted codes + post-H3 idempotency section).221222---223224## Confirm-before-irreversible225226Every signed-write script writes a confirmation block to stderr and waits for227`y` on stdin (matching `awp-wallet send`'s pattern):228229```230[TX] about to submit order:231 market: №6 (aMINE / aGOV / aPRED / aKYA / aARDI / aTMR / aCOM)232 worknet: aGOV (id 11)233 side: buy234 kind: limit @ 0.2200235 quantity: 100236 post_only: false237 reduce_only: false238 stp_mode: cancel_both239 idem-key: 018f-…240 nonce: 43 (was 42)241 proceed? (y/n)242```243244If stdin is not a tty, the script aborts unless `--yes` was supplied. NEVER245auto-execute a signed write without explicit consent.246247---248249## Bundled references250251Load on demand:252253- `references/api-shapes.md` — request/response shape per endpoint.254- `references/signing.md` — EMG-SIG-V1 walkthrough with worked examples.255- `references/status-state-machine.md` — phase transitions + countdown logic.256- `references/error-codes.md` — full code → user-text map.257258---259260## Layout261262```263gov-skill/ # repo root === skill root264├── SKILL.md265├── README.md266├── LICENSE267├── scripts/268│ ├── lib/ # canonical, sign, nonce, ws, govnet_lib269│ ├── public/ # 8 unauthenticated readers270│ ├── private/ # signed reads271│ ├── trade/ # signed writes — orders272│ ├── vote/ # signed writes — votes273│ ├── positions/ # signed writes — split/merge274│ ├── content/ # signed writes — comments / reports275│ ├── stream/ # JSON-Lines WebSocket subscribers276│ └── helpers/ # phase-aware local helpers277├── references/ # markdown loaded on demand by the agent278└── tests/ # pytest — known-answer canonical + EIP-712 digest279```