diff --git a/README.md b/README.md
index 0e17efb..ebfc9e0 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
| Path | Description |
| ---- | ----------- |
| π [algos](./algos/algos-notes.md) | Algorithm practice problems with Go implementations and Anki-formatted notes for deliberate practice |
-| π [aictx](./aictx/README.md) | File to context convertor for pasing files to feed LLMs. Similar to `cat` and `bat`. |
+| π [ctxcat](./ctxcat/README.md) | File to context convertor for pasing files to feed LLMs. Similar to `cat` and `bat`. |
| π [gocovmerge](./gocovmerge/README.md) | Go coverage profile merger with modern CLI features |
| π [mdtoc](./mdtoc/README.md) | Markdown table of contents (TOC) generator |
| π [winfixtext](./winfixtext/README.md) | Fixes Windows encoding issues and corrupted LLM text outputs |
diff --git a/ai-skills/commit-diff/SKILL.md b/ai-skills/commit-diff/SKILL.md
new file mode 100644
index 0000000..4742e6a
--- /dev/null
+++ b/ai-skills/commit-diff/SKILL.md
@@ -0,0 +1,73 @@
+---
+name: commit-diff
+description: >-
+ Generate a clear, cogent, concise, and conventional-like commit message from
+ the relevant git diff. Default is staged changes (what will be committed). Use
+ when the user asks for a commit message, says "save/commit", or asks to
+ summarize staged, working tree, or last commit changes.
+---
+
+# Commit Diff
+
+## When to Apply
+
+Use this skill when the user wants a commit message written from their `git diff`, or when they ask for a commit message for their changes without specifying
+one.
+
+## Instructions
+
+### 0) Confirm what the user wants to summarize
+
+Default to **what will be committed** (staged changes). If the user's phrasing is ambiguous, pick the most likely intent from the table below.
+
+| User intent (trigger phrases) | Use this command | Notes |
+|---|---|---|
+| "commit message", "save", "ready to commit", "staged", "what I'm about to commit" (default) | `git diff --staged` | Exactly what `git commit` would include right now. |
+| "latest changes", "since last commit", "working tree", "everything I changed" | `git diff HEAD` | Includes staged + unstaged changes vs last commit. |
+| "what's in my last commit", "latest commit", "changes in commit" | `git show HEAD` | Shows the patch for the last commit (includes new files). |
+| "compare to main", "PR diff", "branch delta", "since branching" | `git diff ...HEAD` | Merge-base diff (commits on this branch). If `` not given, assume `main` but ask once to confirm. |
+
+If `git diff --staged` is empty, check `git status`. If there are only unstaged changes, either ask the user to stage what they want committed or switch to `git diff HEAD` to summarize the whole working tree.
+
+### 1) Untracked files gotcha (new files)
+
+Diffs only include **tracked** content.
+
+- If `git status` shows `?? some-file`, that file will not appear in `git diff --staged` or `git diff HEAD` until you stage it.
+- If you want a diff that includes new files without fully staging them, use `git add -N ` (intent-to-add), then rerun the chosen diff.
+
+### 2) Write the commit message (Conventional Commits)
+
+Output **one subject line**, plus an optional body when it helps explain "why".
+
+Subject format:
+
+`type(scope): imperative summary`
+
+- **type**: prefer one of `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `build`, `ci`
+- **scope**: optional, short (e.g. `sai`, `indexer`, `scripts`, `docs`)
+- **imperative**: "add", "fix", "remove", "refactor" (not past tense)
+- **length**: keep subject <= 72 chars; no trailing period
+- **breaking changes**: add `!` after type/scope when clearly breaking (`feat!: β¦`, `feat(api)!: β¦`)
+
+Body (optional):
+
+- Blank line after subject
+- 1β3 bullets focusing on **why** and any non-obvious behavior changes
+
+### Examples
+
+```
+feat(sai): add vault OI query helpers
+```
+
+```
+fix(indexer): query staking data via staking container
+
+- Avoid deprecated root-level fields
+- Clarify amount units (unibi) in output
+```
+
+```
+chore(scripts): align tx query output formatting
+```
diff --git a/ai-skills/epics-plus/SKILL.md b/ai-skills/epics-plus/SKILL.md
new file mode 100644
index 0000000..8082444
--- /dev/null
+++ b/ai-skills/epics-plus/SKILL.md
@@ -0,0 +1,62 @@
+---
+name: epics-plus
+description: Discover, read, and manage Epics+ markdown documents in the boku repository. Use when asked about active tasks, priorities, epics for specific repos or tags, or when needing to regenerate the epic index (INDEX.md) using the epics-plus CLI.
+---
+
+# Epics+ Management
+
+Navigate, interpret, and manage Epics+ docs in the boku repository. Use this skill to understand the status and priority of work, and to use the `epics-plus` tooling.
+
+## Quick Start
+
+1. **Check active work**: Read [epics/INDEX.md](/home/realu/ki/boku/epics/INDEX.md) for the current dashboard of active epics grouped by priority.
+2. **Regenerate index**: If you've modified frontmatter or added an epic, update the dashboard:
+ ```bash
+ cd /home/realu/ki/boku/epics && just epics-plus index
+ ```
+3. **Verify/Scan**: To see all epics with frontmatter or validate them:
+ ```bash
+ cd /home/realu/ki/boku/epics && just epics-plus scan --only-with-frontmatter --strict
+ ```
+
+## Interpreting Epics+ Frontmatter (Reader View)
+
+When reading an epic doc, look at the YAML frontmatter at line 1 for context:
+
+- **status**: `active` | `inactive` | `done` | `archived`.
+- **priority**: `p0` (urgent) to `p3` (someday). `p2` is default.
+- **epic_kind**: `spec` | `journal` | `reference`.
+- **tags**: Broad themes (e.g., `sai`, `evm`, `slashing`). Use for "related work" queries.
+- **repos**: Which codebase this epic targets (e.g., `nibi-chain`, `sai-keeper`).
+- **related_context**: Links to other epics (`/epics/...`). Follow these to understand dependencies.
+- **agent_skills**: Cursor skill IDs relevant to this epic. Suggest using them when working on the task.
+
+## Recommended Workflow
+
+### 1. Triage / "What's Active?"
+Read [epics/INDEX.md](/home/realu/ki/boku/epics/INDEX.md). If you think it might be stale, run `just epics-plus index --dry-run` to see the current state without writing.
+
+### 2. Deep Dive / Context Gathering
+When assigned to an epic, open the file and:
+- Check `related_context` and `tags` to find relevant background docs.
+- Use `repos` to identify the target codebase and cross-reference with the `repo-map` skill.
+- Check `agent_skills` to see if specific domain expertise is required.
+
+### 3. Maintenance
+Regenerate the index after any frontmatter changes so the dashboard stays accurate.
+
+## Commands Reference
+
+Run from `/home/realu/ki/boku/epics`:
+
+- `just epics-plus index`: Writes `INDEX.md`.
+ - `--dry-run`: Print to stdout instead.
+ - `--all-statuses`: Include inactive/done/archived epics.
+- `just epics-plus scan`: List epics.
+ - `--only-with-frontmatter`: Skip files without Epics+ YAML.
+ - `--strict`: Exit non-zero if validation fails.
+ - `--format ndjson`: Useful for batch processing.
+
+## Additional Resources
+- Full schema and semantics: [epics/26-02-24-epics-plus.md](/home/realu/ki/boku/epics/26-02-24-epics-plus.md)
+- Index generation dashboard: [epics/INDEX.md](/home/realu/ki/boku/epics/INDEX.md)
diff --git a/ai-skills/epics-plus/reference.md b/ai-skills/epics-plus/reference.md
new file mode 100644
index 0000000..57c61ba
--- /dev/null
+++ b/ai-skills/epics-plus/reference.md
@@ -0,0 +1,31 @@
+# Epics+ Reference
+
+Detailed reference for the Epics+ system in boku.
+
+## Schema & Semantics
+The canonical source for the Epics+ v1 schema is:
+[epics/26-02-24-epics-plus.md](/home/realu/ki/boku/epics/26-02-24-epics-plus.md)
+
+### Key Field Meanings (Reader View)
+- **status**:
+ - `active`: In motion; expect to touch this again.
+ - `inactive`: Paused, but still relevant.
+ - `done`: Complete; useful as reference.
+ - `archived`: Intentionally out of circulation.
+- **priority**:
+ - `p0`: Urgent / drop-everything.
+ - `p1`: Important, should move soon.
+ - `p2`: Normal / default.
+ - `p3`: Nice-to-have / someday.
+
+## Index Generation (INDEX.md)
+The file [epics/INDEX.md](/home/realu/ki/boku/epics/INDEX.md) is a generated dashboard.
+
+- **Grouping**: Active epics are grouped by `priority` (p0 β p3).
+- **Links**: Links in the index use the `/epics/` prefix as per the project's standard.
+- **Regeneration**: If the file feels stale, run `cd /home/realu/ki/boku/epics && just epics-plus index`.
+
+## Relationship to Other Agents/Skills
+- **epics-frontmatter agent**: Used for *writing* frontmatter (adding/updating YAML).
+- **repo-map skill**: Use the `repos` field in an epic's frontmatter to find the target repository in the `repo-map` skill.
+- **agent_skills**: These fields in the frontmatter should be treated as suggestions for which skills to apply when working on the epic's tasks.
diff --git a/ai-skills/evm-rpc/SKILL.md b/ai-skills/evm-rpc/SKILL.md
new file mode 100644
index 0000000..d36c3c7
--- /dev/null
+++ b/ai-skills/evm-rpc/SKILL.md
@@ -0,0 +1,218 @@
+---
+name: evm-rpc
+description: Query EVM JSON-RPC methods for transaction debugging and endpoint
+ triage, including eth_getTransactionReceipt and debug_traceTransaction. Use
+ when the user asks to inspect tx status, retrieve receipts, run tracers,
+ compare EVM vs Cosmos outcomes, or choose between EVM RPC and Comet RPC
+ endpoints.
+---
+
+# EVM RPC: Receipt and Trace Playbook
+
+Use this skill for EVM transaction debugging with JSON-RPC.
+
+## Quick Start
+
+Set a hash and endpoint, then run receipt first and trace second.
+
+```bash
+evm_tx="0xYOUR_evm_tx"
+EVM_RPC="https://evm-rpc.archive.testnet-2.nibiru.fi"
+
+curl -s -X POST \
+ -H "Content-Type: application/json" \
+ --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$evm_tx\"],\"id\":1}" \
+ "$EVM_RPC" | jq
+
+curl -s -X POST \
+ -H "Content-Type: application/json" \
+ --data "{\"jsonrpc\":\"2.0\",\"method\":\"debug_traceTransaction\",\"params\":[\"$evm_tx\",{\"tracer\":\"callTracer\"}],\"id\":1}" \
+ "$EVM_RPC" | jq
+```
+
+## Endpoint Map - Nibiru Examples
+
+Chain-agnostic rule: methods determine endpoint family.
+
+| Purpose | Example endpoint | Methods |
+|---|---|---|
+| EVM JSON-RPC (mainnet) | `https://evm-rpc.nibiru.fi` | `eth_*`, `debug_*`, `net_*`, `web3_*` |
+| EVM JSON-RPC archive (mainnet) | `https://evm-rpc.archive.nibiru.fi` | historical reads, tracing |
+| EVM JSON-RPC archive (testnet) | `https://evm-rpc.archive.testnet-2.nibiru.fi` | testnet tracing and historical reads |
+| Comet RPC archive (mainnet) | `https://rpc.archive.nibiru.fi` | `/block_results`, `/consensus_params` |
+| Comet RPC archive (testnet) | `https://rpc.archive.testnet-2.nibiru.fi` | `/block_results`, `/consensus_params` |
+
+Rules:
+- Run `eth_*` and `debug_*` only on EVM RPC endpoints.
+- Run `/block_results` and `/consensus_params` only on Comet RPC endpoints.
+- Prefer archive endpoints for old heights/hashes and tracing workflows.
+
+## Basic Queries (Health / Identity)
+
+### web3_clientVersion
+
+Queries the traceability info like the client name, version, Git commit, Go version,
+runtime architecture, and build tags.
+
+```bash
+curl -X POST https://evm-rpc.nibiru.fi \
+ -H "Content-Type: application/json" \
+ -d '{
+ "jsonrpc": "2.0",
+ "method": "web3_clientVersion",
+ "params": [],
+ "id": 1
+ }'
+```
+
+The response looks like this: "Nibiru 2.6.0: Compiled at Git commit 3cf97e3468f8ce922c8760f839f8f336ca0c37f4 using Go go1.24.5, arch amd64, and build tags (netgo osusergo ledger static rocksdb pebbledb muslc)"
+
+### eth_chainId
+
+Queries the EIP-155 replay-protection chain id for the current ethereum blockchain (Nibiru).
+
+```bash
+curl -X POST https://evm-rpc.nibiru.fi \
+ -H "Content-Type: application/json" \
+ -d '{
+ "jsonrpc": "2.0",
+ "method": "eth_chainId",
+ "params": [],
+ "id": 1
+ }'
+```
+
+## Transaction Triage Workflow
+
+Use this sequence to avoid false conclusions:
+
+1. `eth_getTransactionByHash`
+ - Confirms the tx exists and gives gas fields, type, input, and block refs.
+2. `eth_getTransactionReceipt`
+ - Authoritative EVM execution result.
+3. `debug_traceTransaction` (usually `callTracer`)
+ - Explains internal call path and where execution diverged.
+4. Optional cross-layer checks on Comet RPC
+ - `block_results?height=...`, `consensus_params?height=...`.
+
+## Concrete Command Cookbook
+
+### 1 - Get Transaction by Hash
+
+```bash
+evm_tx="0xYOUR_evm_tx"
+EVM_RPC="https://evm-rpc.archive.testnet-2.nibiru.fi"
+
+curl -s -X POST \
+ -H "Content-Type: application/json" \
+ --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionByHash\",\"params\":[\"$evm_tx\"],\"id\":1}" \
+ "$EVM_RPC" | jq
+```
+
+### 2 - Get Transaction Receipt - Authoritative Status
+
+```bash
+evm_tx="0xYOUR_evm_tx"
+EVM_RPC="https://evm-rpc.archive.testnet-2.nibiru.fi"
+
+curl -s -X POST \
+ -H "Content-Type: application/json" \
+ --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$evm_tx\"],\"id\":1}" \
+ "$EVM_RPC" | jq
+```
+
+Interpretation:
+- `result == null`: **Not mined** or rejected. Either the tx is still in the mempool, it was dropped, or it failed consensus/validation (e.g., bad nonce, invalid signature, insufficient funds for gas). It is not in any block.
+- `status == "0x1"`: **Full Success**. The transaction was executed successfully, and all state changes were applied.
+- `status == "0x0"`: **Execution Failure (Revert)**. The transaction was successfully mined and included in a block (consuming gas), but the EVM execution was reverted. No state changes occurred except for the gas fee deduction from the sender.
+- `blockNumber` present: Mined/included in a block.
+- `logs` present: Events emitted (decode by `address`, `topics`, `data`). Note that a reverted tx (`0x0`) will generally have an empty `logs` array, even if it attempted to emit them before reverting.
+
+### 3 - Run Debug Tracer - Call Tree View
+
+```bash
+evm_tx="0xYOUR_evm_tx"
+EVM_RPC="https://evm-rpc.archive.testnet-2.nibiru.fi"
+
+curl -s -X POST \
+ -H "Content-Type: application/json" \
+ --data "{\"jsonrpc\":\"2.0\",\"method\":\"debug_traceTransaction\",\"params\":[\"$evm_tx\",{\"tracer\":\"callTracer\"}],\"id\":1}" \
+ "$EVM_RPC" | jq
+```
+
+Interpretation:
+- Inspect top-level `from`, `to`, `gas`, `gasUsed`.
+- Walk `calls[]` for internal call chain and outputs.
+- Use trace to explain behavior; use receipt status to decide success/failure.
+
+### 4 - Cross-Layer Checks with Comet RPC
+
+```bash
+HEIGHT="5959008"
+TM_RPC="https://rpc.archive.testnet-2.nibiru.fi"
+
+curl -s "$TM_RPC/block_results?height=$HEIGHT" | jq
+curl -s "$TM_RPC/consensus_params?height=$HEIGHT" | jq
+```
+
+Use when EVM and Cosmos observations appear inconsistent.
+
+## Receipt and Trace Rules of Thumb
+
+- **Receipt status is the authoritative EVM truth** for success vs. failure.
+- **Mined does not mean successful**: A transaction can be in a block and consume gas (`0x0`), but have zero effect on state. Always check the `status` field.
+- **Null receipt implies non-existence**: If `eth_getTransactionReceipt` returns `null`, the transaction was never mined. This is usually due to a consensus/mempool error (invalid signature, wrong chain ID, bad nonce, or insufficient funds to cover the max gas cost).
+- **Consensus failures don't have receipts**: Errors like "insufficient funds" or "bad nonce" prevent a transaction from even entering a block, so no EVM status is ever generated.
+- **A non-empty `logs` array** (at the receipt level) generally only exists for successful transactions (`0x1`).
+- **Gas consumption**: Even in failure (`0x0`), the `gasUsed` is still charged to the sender up to the point of reversion.
+- **Gas Comparison**: Compare receipt `gasUsed` with the `gas` (limit) from `eth_getTransactionByHash` to see if the transaction ran out of gas.
+- **Tracing Failures**: If tracing fails, verify the provider supports the `debug` namespace and retry on an archive endpoint.
+
+## Endpoint Selection Guide
+
+Choose endpoint by method and historical depth:
+
+- Latest, simple reads (`eth_blockNumber`, recent receipt):
+ - Standard EVM RPC is usually enough.
+- Historical receipts, old block state, trace/debug:
+ - Prefer archive EVM RPC.
+- Tendermint/Comet block metadata and app-level event correlation:
+ - Use Comet RPC endpoints.
+
+## Common Pitfalls
+
+- Calling Comet paths on EVM RPC host (or the reverse).
+- Using the wrong network endpoint for a valid tx hash.
+- Assuming `result == null` always means pending; it may be wrong chain/provider.
+- Treating trace output alone as final status without reading receipt.
+- Ignoring timeout/retry behavior for freshly broadcast transactions.
+
+## Reusable Triage Template
+
+```bash
+evm_tx="0xYOUR_evm_tx"
+EVM_RPC="https://evm-rpc.archive.testnet-2.nibiru.fi"
+TM_RPC="https://rpc.archive.testnet-2.nibiru.fi"
+
+# 1) tx details
+curl -s -X POST -H "Content-Type: application/json" \
+ --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionByHash\",\"params\":[\"$evm_tx\"],\"id\":1}" \
+ "$EVM_RPC" | jq
+
+# 2) receipt status
+curl -s -X POST -H "Content-Type: application/json" \
+ --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$evm_tx\"],\"id\":1}" \
+ "$EVM_RPC" | jq
+
+# 3) call trace
+curl -s -X POST -H "Content-Type: application/json" \
+ --data "{\"jsonrpc\":\"2.0\",\"method\":\"debug_traceTransaction\",\"params\":[\"$evm_tx\",{\"tracer\":\"callTracer\"}],\"id\":1}" \
+ "$EVM_RPC" | jq
+```
+
+## Additional Resources
+
+- Zero-gas debug walkthrough:
+ `/home/realu/ki/boku/epics/epic-evm/26-02-zero-gas/26-02-10-zero-gas-debug.md`
+- Existing endpoint conventions:
+ `/home/realu/ki/boku/nibi/cook/index.ts`
diff --git a/ai-skills/gh-pr/SKILL.md b/ai-skills/gh-pr/SKILL.md
new file mode 100644
index 0000000..7ebc2d1
--- /dev/null
+++ b/ai-skills/gh-pr/SKILL.md
@@ -0,0 +1,54 @@
+---
+name: gh-pr
+description: Writes clear pull request descriptions from the current branch diff against a base branch. Defaults to `main`, but supports `dev` or any other user-specified comparison branch. Use when the user asks for a PR description, pull request summary, or a markdown write-up for changes against a base branch.
+---
+
+# GH PR
+
+## Purpose
+
+Create a clear, logical, and concise pull request description for the current
+branch diff against a base branch.
+
+## Instructions
+
+When using this skill:
+
+1. Determine the comparison branch first:
+ - Use the branch the user specifies when they name one.
+ - Otherwise, default to `main`.
+ - If the correct base branch is unclear, ask the user before drafting the
+ pull request description.
+ - If the repo clearly uses another default integration branch, such as
+ `dev`, use that instead of assuming `main`.
+2. Write a pull request description that explains the rationale behind the
+ changes, not just the file-by-file edits.
+3. Keep the writing concise and non-repetitive.
+4. Save the final output to a markdown file named `pull-request.md`.
+
+## Output format
+
+Use this structure:
+
+```markdown
+#
+
+
+
+## Key Changes
+
+-
+-
+
+## Appendix
+
+
+```
+
+## Style
+
+- Prefer clarity over cleverness.
+- Do not repeat the same point in multiple sections.
+- Explain rationale wherever it is useful.
+- Keep the description easy to scan.
diff --git a/ai-skills/indexer-staking/SKILL.md b/ai-skills/indexer-staking/SKILL.md
new file mode 100644
index 0000000..524195d
--- /dev/null
+++ b/ai-skills/indexer-staking/SKILL.md
@@ -0,0 +1,60 @@
+---
+name: indexer-staking
+description: Use when the user asks about the Nibiru indexer (Heart Monitor), staking GraphQL schema, GQLValidator, GQLDelegation, HeartMonitor, or querying staking data via GraphQL. Explains that staking is under query.staking, not root-level deprecated fields; rewards come from the chain (distribution), not the indexer; amounts are Int in base units (e.g. unibi).
+---
+
+# Nibiru Indexer (Heart Monitor) β Staking
+
+Use this skill when working with the **Nibiru indexer / Heart Monitor** GraphQL API or the **staking** part of the schema (validators, delegations, redelegations, unbondings, history).
+
+## Quick Facts
+
+- **Indexer** = Heart Monitor = GraphQL API. **Endpoints** (path `/query`): Mainnet `https://hm-graphql.nibiru.fi/query`; testnet `https://hm-graphql.itn-2.nibiru.fi/query`. **Env** is the hostname segment for the network: mainnet has no env (host `hm-graphql.nibiru.fi`); testnet uses env `itn-2` (host `hm-graphql.itn-2.nibiru.fi`). The playground uses the same host with path `/graphql`.
+- **Canonical Path**: Always use `query { staking { ... } }`. Root-level fields are **deprecated**.
+- **Bond denom**: The staking token denomination is **unibi** β shorthand for **micro NIBI** (ΞΌ NIBI), where "u" comes from "ΞΌ" (mu).
+- **Amounts**: Stored as **Int** in base units (unibi). Divide by 10^6 for `NIBI`.
+- **Shares**: A delegator's proportional claim on a validator's pool; the shares-to-tokens rate changes with slashing (see Shares vs. Amount below).
+- **Valoper**: Validator operator address (`nibivaloper1...`). Validators have three Bech32 identities: account `nibi1...`, operator (valoper), and consensus `nibivalcons1...`; use `nibid debug addr` to convert. Indexer/CLI staking use valoper.
+- **Rewards**: **Not in the indexer**. Use the chainβs distribution module via the Nibiru CLI: `nibid query distribution` (e.g. `rewards`) and `nibid tx distribution`.
+- **TS SDK**: Use `HeartMonitor` from `@nibiruchain/nibijs`.
+
+## Dependency Flow & Lineage
+
+Data flows from the chain to the client in a specific pipeline. Understanding this prevents schema confusion:
+
+1. **Chain State (Go)**: Source of truth. Staking module (`internal/cosmos-sdk`) stores **Shares** for delegations.
+2. **Indexer Sync**: The `nibi-go-hm` server queries the chain, calculates the token **Balance**, and persists it to the indexer DB.
+3. **GraphQL API**: The API (`staking.graphqls`) exposes these DB fields. This is why you see `amount` (tokens) but usually not `shares` in indexer delegations.
+4. **TS SDK**: The client (`nibi-ts-sdk`) generates types and query builders from the GraphQL schema.
+
+## Shares vs. Amount (Critical)
+
+**Shares** are a delegator's proportional claim on a validator's pool; the shares-to-tokens rate changes with slashing.
+
+- **Indexer `Delegation.amount`** is a **balance snapshot** in tokens. It does not track shares.
+- **Slashing/Compensation**: Here **compensation** means reimbursing delegators after slashing or validator misbehavior (distinct from **commission**, the validator's share of rewards). You **cannot** derive shares from the indexer amount after a slash. For reproducible compensation math, you **MUST** query the chain directly (e.g., `nibid q staking delegations-to`) using a specific `--height`.
+
+## Rule of Thumb: Where to Query?
+
+| Requirement | Preferred Source | Reason |
+| :--- | :--- | :--- |
+| Current delegator balances | **Indexer** | Fast, indexed, GraphQL support. |
+| Historical staking actions | **Indexer** | `staking { history }` tracks events over time. |
+| **Rewards** | **Chain** | Indexer does not track distribution module data. |
+| **Compensation / Slashing** | **Chain** | Requires `shares` and height-specific snapshots. |
+| Validator uptime/jailed status | **Either** | Indexer is fine unless absolute real-time is needed. |
+
+## Additional Resources
+
+- **[Full Reference (reference.md)](reference.md)**:
+ - [Canonical Query Shape & Deprecation](reference.md#3-graphql-root-and-the-staking-container)
+ - [TS SDK Internals & Query Builder](reference.md#2-heart-monitor-abstraction-in-nibi-ts-sdk)
+ - [Pagination & 1000-Row Limits](reference.md#6-pagination--limits)
+ - [Lineage & Chain Types (Go)](reference.md#7-lineage--dependency-flow-go--indexer--ts)
+ - [CLI Mapping (nibid β Indexer)](reference.md#8-cli-mapping-nibid--indexer)
+ - [Compensation & Migration Gotchas](reference.md#10-gotchas--caveats)
+ - [GraphQL Schema Types](reference.md#9-graphql-schema-types-heart-monitor)
+ - [User type definition](reference.md#user)
+ - [Order enums (ValidatorOrder, DelegationOrder, etc.)](reference.md#order-enums-sort-keys)
+ - [Example: list delegations for address X](reference.md#example-list-delegations-by-address)
+ - [Upstream Repo File Pointers](reference.md#12-file-reference-by-repo)
diff --git a/ai-skills/indexer-staking/reference.md b/ai-skills/indexer-staking/reference.md
new file mode 100644
index 0000000..9bcd21e
--- /dev/null
+++ b/ai-skills/indexer-staking/reference.md
@@ -0,0 +1,417 @@
+# Nibiru Indexer (Heart Monitor) β Staking Schema Reference
+
+This document describes the Nibiru indexer (Heart Monitor) and the staking portion of its GraphQL schema: what the indexer is, how the TypeScript SDK exposes it, the lineage of data from the chain, and the types/fields available for staking queries.
+
+- [Repo Definitions](#repo-definitions)
+- [1. What the indexer is and how itΞΓΓs exposed](#1-what-the-indexer-is-and-how-its-exposed)
+- [2. Heart Monitor abstraction in nibi-ts-sdk](#2-heart-monitor-abstraction-in-nibi-ts-sdk)
+ - [Import and Construction](#import-and-construction)
+ - [Method Signature (IHeartMonitor)](#method-signature-iheartmonitor)
+ - [Query Builder Pattern](#query-builder-pattern)
+ - [Batching](#batching)
+- [3. GraphQL Root and the Staking Container](#3-graphql-root-and-the-staking-container)
+- [4. Field Discovery](#4-field-discovery)
+ - [Example: List delegations by address](#example-list-delegations-by-address)
+- [5. Staking type: sub-queries and arguments](#5-staking-type-sub-queries-and-arguments)
+ - [Filters (Summary)](#filters-summary)
+ - [Order enums (sort keys)](#order-enums-sort-keys)
+- [6. Pagination & Limits](#6-pagination--limits)
+- [7. Lineage & Dependency Flow (Go ΞΓ₯Γ Indexer ΞΓ₯Γ TS)](#7-lineage--dependency-flow-go-%E2%86%92-indexer-%E2%86%92-ts)
+ - [The Lineage](#the-lineage)
+ - [Key Chain Types (internal/cosmos-sdk)](#key-chain-types-internalcosmos-sdk)
+ - [Key Indexer Types (nibi-go-hm models)](#key-indexer-types-nibi-go-hm-models)
+- [8. CLI Mapping (nibid ΞΓ₯ΓΆ Indexer)](#8-cli-mapping-nibid-%E2%86%94-indexer)
+ - [Validator addresses (valoper)](#validator-addresses-valoper)
+- [9. GraphQL Schema Types (Heart Monitor)](#9-graphql-schema-types-heart-monitor)
+ - [Validator](#validator)
+ - [Delegation](#delegation)
+ - [Redelegation](#redelegation)
+ - [Unbonding](#unbonding)
+ - [User](#user)
+ - [StakingHistoryItem](#stakinghistoryitem)
+ - [ValidatorDescription](#validatordescription)
+ - [Block](#block)
+- [10. Gotchas & Caveats](#10-gotchas--caveats)
+ - [Compensation & Slashing](#compensation--slashing)
+ - [Migration](#migration)
+ - [General](#general)
+- [11. Future Skills Roadmap](#11-future-skills-roadmap)
+- [12. File Reference (By Repo)](#12-file-reference-by-repo)
+ - [Client (`nibi-ts-sdk`)](#client-nibi-ts-sdk)
+ - [Server (`nibi-go-hm`)](#server-nibi-go-hm)
+
+## Repo Definitions
+
+For repo locations and cross-repo routing (nibi-go-hm, nibi-ts-sdk, nibi-chain,
+etc.), see the **repo-map** skill. It maps which repo to use for indexer schema
+changes, SDK client code, chain logic, and how they connect.
+
+---
+
+## 1. What the indexer is and how itβs exposed
+
+- **Heart Monitor** (or βhmβ) is the server that indexes the blockchain and exposes that data over **GraphQL**.
+- **Implementation**: The server is implemented in the `nibi-go-hm` repo.
+- **Endpoint**: Clients send POST requests to the GraphQL endpoint at path `/query`. **Production URLs**: Mainnet `https://hm-graphql.nibiru.fi/query`; testnet `https://hm-graphql.itn-2.nibiru.fi/query`. **Env** is the hostname segment that identifies the network: omit it for mainnet; use `itn-2` for testnet. Generic form: `https://hm-graphql..nibiru.fi/query` (for mainnet, use `hm-graphql.nibiru.fi` with no `.` segment). The playground is at `/graphql` on the same host.
+- **Scope**: Staking (validators, delegations, redelegations, unbondings, history) is one module. The same API serves governance, oracle, inflation, user balances, IBC, wasm, and more.
+- **Rewards**: **Rewards for a delegator are NOT from the indexer**; they come from the chainβs distribution module. Use the Nibiru CLI: `nibid query distribution` (e.g. `rewards`) and `nibid tx distribution`.
+
+---
+
+## 2. Heart Monitor abstraction in nibi-ts-sdk
+
+The TypeScript SDK (`@nibiruchain/nibijs` from the `nibi-ts-sdk` repo) provides a typed client for the Heart Monitor API.
+
+### Import and Construction
+```typescript
+import { HeartMonitor } from "@nibiruchain/nibijs"
+
+const hm = new HeartMonitor("https://hm-graphql.devnet-2.nibiru.fi/query")
+// Or omit to use default endpoint
+```
+
+### Method Signature (IHeartMonitor)
+```typescript
+readonly staking: (
+ args: QueryStakingArgs,
+ fields: DeepPartial
+) => Promise
+```
+- **args**: Filters, order, limit, offset per sub-query (delegations, validators, history, etc.). Pass `{}` or omit for defaults.
+- **fields**: Object shape specifying which sub-queries and nested fields to request. Keys like `delegations`, `validators`; nested values describe the selection set.
+- **Return**: `GqlOutStaking` with shape `{ staking?: { delegations?, validators?, ... } }`.
+
+### Query Builder Pattern
+The SDK turns `args` + `fields` into a GraphQL document via:
+- `QueryStakingArgs`: Per-sub-query arguments.
+- `GQLStakingFields`: Selection set for the sub-queries.
+- `convertObjectToPropertiesString()`: Recursively converts the `fields` object to a GraphQL selection string.
+- `gqlQuery()`: Builds `fieldName(args) { selection }`.
+- `doGqlQuery()`: POSTs the query to the endpoint.
+
+### Batching
+The SDK exposes `GQLQueryGqlBatchHandler`: given an array of query strings, it merges them into one document `{ query1 query2 ... }` and runs a single POST. Staking can be requested alone or as part of a larger batch with other root fields (communityPool, inflation, proxies, etc.).
+
+---
+
+## 3. GraphQL Root and the Staking Container
+
+Root type `Query` has a field **`staking: Staking!`**. Use the **container pattern**:
+
+```graphql
+query {
+ staking {
+ validators(...) { ... }
+ delegations(...) { ... }
+ redelegations(...) { ... }
+ unbondings(...) { ... }
+ history(...) { ... }
+ }
+}
+```
+
+**CRITICAL**: Root-level fields like `validators`, `delegations`, `unbondings`, `redelegations` exist on `Query` but are **deprecated** ("Moved to staking sub schema"). Always use `query { staking { ... } }`.
+
+---
+
+## 4. Field Discovery
+
+- **Schema as source of truth**: Read `.graphqls` files in `nibi-go-hm/graphql/graph/`. They define types, filters, enums, and arguments.
+- **Generated TS types**: `nibi-ts-sdk/src/gql/utils/generated.ts` contains `GQLValidator`, `GQLDelegation`, etc. that mirror the schema.
+- **Introspection**: You can use GraphQL introspection (`__schema`) or `featureFlags` to check module availability at runtime.
+
+### Example: List delegations by address
+To list delegations for a given delegator address:
+
+```graphql
+query {
+ staking {
+ delegations(where: { delegator_address: "nibi1..." }, limit: 100) {
+ amount
+ delegator { address }
+ validator { operator_address }
+ }
+ }
+}
+```
+
+Equivalent `hm.staking()` call (TS SDK):
+
+```typescript
+const resp = await hm.staking(
+ { delegations: { where: { delegator_address: "nibi1..." }, limit: 100 } },
+ { delegations: { amount: true, delegator: { address: true }, validator: { operator_address: true } } }
+)
+// resp.staking?.delegations
+```
+
+---
+
+## 5. Staking type: sub-queries and arguments
+
+The **`Staking`** type is a container for five sub-queries.
+
+| Field | Description and signature |
+|-------|----------------------------|
+| `validators` | `(limit, offset, order_by: ValidatorOrder, order_desc, where: ValidatorFilter): [Validator!]!` β list of validators with optional filter/sort. |
+| `delegations` | `(limit, offset, order_by: DelegationOrder, order_desc, where: DelegationFilter): [Delegation!]!` β delegations; e.g. filter by `delegator_address` or `validator_address`. |
+| `redelegations` | `(limit, offset, order_by: RedelegationOrder, order_desc, where: RedelegationFilter): [Redelegation!]!` β in-flight redelegations. |
+| `unbondings` | `(limit, offset, order_by: UnbondingOrder, order_desc, where: UnbondingFilter): [Unbonding!]!` β in-flight unbondings. |
+| `history` | `(limit, offset, order_by: StakingHistoryOrder, order_desc, where: StakingHistoryFilter): [StakingHistoryItem!]!` β staking history (delegate, unbond, redelegate, withdraw, cancel, etc.). |
+
+### Filters (Summary)
+- **DelegationFilter**: `delegator_address`, `validator_address`.
+- **ValidatorFilter**: `jailed`, `moniker`, `operator_address`, `status`.
+- **RedelegationFilter / UnbondingFilter**: delegator and validator addresses.
+- **StakingHistoryFilter**: `delegator`, `validator`, `actions`, `amount`, `block`, `completionTime`.
+- **Common Filters**: See `common.graphqls` for `IntFilter`, `TimeFilter`, `StringFilter` (contains, eq, in, etc.).
+
+### Order enums (sort keys)
+Use `order_by` and `order_desc` on sub-queries to sort results. Values:
+
+- **ValidatorOrder**: `operator_address`, `jailed`, `status`, `moniker`, `tokens`
+- **DelegationOrder**: `validator_address`, `delegator_address`
+- **UnbondingOrder**: `validator_address`, `delegator_address`, `creation_height`, `completion_time`
+- **RedelegationOrder**: `source_validator_address`, `destination_validator_address`, `delegator_address`, `creation_height`, `completion_time`
+- **StakingHistoryOrder**: `sequence`, `validator`, `delegator`, `amount`, `action`
+
+Canonical definitions: `nibi-go-hm/graphql/graph/staking.graphqls`. TS SDK: `GQLValidatorOrder`, `GQLDelegationOrder`, etc. in `generated.ts`.
+
+---
+
+## 6. Pagination & Limits
+
+In the server (`nibi-go-hm/graphql/graph/db/utils.go`):
+- **`DEFAULT_LIMIT`**: 1000
+- **`MAX_LIMIT`**: 1000
+
+**Rules**:
+- Requests without `limit` get 1000 rows.
+- `limit` values greater than 1000 are clamped to 1000.
+- **Strategy**: Use `offset` to page (0, 1000, 2000, β¦) until a page returns fewer rows than requested.
+
+---
+
+## 7. Lineage & Dependency Flow (Go β Indexer β TS)
+
+**Shares**: A delegator's proportional claim on a validator's bonded pool. The chain stores delegation in shares; the sharesβtokens rate changes with slashing, so token amount cannot be converted back to shares after the fact.
+
+Understanding where data comes from helps reason about schema differences (e.g., why the indexer has `amount` but not `shares`).
+
+### The Lineage
+1. **Chain State (Go)**: The Cosmos SDK `staking` module defines the canonical shape. `Delegation` in state only stores `Shares`.
+2. **Indexer Sync**: `nibi-go-hm` consumes chain data (via `ValidatorDelegations` β `DelegationResponse`). It writes to the **database** using types derived from Go definitions.
+3. **Indexer Database**: The indexer stores the token `Balance.Amount` at sync time.
+4. **GraphQL Schema**: Reflects the DB types (e.g., `Delegation.amount` is an `Int`).
+5. **TS SDK**: Generates types and query builders from the GraphQL schema.
+
+### Key Chain Types (internal/cosmos-sdk)
+```go
+// Delegation in state (shares only)
+type Delegation struct {
+ DelegatorAddress string
+ ValidatorAddress string
+ Shares math.Dec // used in compensation formula
+}
+
+// DelegationResponse (what queries return)
+type DelegationResponse struct {
+ Delegation Delegation
+ Balance types.Coin // amount = val.TokensFromShares(delegation.Shares)
+}
+
+// Validator
+type Validator struct {
+ OperatorAddress string
+ Tokens math.Int // total tokens
+ DelegatorShares math.Dec // denominator for compensation
+ Jailed bool
+ Status BondStatus
+}
+```
+
+### Key Indexer Types (nibi-go-hm models)
+- **Delegation**: Has `ValidatorAddress`, `DelegatorAddress`, `Amount` (int64). **No shares.** `Amount` is populated from `DelegationResponse.Balance.Amount` at index time.
+- **Validator**: Has `OperatorAddress`, `Jailed`, `Status`, `Tokens`, `DelegatorShares` (float64).
+
+---
+
+## 8. CLI Mapping (nibid β Indexer)
+
+| CLI command (`nibid q staking ...`) | Chain Query | Indexer Equivalent |
+|------------------------------------|-------------|--------------------|
+| `validator ` | `Validator` | `staking { validators(where: { operator_address }) }` |
+| `delegations-to ` | `ValidatorDelegations` | `staking { delegations(where: { validator_address }) }` |
+| `delegations ` | `DelegatorDelegations` | `staking { delegations(where: { delegator_address }) }` |
+| `delegation ` | `Delegation` (single) | Filter `delegations(where: { delegator_address, validator_address })` |
+| `redelegations` | `Redelegations` | `staking { redelegations(...) }` |
+| `unbonding-delegations` | `UnbondingDelegations` | `staking { unbondings(...) }` |
+
+**Pagination Comparison**:
+- **Chain**: `--limit` (default 100), `--page-key`.
+- **Indexer**: `limit` (max 1000), `offset` (0/1000/2000).
+
+### Validator addresses (valoper)
+
+**Valoper** is the validator operator address (Bech32 prefix `nibivaloper`, e.g. `nibivaloper1...`). It is used in staking queries and indexer filters (`operator_address`, `validator_address`).
+
+Validators have **three** valid Bech32 address types for the same logical validator:
+
+- **Account** β `nibi1...` β The validator's wallet/account address.
+- **Operator (valoper)** β `nibivaloper1...` β Used for staking identity, indexer filters, and CLI (e.g. `nibid q staking validator `).
+- **Consensus** β `nibivalcons1...` β Used in consensus messages, block signing, and oracle/slashing logs.
+
+Use `nibid debug addr ` to convert between forms; it shows Bech32 Acc, Val, and Con for any of the three. See e.g. validator incident/slashing docs for examples of all three per validator (e.g. Cogwheel, Roomit).
+
+---
+
+## 9. GraphQL Schema Types (Heart Monitor)
+
+This section describes the **GraphQL types** defined in the Heart Monitor schema (primarily in `staking.graphqls`). These types represent the indexed blockchain data available for query. While they mirror the underlying database models and chain state, they are optimized for client consumption (e.g., using token `amount` instead of internal `shares`).
+
+### Validator
+Represents the state and metadata of a node operator. This includes their bonded status, voting power (tokens), commission rates, and uptime.
+
+- `operator_address`: The validator operator address (valoper; see Validator addresses in Β§8) (String).
+- `tokens`: Total tokens bonded to the validator in base units, e.g., `unibi` (Int).
+- `status`: The validator's bonding status (Enum: `BONDED`, `UNBONDED`, `UNBONDING`).
+- `description`: Metadata including moniker, website, and identity (`ValidatorDescription`).
+- `commission_rates`: The current, max, and max change rates for commissions (`ValidatorCommission`). **ValidatorCommission**: `rate` (current commission), `max_rate` (ceiling), `max_change_rate` (max change per update).
+- `commission_update_time`: Timestamp of the last commission change (String).
+- `delegator_shares`: The total shares issued to delegators (Float). Used as the denominator in compensation math.
+- `jailed`: Whether the validator is currently excluded from the consensus set (Boolean).
+- `uptime`: The percentage of blocks signed in the recent window (Optional Float).
+- `self_delegation`: The amount of tokens the operator has delegated to themselves (Int).
+- `min_self_delegation`: The minimum tokens required for the validator to remain active (Int).
+- `unbonding_time`: The time at which the validator finishes unbonding (String).
+- `unbonding_block`: The block height at which the validator finishes unbonding (`Block`).
+- `creation_block`: The block height at which the validator was created (`Block`).
+
+### Delegation
+Represents the relationship and token balance between a delegator and a specific validator.
+
+- `amount`: The current token balance of the delegation in base units (Int). Derived from shares at index time.
+- `delegator`: The account holding the delegation (`User`).
+- `validator`: The validator receiving the delegation (`Validator`).
+
+### Redelegation
+Represents tokens in the process of being moved from one validator to another.
+
+- `amount`: The amount of tokens being redelegated (Int).
+- `completion_time`: When the redelegation will finish (String).
+- `creation_block`: The block height where the redelegation started (`Block`).
+- `delegator`: The account performing the redelegation (`User`).
+- `source_validator`: The validator tokens are moving from (`Validator`).
+- `destination_validator`: The validator tokens are moving to (`Validator`).
+
+### Unbonding
+Represents tokens in the process of being withdrawn from a validator.
+
+- `amount`: The amount of tokens being unbonded (Int).
+- `completion_time`: When the tokens will become liquid (String).
+- `creation_block`: The block height where the unbonding started (`Block`).
+- `delegator`: The account unbonding tokens (`User`).
+- `validator`: The validator tokens are being removed from (`Validator`).
+
+### User
+Represents a Nibiru account/address and its associated indexer-tracked metadata.
+
+```graphql
+type User @goModel(model: "heartmonitor/graphql/graph/model.User") {
+ address: String!
+ balances: [Token]!
+ created_block: Block!
+ is_blocked: Boolean
+}
+```
+
+- `address`: The Bech32 account address (`nibi1...`).
+- `balances`: List of token balances for the user.
+- `created_block`: The block height where the account was first seen by the indexer.
+- `is_blocked`: Whether the account is currently blocked in the indexer.
+
+### StakingHistoryItem
+A record of a discrete staking event. Useful for auditing user activity over time.
+
+- `action`: The type of staking event (Enum: `delegate`, `unbond`, `redelegate`, `withdraw`, `cancel`, etc.).
+- `amount`: The token amount associated with the action (Int).
+- `block`: The block height and timestamp of the event (`Block`).
+- `completion_time`: The expected completion time for delayed actions (Optional Time).
+- `delegator`: The account that initiated the action (`User`).
+- `validator`: The primary validator involved in the action (`Validator`).
+- `destination_validator`: The target validator for `redelegate` actions (Optional `Validator`).
+
+### ValidatorDescription
+Human-readable metadata for a validator.
+
+- `moniker`: The validator's display name (String).
+- `website`: Official website URL (String).
+- `details`: Additional information or bio (String).
+- `identity`: Keybase or other identity signature (String).
+- `security_contact`: Email or contact info for security issues (String).
+
+### Block
+Common metadata for a specific block height.
+
+- `block`: The block height (Int).
+- `block_ts`: The UNIX timestamp of the block (Int).
+- `block_duration`: Time taken to produce the block (Float).
+- `num_txs`: Number of transactions in the block (Int).
+
+**Units**: All token amounts (`amount`, `tokens`, `self_delegation`, etc.) are returned as **Int** in base units (e.g., `unibi`). To display as `NIBI`, divide by $10^6$.
+
+---
+
+## 10. Gotchas & Caveats
+
+### Compensation & Slashing
+**Compensation** here means reimbursing delegators after slashing or validator misbehavior β not to be confused with **commission** (the validator's share of rewards).
+
+- **Shares required**: Compensation formulas need `delegation.shares` and `validator.delegator_shares`.
+- **Indexer Limit**: The indexer has `amount` (balance), not shares. You cannot derive shares from amount because the exchange rate changes with slashing.
+- **Snapshot accuracy**: Indexer can lag chain state. For reproducible compensation at a specific slash height, use chain queries with `--height`.
+- **Bond Denom**: The staking token denomination is **unibi** β micro NIBI (ΞΌ NIBI), where "u" comes from "ΞΌ" (mu). Check `nibid query staking params` to confirm on-chain.
+
+### Migration
+- **Blocked entries**: In-flight `redelegations` and `unbondings` can block forced migrations.
+- **Filtering**: Use `Validator.jailed` and `delegations(where: { validator_address })` to identify affected delegators.
+
+### General
+- **Use the container**: Only use `query { staking { ... } }`.
+- **Lag**: The indexer indexes blocks asynchronously. Data may be a few seconds behind the chain.
+
+---
+
+## 11. Future Skills Roadmap
+
+Other API areas follow the same pattern (Container β Sub-queries β Types).
+
+| Module | Schema File | Focus Area |
+|--------|-------------|------------|
+| **governance** | `governance.graphqls` | Proposals, deposits, votes |
+| **oracle** | `oracle.graphqls` | Prices, entries |
+| **wasm** | `wasm.graphqls` | Contracts, code, instances |
+| **user** | `user.graphqls` | Balances, messages |
+| **inflation** | `inflation.graphqls` | Inflation data |
+| **distribution**| `distribution.graphqls`| Commissions |
+
+---
+
+## 12. File Reference (By Repo)
+
+### Client (`nibi-ts-sdk`)
+- `src/gql/heart-monitor/heart-monitor.ts`: `HeartMonitor` class and interfaces.
+- `src/gql/query/staking.ts`: Staking query builder and types (`QueryStakingArgs`, `GQLStakingFields`).
+- `src/gql/utils/consts.ts`: Query utilities (`doGqlQuery`, `gqlQuery`, `convertObjectToPropertiesString`).
+- `src/gql/utils/schema.graphql`: Local copy of the schema.
+- `src/gql/utils/generated.ts`: Generated types (GQLValidator, etc.).
+
+### Server (`nibi-go-hm`)
+- `graphql/graph/query.graphqls`: Root Query definition.
+- `graphql/graph/staking.graphqls`: Staking-specific types and filters.
+- `graphql/graph/common.graphqls`: Common filters (IntFilter, etc.).
+- `graphql/graph/resolver/staking.resolvers.go`: Staking resolvers.
+- `graphql/graph/db/staking.go`: DB queries for staking.
+- `graphql/graph/db/utils.go`: `DEFAULT_LIMIT`, `MAX_LIMIT`.
+
+---
diff --git a/ai-skills/indexer-user-balances/SKILL.md b/ai-skills/indexer-user-balances/SKILL.md
new file mode 100644
index 0000000..c92030b
--- /dev/null
+++ b/ai-skills/indexer-user-balances/SKILL.md
@@ -0,0 +1,66 @@
+---
+name: indexer-user-balances
+description: Queries the Nibiru indexer (Heart Monitor) for user account and balance data. Use when querying user balances by address, the user/users root fields, Token amounts, or HeartMonitor.user/users in the TS SDK.
+---
+
+# Nibiru Indexer (Heart Monitor) β User Balances
+
+Use this skill when working with the **Nibiru indexer / Heart Monitor** GraphQL API for **user** and **users** queries and **Token** balances (account balances by address).
+
+## Quick Facts
+
+- **Indexer** = Heart Monitor = GraphQL API. **Endpoints** (path `/query`): Mainnet `https://hm-graphql.nibiru.fi/query`; testnet `https://hm-graphql.itn-2.nibiru.fi/query`. **Env** is the hostname segment for the network: mainnet has no env (host `hm-graphql.nibiru.fi`); testnet uses env `itn-2` (host `hm-graphql.itn-2.nibiru.fi`). The playground is at `/graphql` on the same host.
+- **No container**: Unlike staking (`query { staking { ... } }`), **user** and **users** are **root-level** fields on `Query`. Use `user(where: { address: "nibi1..." })` and `users(...)` directly.
+- **`where.address`**: Accepts **Bech32** (`nibi1...`) or **EVM hex** (`0x...`). The resolver normalizes to the same account; **`user.address` in the response is always Bech32**.
+- **`balances` vs `all_balances`**: **`balances`** is the legacy `{ denom, amount }` list and is often **empty** for accounts whose funds are mostly **EVM / FunToken / enriched** balances. Prefer **`all_balances`**: each row is a **Balance** with **`amount`** (string, base units) and **`token_info`** (**TokenInfo**: symbol, name, decimals, `bank_denom`, `erc20_contract_address`, `type`, etc.). See [reference.md β User, Balance, TokenInfo](reference.md#4-graphql-schema-types-heart-monitor).
+- **Amount strings**: **Token.amount** and **Balance.amount** are **String** scalars (unlike staking Ints). Parse for display using **`token_info.decimals`** when present.
+- **Pagination**: Same as indexer-staking β default limit 1000, max 1000; use `limit` (and for list endpoints, offset where supported) to page. See [reference.md](reference.md#6-pagination--limits).
+- **TS SDK**: Use `HeartMonitor` from `@nibiruchain/nibijs`; methods `user()` and `users()` for these queries.
+
+## Canonical Usage
+
+- **Single user by address**: `user(where: { address: "nibi1..." })` or `user(where: { address: "0x..." })` with a selection set that includes **`all_balances`** when you need real balances, for example:
+
+```graphql
+user(where: { address: "0x..." }) {
+ address
+ balances { denom amount }
+ all_balances {
+ amount
+ token_info {
+ type
+ name
+ symbol
+ bank_denom
+ erc20_contract_address
+ decimals
+ verified
+ price
+ logo
+ }
+ }
+ created_block { block }
+ is_blocked
+}
+```
+
+- **List users**: `users(where: ..., order_by: UserOrder, order_desc: Boolean, limit: Int)` with the same **`all_balances`** / **`balances`** shape as needed. Filter by `UsersFilter` (optional `address`, `created_block_eq`, `created_block_lte`, `created_block_gte`); order by `UserOrder` (`address`, `created_block`).
+
+## Rule of Thumb: Where to Query?
+
+| Requirement | Preferred Source | Reason |
+| :--- | :--- | :--- |
+| Current account balances (by address) | **Indexer** | Fast, indexed, GraphQL support. |
+| Strict consistency or balance type not in indexer | **Chain** | Use `nibid query bank balances ` or chain RPC. |
+| Indexer lag acceptable | **Indexer** | Data may be a few seconds behind the chain. |
+
+## Additional Resources
+
+- **[Full Reference (reference.md)](reference.md)**:
+ - [GraphQL root: no container](reference.md#3-graphql-root-no-container)
+ - [GraphQL schema types (User, balances, all_balances, Token, Balance, TokenInfo)](reference.md#4-graphql-schema-types-heart-monitor)
+ - [Filters and order](reference.md#5-filters-and-order)
+ - [Pagination and limits](reference.md#6-pagination--limits)
+ - [TS SDK (Heart Monitor)](reference.md#7-ts-sdk-heart-monitor)
+ - [Gotchas](reference.md#8-gotchas)
+ - [File reference (by repo)](reference.md#9-file-reference-by-repo)
diff --git a/ai-skills/indexer-user-balances/reference.md b/ai-skills/indexer-user-balances/reference.md
new file mode 100644
index 0000000..6a2324c
--- /dev/null
+++ b/ai-skills/indexer-user-balances/reference.md
@@ -0,0 +1,315 @@
+# Nibiru Indexer (Heart Monitor) β User Balances Schema Reference
+
+This document describes the Nibiru indexer (Heart Monitor) and the **user** / **users** / **Token** portion of its GraphQL schema: what the indexer is, how the TypeScript SDK exposes it, and the types and fields available for user balance queries.
+
+- [1. Repo and indexer context](#1-repo-and-indexer-context)
+- [2. What the indexer is and how it's exposed](#2-what-the-indexer-is-and-how-its-exposed)
+- [3. GraphQL root: no container](#3-graphql-root-no-container)
+- [4. GraphQL schema types (Heart Monitor)](#4-graphql-schema-types-heart-monitor)
+- [5. Filters and order](#5-filters-and-order)
+- [6. Pagination and limits](#6-pagination--limits)
+- [7. TS SDK (Heart Monitor)](#7-ts-sdk-heart-monitor)
+- [8. Gotchas](#8-gotchas)
+- [9. File reference (by repo)](#9-file-reference-by-repo)
+
+---
+
+## 1. Repo and indexer context
+
+For repo locations and cross-repo routing (nibi-go-hm, nibi-ts-sdk, nibi-chain, etc.), see the **repo-map** skill. The indexer schema lives in **nibi-go-hm**; the TypeScript client (HeartMonitor, generated types) lives in **nibi-ts-sdk**.
+
+---
+
+## 2. What the indexer is and how it's exposed
+
+- **Heart Monitor** (or "hm") is the server that indexes the blockchain and exposes that data over **GraphQL**.
+- **Implementation**: The server is implemented in the `nibi-go-hm` repo.
+- **Endpoint**: Clients send POST requests to the GraphQL endpoint at path `/query`. **Production URLs**: Mainnet `https://hm-graphql.nibiru.fi/query`; testnet `https://hm-graphql.itn-2.nibiru.fi/query`. **Env** is the hostname segment that identifies the network: omit it for mainnet; use `itn-2` for testnet. Generic form: `https://hm-graphql..nibiru.fi/query` (for mainnet, use `hm-graphql.nibiru.fi` with no `.` segment). The playground is at `/graphql` on the same host.
+- **Scope**: User balances (and the `user` / `users` root fields) are one part of the API. The same API serves staking, governance, oracle, inflation, IBC, wasm, and more.
+
+---
+
+## 3. GraphQL root: no container
+
+Unlike staking, which uses a container field `query { staking { ... } }`, **user** and **users** are **root-level** fields on `Query`. There is no `user: User!` container type.
+
+Root signatures (from the schema):
+
+```graphql
+type Query {
+ user(where: UserFilter!): User
+ users(
+ where: UsersFilter
+ order_by: UserOrder
+ order_desc: Boolean
+ limit: Int
+ ): [User!]!
+ # ... other root fields (staking, governance, oracle, etc.)
+}
+```
+
+- **user**: Takes a required **UserFilter** (address). Returns a single **User** (nullable if no such user).
+- **users**: Takes optional **UsersFilter**, **UserOrder**, **order_desc**, and **limit**. Returns a non-null list of **User**.
+
+---
+
+## 4. GraphQL schema types (Heart Monitor)
+
+The following types are defined in the schema and used by `user` / `users` and by **User**. Only these types are documented here.
+
+### User
+
+Defined in `user.graphqls`. Represents an account and its indexed balances.
+
+```graphql
+type User {
+ address: String!
+ balances: [Token]!
+ all_balances: [Balance]!
+ created_block: Block!
+ is_blocked: Boolean
+}
+```
+
+- **address**: Bech32 account address returned by the API (e.g. `nibi1...`), even when the lookup used an EVM `0x` address in **UserFilter**.
+- **balances**: Legacy list of **Token** rows (`denom` + `amount`). Often **empty** on mainnet for wallets that hold assets surfaced primarily through **all_balances** (e.g. ERC20 balances the indexer attaches **TokenInfo** to). Still useful when populated.
+- **all_balances**: Preferred for a **full balance view**. Each element is **Balance** (`amount` + **token_info**). Use this when **`balances`** looks empty but the account should have funds.
+- **created_block**: The block at which the indexer first saw this account. Type is **Block**.
+- **is_blocked**: Optional. Whether the account is currently blocked in the indexer.
+
+### Balance
+
+Used by **User.all_balances**.
+
+```graphql
+type Balance {
+ amount: String!
+ token_info: TokenInfo!
+}
+```
+
+- **amount**: Balance in **base units** as a string (e.g. `"200000000"` for 200 USDC when **token_info.decimals** is `6`).
+- **token_info**: Metadata for the asset (bank denom, ERC20 address, symbol, decimals, etc.). See **TokenInfo** below.
+
+### TokenInfo
+
+Used by **Balance.token_info**. Field names differ from **Token** (there is no `denom` on **TokenInfo**; use **bank_denom**).
+
+Typical fields (see live schema in GraphQL introspection or `nibi-go-hm`):
+
+- **type**: Asset class (e.g. `erc20`).
+- **name**, **symbol**: Human-readable labels.
+- **bank_denom**: Cosmos bank denomination when applicable; may be **empty** for pure ERC20-only representations.
+- **erc20_contract_address**: EVM contract when **type** is ERC20-related.
+- **decimals**: Use with **amount** for display.
+- **verified**, **price**, **logo**: Optional metadata / pricing / branding.
+
+Example selection for tooling:
+
+```graphql
+all_balances {
+ amount
+ token_info {
+ type
+ name
+ symbol
+ bank_denom
+ erc20_contract_address
+ decimals
+ verified
+ price
+ logo
+ }
+}
+```
+
+### Token
+
+Defined in `common.graphqls`. Used by **User.balances** and elsewhere in the API.
+
+```graphql
+type Token {
+ denom: String!
+ amount: String!
+}
+```
+
+- **denom**: Token denomination (e.g. `unibi`, or Token Factory denoms).
+- **amount**: Balance amount as a **String**. Unlike staking delegation amounts (Int in base units), Token amounts are strings; parse for display or arithmetic as needed.
+
+### Block
+
+Defined in `block.graphqls`. Used by **User.created_block**.
+
+```graphql
+type Block {
+ block: Int!
+ block_ts: Int!
+ num_txs: Int!
+ block_duration: Float!
+}
+```
+
+- **block**: Block height.
+- **block_ts**: Unix timestamp of the block.
+- **num_txs**: Number of transactions in the block.
+- **block_duration**: Time taken to produce the block (float).
+
+### UserFilter
+
+Defined in `user.graphqls`. Required argument for the **user** root query.
+
+```graphql
+input UserFilter {
+ address: String!
+}
+```
+
+- **address**: Required. **Bech32** (`nibi1...`) or **EVM hex** (`0x...`). The server resolves both to the same account; the **User.address** field in the response is Bech32.
+
+### UsersFilter
+
+Defined in `user.graphqls`. Optional argument for the **users** root query.
+
+```graphql
+input UsersFilter {
+ address: String
+ created_block_eq: Int
+ created_block_lte: Int
+ created_block_gte: Int
+}
+```
+
+- **address**: Optional. Filter by account address (exact match, if supported by the resolver).
+- **created_block_eq / created_block_lte / created_block_gte**: Optional. Filter by the block height at which the user was first seen.
+
+### UserOrder
+
+Defined in `user.graphqls`. Sort key for **users** (with **order_desc**).
+
+```graphql
+enum UserOrder {
+ address
+ created_block
+}
+```
+
+Use with **users** as `order_by: UserOrder` and `order_desc: Boolean` to control sort order.
+
+---
+
+## 5. Filters and order
+
+| Input / enum | Used by | Summary |
+| --- | --- | --- |
+| **UserFilter** | `user(where: UserFilter!)` | **address** (required). |
+| **UsersFilter** | `users(where: UsersFilter)` | **address** (optional), **created_block_eq**, **created_block_lte**, **created_block_gte** (optional Int). |
+| **UserOrder** | `users(order_by: UserOrder, order_desc: Boolean)` | Enum: **address**, **created_block**. |
+
+---
+
+## 6. Pagination and limits
+
+Pagination follows the same server rules as the rest of the indexer (e.g. staking): see `nibi-go-hm/graphql/graph/db/utils.go` for `DEFAULT_LIMIT` and `MAX_LIMIT`.
+
+- **Default limit**: 1000 (when not specified).
+- **Max limit**: 1000 (values above 1000 are clamped).
+- **users**: The **users** query accepts **limit** only (no **offset** in the schema). Use **limit** to cap the number of users returned; for larger result sets, combine with **UsersFilter** (e.g. **created_block_gte** / **created_block_lte**) to page by block range if the resolver supports it, or rely on limit-only fetching.
+
+---
+
+## 7. TS SDK (Heart Monitor)
+
+The TypeScript SDK (`@nibiruchain/nibijs` from the `nibi-ts-sdk` repo) provides a typed client for the Heart Monitor API.
+
+### Import and construction
+
+```typescript
+import { HeartMonitor } from "@nibiruchain/nibijs"
+
+const hm = new HeartMonitor("https://hm-graphql.devnet-2.nibiru.fi/query")
+// Or omit to use default endpoint
+```
+
+### Method signatures
+
+- **user**: `HeartMonitor.user(args: GQLQueryGqlUserArgs, fields: DeepPartial)` β `Promise`.
+ **GqlOutUser** has shape `{ user?: GQLUser }`. Use **args** to pass `where: { address: "nibi1..." }`.
+
+- **users**: `HeartMonitor.users(args: GQLQueryGqlUsersArgs, fields: DeepPartial)` β `Promise`.
+ **GqlOutUsers** has shape `{ users?: GQLUser[] }`. **args** can include `where`, `order_by`, `order_desc`, `limit`. The SDK defaults for **users** (in `users.ts`) set `limit: 100`, `order_desc: true`, and `order_by: GQLUserOrder.GQLCreatedBlock` when not provided.
+
+### Example: get one user's address and balances
+
+Prefer **`all_balances`** (with **token_info**) when **`balances`** is empty or incomplete:
+
+```typescript
+const out = await hm.user(
+ { where: { address: "nibi1..." } }, // or "0x..." EVM address
+ {
+ address: true,
+ balances: { denom: true, amount: true },
+ all_balances: {
+ amount: true,
+ token_info: {
+ type: true,
+ name: true,
+ symbol: true,
+ bank_denom: true,
+ erc20_contract_address: true,
+ decimals: true,
+ verified: true,
+ price: true,
+ logo: true,
+ },
+ },
+ created_block: { block: true },
+ is_blocked: true,
+ }
+)
+const user = out.user
+// user?.address, user?.balances, user?.all_balances, user?.created_block, user?.is_blocked
+```
+
+### Example: list users with limit
+
+```typescript
+const out = await hm.users(
+ { where: {}, limit: 50, order_by: GQLUserOrder.GQLAddress, order_desc: false },
+ { address: true, balances: { denom: true, amount: true } }
+)
+const list = out.users ?? []
+```
+
+Generated types **GQLUser**, **GQLToken**, **GQLUserFilter**, **GQLUsersFilter**, **GQLUserOrder**, **GQLQueryGqlUserArgs**, **GQLQueryGqlUsersArgs** live in `nibi-ts-sdk/src/gql/utils/generated.ts`. Query builders and helpers: `nibi-ts-sdk/src/gql/query/user.ts`, `nibi-ts-sdk/src/gql/query/users.ts`, `nibi-ts-sdk/src/gql/heart-monitor/heart-monitor.ts`.
+
+---
+
+## 8. Gotchas
+
+- **`balances` vs `all_balances`**: Do not assume **`balances`** is complete. If it is empty but the wallet holds tokens on chain, query **`all_balances`** with **`token_info`** (and compare with **`nibid query bank balances`** / EVM **`balanceOf`** if needed).
+- **Indexer lag**: The indexer indexes blocks asynchronously. Data may be a few seconds behind the chain. For strict consistency, use the chain (e.g. `nibid query bank balances `).
+- **Token.amount and Balance.amount are String**: Parse for display or arithmetic. Use **TokenInfo.decimals** for **all_balances** rows when present.
+- **user(where) returns nullable User**: If no user exists for the given address, **user** can return null. **users** always returns an array (possibly empty).
+- **is_blocked**: Optional; when true, the account is blocked in the indexer.
+- **created_block**: Represents the block at which the indexer first saw this account, not necessarily the chainβs βaccount creationβ event.
+- **Coverage**: Some chain-only denoms may appear only on **`balances`** or only via bank queries; **all_balances** is the right default for enriched / EVM-associated balances in the indexer.
+
+---
+
+## 9. File reference (by repo)
+
+### Server (nibi-go-hm)
+
+- `graphql/graph/user.graphqls`: **User**, **UserFilter**, **UsersFilter**, **UserOrder**.
+- `graphql/graph/common.graphqls`: **Token**.
+- `graphql/graph/query.graphqls`: Root **user** and **users** field definitions.
+- `graphql/graph/block.graphqls`: **Block** (used by **User.created_block**).
+- `graphql/graph/db/utils.go`: Default and max limit (1000) for pagination.
+
+### Client (nibi-ts-sdk)
+
+- `src/gql/heart-monitor/heart-monitor.ts`: **HeartMonitor** class and **user** / **users** methods.
+- `src/gql/query/user.ts`: **user** query string builder and **user()**; **GqlOutUser**.
+- `src/gql/query/users.ts`: **users** query string builder and **users()**; **GqlOutUsers**; default args.
+- `src/gql/utils/generated.ts`: **GQLUser**, **GQLToken**, **GQLBalance**, **GQLTokenInfo**, **GQLUserFilter**, **GQLUsersFilter**, **GQLUserOrder**, **GQLQueryGqlUserArgs**, **GQLQueryGqlUsersArgs**, **GQLBlock** (exact names may vary by SDK version; introspect or read generated types).
diff --git a/ai-skills/nibid-gov-upgrade/SKILL.md b/ai-skills/nibid-gov-upgrade/SKILL.md
new file mode 100644
index 0000000..1a6d380
--- /dev/null
+++ b/ai-skills/nibid-gov-upgrade/SKILL.md
@@ -0,0 +1,227 @@
+---
+name: nibid-gov-upgrade
+description: Queries Nibiru governance proposals (including software-upgrade proposals) using the `nibid` CLI, maps explorer UI fields to on-chain queries, computes quorum/threshold/veto metrics, and correlates validator votes using staking queries. Use when the user mentions `nibid q gov`, governance proposals, software upgrades, votes, deposits, tally, quorum/threshold, validators, proposal status, or `nibid tx gov`.
+---
+
+# Nibid Gov + Upgrade (Nibiru mainnet)
+
+## Workflow: How to Use this Skill
+
+Start with the canonical query bundle in
+[`Quick start (mainnet)`](#quick-start-mainnet). That is the default first step
+when the user asks about a proposal and you need the core governance state.
+
+After that, choose the recipe that matches the user's question:
+
+1. For a live proposal's voting properties, use
+ [`Recipe: Show voting and quorum info for a live proposal`](#recipe-show-voting-and-quorum-info-for-a-live-proposal).
+ This is the fast path when the user wants a voting and quorum summary instead
+ of only raw `proposal` or `tally` JSON.
+2. For governance thresholds in the same output, use
+ [`reference.md#explorer-style-quorum-report`](./reference.md#explorer-style-quorum-report).
+3. If the proposal is a software upgrade, use
+ [`Software-upgrade proposal: extract plan (name/height/info/binaries)`](#software-upgrade-proposal-extract-plan-nameheightinfobinaries)
+ to inspect `plan.name`, `plan.height`, and parsed binaries from `plan.info`.
+4. If the user wants validator-only participation rather than all voters, use
+ [`Validator votes (filter votes to validator set)`](#validator-votes-filter-votes-to-validator-set).
+
+## Additional resources
+
+- Quorum, threshold, and veto:
+ [`reference.md#quorum-threshold-and-veto`](./reference.md#quorum-threshold-and-veto)
+- Explorer-style quorum report:
+ [`reference.md#explorer-style-quorum-report`](./reference.md#explorer-style-quorum-report)
+
+## Quick start (mainnet)
+
+1. Configure CLI for mainnet (uses your `ud` wrapper):
+
+```bash
+ud nibi cfg prod
+nibid config
+```
+
+2. Pick a proposal id:
+
+```bash
+ID=27
+```
+
+3. Run the canonical set of queries (JSON output):
+
+```bash
+nibid q gov proposal "$ID"
+nibid q gov votes "$ID" --limit 1000
+nibid q gov tally "$ID"
+nibid q gov deposits "$ID"
+nibid q gov params
+
+nibid q staking pool
+nibid q staking validators --limit 1000
+```
+
+## Mapping: "Proposal #27" explorer fields β on-chain queries
+
+- **Proposal metadata (status, times, title/summary, total_deposit, proposer)**:
+ - `nibid q gov proposal "$ID"`
+ - Example fields: `.status`, `.submit_time`, `.voting_end_time`, `.title`,
+ `.summary`, `.total_deposit`, `.proposer`
+
+- **Deposits table**:
+ - `nibid q gov deposits "$ID"`
+
+- **Votes table (all votes, incl. weighted)**:
+ - `nibid q gov votes "$ID" --limit 1000`
+
+- **Current tally (yes/no/veto/abstain counts)**:
+ - `nibid q gov tally "$ID"`
+
+- **Quorum/threshold/veto thresholds**:
+ - `nibid q gov params` (or `nibid q gov param voting|deposit|tallying`)
+
+- **Bonded voting power denominator (for quorum math)**:
+ - `nibid q staking pool` (use `.bonded_tokens`)
+
+## Software-upgrade proposal: extract `plan` (name/height/info/binaries)
+
+Nibiru mainnet currently surfaces software upgrades as a gov v1 proposal that
+executes legacy content (you'll typically see `MsgExecLegacyContent` wrapping a
+`SoftwareUpgradeProposal`).
+
+Extract the upgrade plan:
+
+```bash
+nibid q gov proposal "$ID" | jq '
+ .messages[]
+ | select(."@type" == "/cosmos.gov.v1.MsgExecLegacyContent")
+ | .content
+ | select(."@type" == "/cosmos.upgrade.v1beta1.SoftwareUpgradeProposal")
+ | .plan
+'
+```
+
+Parse the `binaries` object out of `plan.info` (it's a JSON string):
+
+```bash
+nibid q gov proposal "$ID" | jq -r '
+ .messages[]
+ | select(."@type" == "/cosmos.gov.v1.MsgExecLegacyContent")
+ | .content
+ | select(."@type" == "/cosmos.upgrade.v1beta1.SoftwareUpgradeProposal")
+ | .plan.info
+ | fromjson
+ | .binaries
+'
+```
+
+## Recipe: Show voting and quorum info for a live proposal
+
+Quick command:
+
+```bash
+jq -s '
+ .[0] as $t
+ | .[1] as $p
+ | ($p.bonded_tokens|tonumber) as $bonded
+ | ($t.yes_count|tonumber) as $yes
+ | ($t.no_count|tonumber) as $no
+ | ($t.abstain_count|tonumber) as $abstain
+ | ($t.no_with_veto_count|tonumber) as $veto
+ | ($yes+$no+$abstain+$veto) as $total
+ | {
+ bonded_tokens: $bonded,
+ total_voted_tokens: $total,
+ quorum_pct: (if $bonded==0 then null else ($total / $bonded * 100) end),
+ yes_pct_of_non_abstain: (
+ if ($yes+$no+$veto)==0 then null
+ else ($yes / ($yes+$no+$veto) * 100)
+ end
+ ),
+ veto_pct_of_non_abstain: (
+ if ($yes+$no+$veto)==0 then null
+ else ($veto / ($yes+$no+$veto) * 100)
+ end
+ )
+ }
+' <(nibid q gov tally "$ID") <(nibid q staking pool)
+```
+
+Raw sources:
+
+- `nibid q gov tally "$ID"` for current vote weights
+- `nibid q gov params` for quorum / threshold / veto parameters
+- `nibid q staking pool` for `bonded_tokens`
+
+For a fuller report with governance thresholds and field notes, see
+[`reference.md#explorer-style-quorum-report`](./reference.md#explorer-style-quorum-report)
+and [`reference.md#field-meanings`](./reference.md#field-meanings).
+
+## Validator votes (filter votes to validator set)
+
+`nibid q gov votes` includes **all** voters (delegators + validators). To match
+"Validators Votes" UIs, filter to validator accounts by converting each
+validator's `nibivaloper...` to its corresponding `nibi...` address.
+
+Helpers:
+
+```bash
+# Shows both Bech32 Acc (nibi...) and Bech32 Val (nibivaloper...)
+nibid debug addr nibi1ah8gqrtjllhc5ld4rxgl4uglvwl93ag0sh6e6v
+```
+
+Example report (bonded validators who voted on `$ID`):
+
+```bash
+ID=27
+VOTES_JSON="$(nibid q gov votes "$ID" --limit 1000)"
+VALS_JSON="$(nibid q staking validators --limit 2000)"
+
+echo "$VALS_JSON" \
+ | jq -r '.validators[] | select(.status=="BOND_STATUS_BONDED") | .operator_address' \
+ | while read -r valoper; do
+ acc="$(nibid debug addr "$valoper" | sed -n 's/^Bech32 Acc: //p')"
+ vote="$(echo "$VOTES_JSON" | jq -c --arg acc "$acc" '.votes[] | select(.voter==$acc) | {voter, options}')"
+ if [[ -n "$vote" ]]; then
+ moniker="$(echo "$VALS_JSON" | jq -r --arg valoper "$valoper" '.validators[] | select(.operator_address==$valoper) | .description.moniker')"
+ tokens="$(echo "$VALS_JSON" | jq -r --arg valoper "$valoper" '.validators[] | select(.operator_address==$valoper) | .tokens')"
+ echo "$moniker $valoper $acc tokens=$tokens vote=$vote"
+ fi
+ done
+```
+
+## Governance transactions (tx)
+
+- **Vote**:
+
+```bash
+nibid tx gov vote "$ID" yes --from
+```
+
+- **Weighted vote**:
+
+```bash
+nibid tx gov weighted-vote "$ID" yes=0.6,no=0.3,abstain=0.1 --from
+```
+
+- **Deposit**:
+
+```bash
+nibid tx gov deposit "$ID" 1000000unibi --from
+```
+
+- **Submit a software-upgrade proposal (legacy content path)**:
+
+```bash
+nibid tx gov submit-legacy-proposal software-upgrade v2.11.0 \
+ --title "v2.11.0" \
+ --description "Upgrade to v2.11.0" \
+ --deposit 20000000000unibi \
+ --upgrade-height 36757700 \
+ --upgrade-info '{"binaries":{"linux/amd64":"https://...","linux/arm64":"https://...","docker":"ghcr.io/...:2.11.0"}}' \
+ --from
+```
+
+## Upgrade module queries (after a proposal passes)
+
+- `nibid q upgrade plan` only works once an upgrade is scheduled in `x/upgrade`.
+ Before then, it can return "no upgrade scheduled".
\ No newline at end of file
diff --git a/ai-skills/nibid-gov-upgrade/reference.md b/ai-skills/nibid-gov-upgrade/reference.md
new file mode 100644
index 0000000..dcfa754
--- /dev/null
+++ b/ai-skills/nibid-gov-upgrade/reference.md
@@ -0,0 +1,299 @@
+# nibid-gov-upgrade: reference
+
+Extended reference for [`SKILL.md`](./SKILL.md). This file is intentionally
+longer and optimized for βcopy/pasteβ and quick lookup.
+
+## Assumptions
+
+- **Network**: mainnet via `ud nibi cfg prod`
+- **Output**: `ud nibi cfg prod` sets `nibid config output json`, so examples
+ omit `-o json`
+- **Tools**: `jq` installed
+
+Quick setup:
+
+```bash
+ud nibi cfg prod
+nibid config
+ID=27
+```
+
+## Proposal JSON shapes (why the fields look the way they do)
+
+On Nibiru mainnet, software upgrades show up as a **gov v1** proposal containing
+`messages[]`, typically with a legacy wrapper:
+
+- `messages[].@type == "/cosmos.gov.v1.MsgExecLegacyContent"`
+- `messages[].content.@type == "/cosmos.upgrade.v1beta1.SoftwareUpgradeProposal"`
+
+So the upgrade plan lives under:
+
+- `.messages[] | ... | .content.plan`
+
+Other chains / older SDK versions may still surface βlegacy content proposalsβ
+under a single `content` field (gov v1beta1). When youβre scripting, handle both
+shapes.
+
+## Explorer UI β on-chain mapping (commands + JSON paths)
+
+| Explorer UI area | Command | Primary JSON paths / notes |
+|---|---|---|
+| Overview (title/summary/status) | `nibid q gov proposal "$ID"` | `.title`, `.summary`, `.status` |
+| Voting start/end | `nibid q gov proposal "$ID"` | `.voting_start_time`, `.voting_end_time` |
+| Submit/deposit times | `nibid q gov proposal "$ID"` | `.submit_time`, `.deposit_end_time` |
+| Proposer | `nibid q gov proposal "$ID"` or `nibid q gov proposer "$ID"` | `.proposer` |
+| Total deposit | `nibid q gov proposal "$ID"` | `.total_deposit[]` |
+| Deposits tab | `nibid q gov deposits "$ID"` | `.deposits[]` |
+| Votes tab | `nibid q gov votes "$ID" --limit 1000` | `.votes[]` (weighted `options[]`) |
+| Tally totals | `nibid q gov tally "$ID"` | `.yes_count`, `.no_count`, `.abstain_count`, `.no_with_veto_count` |
+| Quorum/threshold params | `nibid q gov params` or `nibid q gov param tallying` | `.tally_params.quorum`, `.threshold`, `.veto_threshold` |
+| Voting/deposit params | `nibid q gov params` or `nibid q gov param voting|deposit` | `voting_period`, `min_deposit`, `max_deposit_period` |
+| Bonded denominator for quorum | `nibid q staking pool` | `.bonded_tokens` |
+| Validator set (w/ power) | `nibid q staking validators --limit 2000` | `.validators[]` includes `.tokens`, `.status`, `.operator_address` |
+| Upgrade plan (name/height/info) | `nibid q gov proposal "$ID"` | see extraction recipes below |
+| Scheduled upgrade plan | `nibid q upgrade plan` | Only after chain schedules an upgrade; can return βno upgrade scheduledβ during voting |
+
+## Canonical query bundle (proposal-centric)
+
+```bash
+nibid q gov proposal "$ID"
+nibid q gov votes "$ID" --limit 1000
+nibid q gov tally "$ID"
+nibid q gov deposits "$ID"
+nibid q gov params
+nibid q staking pool
+nibid q staking validators --limit 2000
+```
+
+## Recipes
+
+### Extract the software-upgrade plan (proposal content)
+
+```bash
+nibid q gov proposal "$ID" | jq '
+ .messages[]
+ | select(."@type" == "/cosmos.gov.v1.MsgExecLegacyContent")
+ | .content
+ | select(."@type" == "/cosmos.upgrade.v1beta1.SoftwareUpgradeProposal")
+ | .plan
+'
+```
+
+### Parse `plan.info` and extract `binaries`
+
+`plan.info` is a JSON *string* (Cosmovisor-compatible metadata is a common
+convention).
+
+```bash
+nibid q gov proposal "$ID" | jq -r '
+ .messages[]
+ | select(."@type" == "/cosmos.gov.v1.MsgExecLegacyContent")
+ | .content
+ | select(."@type" == "/cosmos.upgrade.v1beta1.SoftwareUpgradeProposal")
+ | .plan.info
+ | fromjson
+ | .binaries
+'
+```
+
+### Quorum, threshold, and veto
+
+Conceptually:
+
+- **Quorum**: \((yes+no+abstain+veto) / bonded_tokens\)
+- **Threshold**: \(yes / (yes+no+veto)\) (abstain excluded)
+- **Veto%**: \(veto / (yes+no+veto)\) (abstain excluded)
+
+Inputs:
+
+- `nibid q gov tally "$ID"` gives the current vote weights:
+ `yes_count`, `no_count`, `abstain_count`, `no_with_veto_count`
+- `nibid q gov params` gives the governance thresholds:
+ `quorum`, `threshold`, `veto_threshold`
+- `nibid q staking pool` gives `bonded_tokens`, the quorum denominator
+
+### Explorer-style quorum report
+
+```bash
+TALLY="$(nibid q gov tally "$ID")"
+PARAMS="$(nibid q gov params)"
+POOL="$(nibid q staking pool)"
+
+jq -n \
+ --argjson tally "$TALLY" \
+ --argjson params "$PARAMS" \
+ --argjson pool "$POOL" '
+ ($tally.yes_count|tonumber) as $yes
+ | ($tally.no_count|tonumber) as $no
+ | ($tally.abstain_count|tonumber) as $abstain
+ | ($tally.no_with_veto_count|tonumber) as $veto
+ | ($pool.bonded_tokens|tonumber) as $bonded
+ | ($yes+$no+$abstain+$veto) as $total
+ | {
+ tally: $tally,
+ bonded_tokens: $pool.bonded_tokens,
+ quorum_required_pct: (($params.tally_params.quorum|tonumber) * 100),
+ threshold_required_pct: (($params.tally_params.threshold|tonumber) * 100),
+ veto_threshold_pct: (($params.tally_params.veto_threshold|tonumber) * 100),
+ total_voted_tokens: $total,
+ quorum_pct: (if $bonded==0 then null else ($total / $bonded * 100) end),
+ yes_pct_of_non_abstain: (
+ if ($yes+$no+$veto)==0 then null
+ else ($yes / ($yes+$no+$veto) * 100)
+ end
+ ),
+ veto_pct_of_non_abstain: (
+ if ($yes+$no+$veto)==0 then null
+ else ($veto / ($yes+$no+$veto) * 100)
+ end
+ )
+ }'
+```
+
+### Field meanings
+
+- `tally`: raw output copied from `nibid q gov tally "$ID"`
+- `bonded_tokens`: raw output copied from `nibid q staking pool`
+- `quorum_required_pct`: derived from `nibid q gov params` by converting
+ `.tally_params.quorum` from a decimal fraction to percent
+- `threshold_required_pct`: derived from `nibid q gov params` by converting
+ `.tally_params.threshold` from a decimal fraction to percent
+- `veto_threshold_pct`: derived from `nibid q gov params` by converting
+ `.tally_params.veto_threshold` from a decimal fraction to percent
+- `total_voted_tokens`: `yes + no + abstain + veto`
+- `quorum_pct`: `total_voted_tokens / bonded_tokens * 100`
+- `yes_pct_of_non_abstain`: `yes / (yes + no + veto) * 100`
+- `veto_pct_of_non_abstain`: `veto / (yes + no + veto) * 100`
+
+Notes:
+
+- `abstain` counts toward quorum participation, so it is included in
+ `total_voted_tokens`.
+- `abstain` is excluded from `yes_pct_of_non_abstain` and
+ `veto_pct_of_non_abstain` to match governance threshold semantics and typical
+ explorer displays.
+
+### Pagination patterns (votes, deposits, validators)
+
+Most list queries support (varying by command):
+
+- `--limit`, `--page`, `--offset`, `--page-key`, `--count-total`, `--reverse`
+
+Examples:
+
+```bash
+# Votes
+nibid q gov votes "$ID" --limit 100 --page 1
+nibid q gov votes "$ID" --limit 100 --page 2
+
+# Deposits
+nibid q gov deposits "$ID" --limit 100 --page 1
+
+# Validators
+nibid q staking validators --limit 100 --page 1
+```
+
+### Validator vote report (join gov votes to validators)
+
+Problem: `nibid q gov votes "$ID"` includes **all** voters, not just validators.
+To filter to validators, you need to map each validator operator address
+(`nibivaloper...`) to its corresponding account address (`nibi...`).
+
+Nibiru has built-in helpers:
+
+```bash
+nibid debug prefixes
+nibid debug addr nibivaloper1...
+```
+
+`nibid debug addr` prints lines like:
+
+- `Bech32 Acc: nibi1...`
+- `Bech32 Val: nibivaloper1...`
+
+Example workflow (bonded validators who voted):
+
+```bash
+VOTES_JSON="$(nibid q gov votes "$ID" --limit 1000)"
+VALS_JSON="$(nibid q staking validators --limit 2000)"
+
+echo "$VALS_JSON" \
+ | jq -r '.validators[] | select(.status=="BOND_STATUS_BONDED") | .operator_address' \
+ | while read -r valoper; do
+ acc="$(nibid debug addr "$valoper" | sed -n 's/^Bech32 Acc: //p')"
+ vote="$(echo "$VOTES_JSON" | jq -c --arg acc "$acc" '.votes[] | select(.voter==$acc) | {voter, options}')"
+ if [[ -n "$vote" ]]; then
+ moniker="$(echo "$VALS_JSON" | jq -r --arg valoper "$valoper" '.validators[] | select(.operator_address==$valoper) | .description.moniker')"
+ tokens="$(echo "$VALS_JSON" | jq -r --arg valoper "$valoper" '.validators[] | select(.operator_address==$valoper) | .tokens')"
+ echo "$moniker $valoper $acc tokens=$tokens vote=$vote"
+ fi
+ done
+```
+
+Notes / pitfalls:
+
+- `staking validators` includes unbonded/unbonding validators; explorers often
+ display only the bonded set.
+- Voting power in governance is delegation-weighted; validator βtokensβ here are
+ a proxy for validator voting power, not a direct βvote weightβ per voter.
+
+## Transactions reference (gov)
+
+### Vote
+
+```bash
+nibid tx gov vote "$ID" yes --from
+```
+
+### Weighted vote
+
+```bash
+nibid tx gov weighted-vote "$ID" yes=0.6,no=0.3,abstain=0.1 --from
+```
+
+### Deposit
+
+```bash
+nibid tx gov deposit "$ID" 1000000unibi --from
+```
+
+### Submit proposal (gov v1)
+
+`nibid tx gov submit-proposal` takes a proposal JSON file containing `messages`
+(proto-JSON `sdk.Msg`s) plus metadata and deposit information.
+
+See:
+
+```bash
+nibid tx gov submit-proposal --help
+```
+
+### Submit software-upgrade proposal (legacy content path)
+
+```bash
+nibid tx gov submit-legacy-proposal software-upgrade v2.11.0 \
+ --title "v2.11.0" \
+ --description "Upgrade to v2.11.0" \
+ --deposit 20000000000unibi \
+ --upgrade-height 36757700 \
+ --upgrade-info '{"binaries":{"linux/amd64":"https://...","linux/arm64":"https://...","docker":"ghcr.io/...:2.11.0"}}' \
+ --from
+```
+
+## Upgrade module vs proposal content (common confusion)
+
+- `nibid q gov proposal "$ID"` always shows whatβs being voted on, including an
+ upgrade plan embedded in proposal messages.
+- `nibid q upgrade plan` queries the chainβs scheduled upgrade plan in `x/upgrade`.
+ If the proposal hasnβt passed / been scheduled yet, it can return:
+
+ `Error: no upgrade scheduled`
+
+So: treat the proposal as the source of truth while voting is ongoing.
+
+## Links
+
+- Nibiru governance lifecycle: `https://nibiru.fi/docs/community/governance.html`
+- Nibiru submitting proposals: `https://nibiru.fi/docs/community/submitting-proposals.html`
+- Cosmos SDK upgrade module: `https://docs.cosmos.network/v0.53/build/modules/upgrade`
+
diff --git a/ai-skills/nibiru-cli-nibid/SKILL.md b/ai-skills/nibiru-cli-nibid/SKILL.md
new file mode 100644
index 0000000..1d0b3bc
--- /dev/null
+++ b/ai-skills/nibiru-cli-nibid/SKILL.md
@@ -0,0 +1,245 @@
+---
+name: nibiru-cli-nibid
+description: Query Nibiru chain state and submit transactions with the `nibid` CLI, including setup via `ud`, config inspection, key discovery with `nibid keys list`, bank transfers, bank balance queries, wasm contract queries, wasm execute/instantiate flows, and transaction lookups by hash. Use when the user mentions `nibid`, Nibiru CLI, `nibid keys list`, `--from`, `nibid tx bank`, `nibid tx wasm`, `nibid q bank`, `nibid q wasm`, `nibid q tx`, balances, contract-state smart, or tx hashes.
+---
+
+# Nibiru CLI (`nibid`)
+
+Use this skill for general Nibiru CLI work. Prefer it when the user wants the
+authoritative chain view or wants to submit a transaction with `nibid`.
+
+## Additional resources
+
+- EVM account, FunToken, and unit notes: [`reference.md`](./reference.md)
+- Copy-paste examples: [`examples.md`](./examples.md)
+
+## Quick start
+
+1. Select the chain config with your `ud` wrapper:
+
+```bash
+ud nibi cfg prod
+```
+
+2. Confirm what `nibid` is pointed at:
+
+```bash
+nibid config
+```
+
+Example - Mainnet.
+```js
+{
+ "chain-id": "cataclysm-1",
+ "keyring-backend": "test",
+ "output": "json",
+ "node": "https://rpc.archive.nibiru.fi:443",
+ "broadcast-mode": "sync"
+}
+```
+
+3. Assume JSON output by default.
+All default `nibid` configs use `nibid config output json`, so commands inherit
+JSON output unless the user says otherwise.
+
+```bash
+ADDR="nibi1..."
+nibid q bank balances "$ADDR"
+```
+
+4. Discover which local keys are available for `--from`:
+
+```bash
+nibid keys list | jq
+
+# names that are usually valid for direct signing
+nibid keys list | jq -r '.[] | select(.type=="local") | .name'
+```
+
+Rules of thumb:
+
+- `type == "local"` is the default set of names to use for direct `--from`
+ signing.
+- `type == "offline"` is useful for address/pubkey reference, not direct local
+ signing.
+- `type == "multi"` is a multisig identity and usually needs a multisig flow,
+ not a simple single-signer `--from`.
+- If the user says "use the `validator-6900` key", prefer `--from
+ validator-6900` when that name appears in `nibid keys list`.
+
+## Core mental model
+
+- `nibid q ...` / `nibid query ...` reads chain state.
+- `nibid tx ...` builds and broadcasts transactions.
+- `nibid config` shows the current CLI configuration and is the first check when
+ a result looks wrong for the expected network.
+- Use `ud nibi cfg ...` to switch networks before querying or sending.
+- Use `nibid keys list | jq` to discover available `--from` values before
+ proposing or running a transaction.
+- Pipe to `jq` for nested fields when needed.
+- Use specialized skills for deeper workflows:
+ - `nibid-gov-upgrade` for governance and software upgrades
+ - `sai-perps-query` for Sai contract query recipes
+ - `evm-rpc` for `eth_*` / `debug_*` EVM tracing rather than Cosmos CLI
+
+## Query routing
+
+If the user asks about:
+
+- **Wallet balances** -> use `nibid q bank ...`
+- **Wasm contract state** -> use `nibid q wasm ...`
+- **A tx hash** -> use `nibid q tx "$TX"`
+- **Sending funds** -> use `nibid tx bank ...`
+- **Executing or instantiating a contract** -> use `nibid tx wasm ...`
+
+## Golden paths
+
+### Query Bank
+
+```bash
+# all balances for an account
+ADDR="nibi1..."
+nibid q bank balances "$ADDR"
+
+# one denom balance
+DENOM="unibi"
+nibid q bank balances "$ADDR" --denom "$DENOM"
+
+# total supply for a denom
+nibid q bank total "$DENOM"
+```
+
+Notes:
+
+- Amounts are usually base units such as `unibi`.
+- For display, divide `unibi` by `1e6` to get `NIBI`.
+
+### Tx Bank
+
+```bash
+# send tokens
+FROM_KEY=""
+TO_ADDR="nibi1recipient..."
+AMOUNT="1000000unibi"
+nibid tx bank send "$FROM_KEY" "$TO_ADDR" "$AMOUNT"
+```
+
+Common flags to add when needed:
+
+```bash
+--from --gas auto --gas-adjustment 1.3 --fees 10000unibi
+```
+
+Rule of thumb:
+
+- Use `tx bank send` for simple transfers.
+- Prefer a key name from `nibid keys list` for `--from`, for example `--from
+ validator-6900`.
+- Use `--note` when the receiving flow needs a memo or exchange-style reference.
+- Confirm the active network with `nibid config` before sending anything.
+- See [`examples.md`](./examples.md) for a copy-paste bank send with memo.
+
+### Query Wasm
+
+```bash
+# contract metadata
+CONTRACT_ADDR="nibi1..."
+nibid q wasm contract "$CONTRACT_ADDR"
+
+# list contracts by code id
+CODE_ID="123"
+nibid q wasm list-contract-by-code "$CODE_ID"
+
+# raw smart query
+QUERY_MSG='{"get_config":{}}'
+nibid q wasm contract-state smart "$CONTRACT_ADDR" "$QUERY_MSG"
+```
+
+Rules of thumb:
+
+- `contract-state smart` is the main path for app-level contract queries.
+- Query JSON keys are contract-specific and usually `snake_case`.
+- Reuse a known-good query message shape from the relevant repo or skill when
+ the exact message is not obvious.
+
+### Tx Wasm
+
+```bash
+# execute a contract
+CONTRACT_ADDR="nibi1..."
+EXECUTE_MSG='{"claim":{}}'
+FROM_KEY=""
+nibid tx wasm execute "$CONTRACT_ADDR" "$EXECUTE_MSG" --from "$FROM_KEY"
+
+# instantiate a contract from a code id
+CODE_ID="123"
+INIT_MSG='{"count":0}'
+LABEL="my-contract"
+nibid tx wasm instantiate "$CODE_ID" "$INIT_MSG" --label "$LABEL" --from "$FROM_KEY"
+```
+
+Common additions:
+
+```bash
+--admin --amount 1000000unibi --gas auto --gas-adjustment 1.5 --fees 10000unibi
+```
+
+Rules of thumb:
+
+- `execute` changes state and usually needs `--from`.
+- Prefer a key name from `nibid keys list` for `--from`, not a guessed alias.
+- `instantiate` creates a new contract instance from uploaded code.
+- If funds must be sent alongside execution or instantiation, use `--amount`.
+- Keep the JSON message exact; contract query/execute schemas are not inferred by
+ the CLI.
+
+### Transaction lookup by hash
+
+```bash
+TX="ABCD1234..."
+nibid q tx "$TX"
+```
+
+Useful follow-up:
+
+```bash
+nibid q tx "$TX" | jq .
+```
+
+Rules of thumb:
+
+- Start with `q tx` when the user gives a Cosmos-style tx hash.
+- If the user gives an EVM tx hash and wants receipt/trace semantics, use the
+ `evm-rpc` skill instead.
+
+## Working conventions
+
+1. Start by checking or setting the network. This is how you move between using
+different instances of the Nibiru blockchain.
+
+```bash
+ud nibi cfg prod
+nibid config
+```
+
+2. Before any signing flow, list the keyring and pick an explicit `--from` value:
+
+```bash
+nibid keys list | jq
+nibid keys list | jq -r '.[] | select(.type=="local") | .name'
+```
+
+ - This is useful for debugging, as it can bring to light a missing signer or be
+ used to find potential accounts with funds under management.
+
+3. When command output is too broad, extract the exact field with `jq`.
+4. If the user asks for explorer-style views, derive them from CLI JSON rather
+ than treating the explorer as the source of truth.
+
+## Common pitfalls
+
+- Wrong network selected in the CLI config.
+- Guessing a `--from` value instead of checking `nibid keys list`.
+- Using human units instead of base units like `unibi`.
+- Treating EVM JSON-RPC questions as `nibid` questions.
+- Guessing Wasm query message shapes instead of checking the contract or app code.
\ No newline at end of file
diff --git a/ai-skills/nibiru-cli-nibid/examples.md b/ai-skills/nibiru-cli-nibid/examples.md
new file mode 100644
index 0000000..01f8662
--- /dev/null
+++ b/ai-skills/nibiru-cli-nibid/examples.md
@@ -0,0 +1,51 @@
+# `nibid` Examples
+
+Concrete copy-paste examples for [`SKILL.md`](./SKILL.md).
+
+## Bank send with memo (`--note`)
+
+Use this when the destination requires a memo or reference code, such as an
+exchange deposit account.
+
+```bash
+FROM="ud-prod"
+TO_ADDR="nibi1recipient..."
+MEMO="2097215673"
+AMOUNT="1000000unibi"
+
+nibid tx bank send \
+ "$FROM" \
+ "$TO_ADDR" \
+ "$AMOUNT" \
+ --note "$MEMO" \
+ --fees "5000unibi" \
+ -y
+```
+
+Safer pre-flight variant:
+
+```bash
+FROM="ud-prod"
+TO_ADDR="nibi1recipient..."
+MEMO="2097215673"
+AMOUNT="1000000unibi"
+
+nibid tx bank send \
+ "$FROM" \
+ "$TO_ADDR" \
+ "$AMOUNT" \
+ --note "$MEMO" \
+ --chain-id "cataclysm-1" \
+ --node "https://rpc.archive.nibiru.fi:443" \
+ --fees "5000unibi" \
+ --gas auto \
+ --gas-adjustment 1.3 \
+ --dry-run
+```
+
+Notes:
+
+- Use `--note`, not a guessed memo flag.
+- Keep the amount in base units such as `unibi` unless the user explicitly wants
+ a human-unit conversion.
+- Verify the active network and `--from` value before broadcasting.
diff --git a/ai-skills/nibiru-cli-nibid/reference.md b/ai-skills/nibiru-cli-nibid/reference.md
new file mode 100644
index 0000000..b4afe7e
--- /dev/null
+++ b/ai-skills/nibiru-cli-nibid/reference.md
@@ -0,0 +1,105 @@
+# `nibid` Reference Notes
+
+## EVM via `nibid`
+
+Use these commands when the user wants EVM-related state through the Nibiru CLI
+rather than Ethereum JSON-RPC.
+
+Start on the intended network first:
+
+```bash
+ud nibi cfg prod
+nibid config
+```
+
+Assume the active CLI config is already set to JSON output unless the user says
+otherwise.
+
+### `nibid q evm account`
+
+This is the fastest CLI check for an EVM account's native gas balance on Nibiru.
+It accepts either a hex `0x...` address or a Bech32 `nibi1...` address and
+returns both forms in the response.
+
+```bash
+ADDR_HEX="0x7027d536380F0474B7Ec119c2c5250821902e6dC"
+ADDR_BECH32="nibi1wqna2d3cpuz8fdlvzxwzc5jssgvs9ekufm3uj2"
+nibid q evm account "$ADDR_HEX"
+nibid q evm account "$ADDR_BECH32"
+```
+
+Returned fields:
+
+- `balance_wei`: native EVM balance in wei
+- `nonce`: account nonce
+- `code_hash`: code hash at the address
+- `eth_address`: normalized hex form
+- `bech32_address`: normalized Bech32 form
+
+Reminder:
+
+- The empty-account code hash appears as `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`; seeing that value is a quick hint that the address is not holding deployed EVM bytecode.
+
+Offline normalization only:
+
+```bash
+ADDR="0x7027d536380F0474B7Ec119c2c5250821902e6dC"
+nibid q evm account "$ADDR" --offline
+```
+
+With `--offline`, the CLI only normalizes the address forms. It does not query
+live chain state, so `balance_wei` and `code_hash` come back empty and `nonce`
+defaults to `0`.
+
+### `balance_wei` and bank `unibi`
+
+`balance_wei` from `nibid q evm account` and bank `unibi` from `nibid q bank balances`
+represent the same native NIBI balance, just at different granularities.
+
+- `unibi` means micro NIBI (`ΞΌ NIBI`)
+- `1 NIBI = 1_000_000 unibi`
+- `1 NIBI = 10^18 wei`
+- Therefore `1 unibi = 10^12 wei`
+
+Examples:
+
+```bash
+ADDR="nibi1wqna2d3cpuz8fdlvzxwzc5jssgvs9ekufm3uj2"
+DENOM="unibi"
+nibid q evm account "$ADDR"
+nibid q bank balances "$ADDR" --denom "$DENOM"
+```
+
+Rule of thumb:
+
+- Use `q evm account` when the user thinks in EVM/native-gas terms or has a `0x...` address.
+- Use `q bank balances --denom unibi` when the user wants the Cosmos bank representation.
+
+### `nibid q evm funtoken`
+
+Use this to query the mapping between a bank denom and its ERC20 representation.
+
+```bash
+COIN_OR_ERC20="unibi"
+nibid q evm funtoken "$COIN_OR_ERC20"
+
+COIN_OR_ERC20="0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6"
+nibid q evm funtoken "$COIN_OR_ERC20"
+```
+
+## `nibid tx evm` scope
+
+On this CLI build, `nibid tx evm` is for FunToken and bank/ERC20 conversion
+flows, not generic Ethereum wallet-like transactions.
+
+Available commands:
+
+- `nibid tx evm convert-coin-to-evm`
+- `nibid tx evm convert-evm-to-coin`
+- `nibid tx evm create-funtoken`
+
+Use other surfaces when appropriate:
+
+- `nibid tx bank` for native bank sends
+- `nibid tx wasm` for CosmWasm contract execution and instantiation
+- `evm-rpc` for receipts, traces, and `eth_*` / `debug_*` RPC workflows
diff --git a/ai-skills/postgresql-psql/SKILL.md b/ai-skills/postgresql-psql/SKILL.md
new file mode 100644
index 0000000..2aa8a4f
--- /dev/null
+++ b/ai-skills/postgresql-psql/SKILL.md
@@ -0,0 +1,92 @@
+---
+name: postgresql-psql
+description: >-
+ Comprehensive guide for PostgreSQL psql - the interactive terminal client for
+ PostgreSQL. Use when connecting to PostgreSQL databases, executing queries,
+ managing databases/tables, configuring connection options, formatting output,
+ writing scripts, managing transactions, and using advanced psql features for
+ database administration and development.
+license: PostgreSQL
+version: 1.0.0
+---
+
+# PostgreSQL psql Skill
+
+PostgreSQL psql (PostgreSQL interactive terminal) is the primary command-line client for interacting with PostgreSQL databases. It provides both interactive query execution and powerful scripting capabilities for database management and administration.
+
+## When to Use This Skill
+
+Use this skill when:
+
+- Connecting to PostgreSQL databases from the command line
+- Executing SQL queries interactively
+- Writing SQL scripts for automation
+- Creating and managing databases and schemas
+- Managing database objects (tables, views, indexes, functions)
+- Backing up and restoring databases
+- Configuring connections and authentication
+- Formatting and exporting query results
+- Managing transactions and permissions
+- Debugging SQL queries
+- Automating database administration tasks
+- Setting up replication and high availability
+- Creating stored procedures and functions
+
+## Core Concepts
+
+### REPL Model
+
+- psql operates as an interactive REPL (Read-Eval-Print Loop)
+- Accepts SQL commands and meta-commands (backslash commands)
+- Maintains connection state across commands within a session
+- Supports command history and editing
+
+### Command Types
+
+- **SQL Commands**: Standard SQL statements (SELECT, INSERT, UPDATE, DELETE, etc.)
+- **Meta-Commands**: psql-specific commands prefixed with backslash (e.g., `\dt`, `\d`)
+- **Backslash Commands**: Control query output, session variables, and psql behavior
+
+### Connection Model
+
+- Single database connection per session
+- Can switch databases without reconnecting
+- Connection state includes current database, user, and search path
+- Environmental variables and .pgpass for credential management
+
+## Reference Pointers
+
+Detailed command lists, flags, and configuration options are available in [reference.md](./reference.md):
+
+- Connect and Authenticate
+- Meta-Commands
+- Output and Formatting
+- Files, Editor, and Scripting
+- Command-Line Flags
+- Import and Export
+- Backup and Restore
+- Performance and Debug Toolbox
+- Troubleshooting
+
+## Golden Paths
+
+### 1. Connect using flags
+```bash
+psql -h localhost -p 5432 -U postgres -d mydb
+```
+
+### 2. Connect using URI
+```bash
+psql postgresql://user:pass@host:5432/dbname?sslmode=require
+```
+
+### 3. Inspect a table
+```bash
+\dt # List all tables
+\d table_name # Describe specific table
+```
+
+### 4. Export query results to CSV
+```bash
+\copy (SELECT * FROM users) TO 'users.csv' WITH (FORMAT CSV, HEADER)
+```
diff --git a/ai-skills/postgresql-psql/reference.md b/ai-skills/postgresql-psql/reference.md
new file mode 100644
index 0000000..cf005da
--- /dev/null
+++ b/ai-skills/postgresql-psql/reference.md
@@ -0,0 +1,728 @@
+# PostgreSQL psql Reference
+
+## Connect and Authenticate
+
+### Basic Connection Command
+
+```bash
+psql [OPTIONS] [DBNAME [USERNAME]]
+```
+
+### Common Connection Options
+
+```bash
+# Connect with username and host
+psql -U username -h hostname -p 5432 -d database_name
+
+# Connect using connection string
+psql postgresql://username:password@hostname:5432/database_name
+
+# Connect with password prompt
+psql -U postgres -h localhost -W
+
+# Connect to specific database on local machine
+psql -d myapp_development
+
+# Environment variables (alternative)
+export PGUSER=postgres
+export PGPASSWORD=mypassword
+export PGHOST=localhost
+export PGPORT=5432
+export PGDATABASE=mydb
+psql
+```
+
+### Connection String Formats
+
+**Standard URI format**:
+
+```
+postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]
+```
+
+**Example**:
+
+```
+postgresql://app_user:secretpass@db.example.com:5432/production_db?sslmode=require
+```
+
+### Authentication Methods
+
+**Password file (.pgpass)**:
+
+```
+# ~/.pgpass (chmod 600)
+hostname:port:database:username:password
+localhost:5432:mydb:postgres:mypassword
+*.example.com:5432:*:appuser:apppass
+```
+
+**Connection via SSH tunnel**:
+
+```bash
+ssh -L 5432:localhost:5432 user@remote-host
+psql -U postgres -h localhost
+```
+
+### SSL/TLS Connection Options
+
+```bash
+# Require SSL
+psql -h hostname -sslmode require -U username database
+
+# Verify certificate
+psql -h hostname -sslmode verify-full \
+ -sslcert=/path/to/client-cert.crt \
+ -sslkey=/path/to/client-key.key \
+ -sslrootcert=/path/to/ca-cert.crt database
+
+# SSL modes: disable, allow, prefer (default), require, verify-ca, verify-full
+```
+
+## Meta-Commands
+
+### Database and Schema Navigation
+
+```
+\l or \list # List all databases
+\l+ or \list+ # List databases with sizes
+\c or \connect DATABASE USER # Connect to different database
+\dn or \dn+ # List schemas (namespaces)
+\dt or \dt+ # List tables in current schema
+\di or \di+ # List indexes
+\dv or \dv+ # List views
+\dm or \dm+ # List materialized views
+\ds or \ds+ # List sequences
+\df or \df+ # List functions/procedures
+\da or \da+ # List aggregates
+\dT or \dT+ # List data types
+\dF or \dF+ # List text search configurations
+```
+
+### Object Inspection Commands
+
+```
+\d or \d NAME # Describe table, view, index, sequence, or function
+\d+ or \d+ NAME # Extended description with details
+\da PATTERN # List aggregate functions matching pattern
+\db or \db+ # List tablespaces
+\dc or \dc+ # List character set encodings
+\dC or \dC+ # List type casts
+\dd or \dd+ # List object descriptions/comments
+\dD or \dD+ # List domains
+\de or \de+ # List foreign data wrappers
+\dE or \dE+ # List foreign servers
+\dF or \dF+ # List text search configurations
+\dFd or \dFd+ # List text search dictionaries
+\dFp or \dFp+ # List text search parsers
+\dFt or \dFt+ # List text search templates
+\dg or \dg+ # List database roles/users
+\dl or \dl+ # List large objects (same as \lo_list)
+\dL or \dL+ # List procedural languages
+\dO or \dO+ # List collations
+\dp or \dp+ # List table access privileges
+\dRp or \dRp+ # List replication origins
+\dRs or \dRs+ # List replication subscriptions
+\ds or \ds+ # List sequences
+\dt or \dt+ # List tables
+\dU or \dU+ # List user mapping
+\du or \du+ # List roles
+\dv or \dv+ # List views
+\dx or \dx+ # List extensions
+\dX or \dX+ # List extended statistics
+```
+
+### Formatting and Output Commands
+
+```
+\a # Toggle between aligned and unaligned output
+\C [STRING] # Set table title
+\f [STRING] # Set field separator for unaligned output
+\H # Toggle HTML output mode
+\pset OPTION [VALUE] # Set output option (detailed below)
+\t [on|off] # Toggle tuple-only output (no headers/footers)
+\T [STRING] # Set HTML table tag attributes
+\x or \x [on|off|auto] # Toggle expanded/vertical output
+\g or \g [FILENAME|COMMAND] # Execute query and send output to file/command
+```
+
+### File and History Commands
+
+```
+\copy QUERY TO FILENAME [FORMAT] # Client-side COPY (requires fewer permissions)
+\copy QUERY TO STDOUT # Copy to standard output
+\copy TABLE FROM FILENAME [FORMAT] # Import data from file
+\e or \edit # Edit current query buffer in editor
+\e FILENAME # Edit file in editor
+\ef [FUNCNAME] # Edit function definition
+\ev [VIEWNAME] # Edit view definition
+\w FILENAME or \write FILENAME # Write current query buffer to file
+\i FILENAME or \include FILENAME # Execute SQL commands from file
+\ir FILENAME or \include_relative FILE # Execute relative path file
+\s [FILENAME] # Show command history (or save to file)
+\o FILENAME or \out FILENAME # Send all output to file
+\o # Return output to terminal
+```
+
+### Batch and Script Commands
+
+```
+\echo TEXT # Print text (useful in scripts)
+\errverbose # Show last error in verbose form
+\q or \quit # Quit psql
+\! COMMAND or \shell COMMAND # Execute shell command
+\cd DIRECTORY # Change working directory
+\pwd # Print current working directory
+\set VARIABLE VALUE # Set psql variable
+\unset VARIABLE # Unset psql variable
+\setenv VARNAME VALUE # Set environment variable
+\getenv VARNAME # Get environment variable value
+\prompt [TEXT] VARIABLE # Prompt user for input and set variable
+```
+
+### Transaction Commands
+
+```
+\begin or BEGIN # Start transaction
+\commit or COMMIT # Commit transaction
+\rollback or ROLLBACK # Rollback transaction
+\savepoint NAME # Create savepoint
+\release SAVEPOINT # Release savepoint
+\rollback TO SAVEPOINT # Rollback to savepoint
+```
+
+### Information Commands
+
+```
+\d+ TABLENAME # Show table with extended info and storage info
+\dt *.* # List all tables in all schemas
+\dn * # List all schemas
+\du # List all users/roles
+\db # List tablespaces
+\dx # List installed extensions
+\h or \help # List available SQL commands
+\h COMMAND or \help COMMAND # Show help for specific SQL command
+\? # Show psql help
+\copyright # Show PostgreSQL copyright/license info
+\version or SELECT version() # Show PostgreSQL version
+```
+
+## Output and Formatting
+
+### \pset Options
+
+(See examples in configuration below for common settings like border, linestyle, and expanded mode.)
+
+### Output Formats Comparison
+
+```
+-- Aligned (default)
+\pset format aligned
+
+-- CSV
+\pset format csv
+\copy (SELECT * FROM users) TO STDOUT WITH (FORMAT CSV);
+
+-- HTML
+\pset format html
+SELECT * FROM users LIMIT 5;
+
+-- LaTeX
+\pset format latex
+SELECT * FROM users LIMIT 5;
+
+-- Expanded (vertical)
+\x
+SELECT * FROM users LIMIT 1;
+```
+
+## Files, Editor, and Scripting
+
+### Scripting with psql
+
+#### Running SQL Files
+
+```bash
+# Execute file
+psql -d mydb -f script.sql
+
+# Execute with output to file
+psql -d mydb -f script.sql -o results.txt
+
+# Execute with error stopping
+psql -d mydb -f script.sql --on-error-stop
+
+# Execute in single transaction
+psql -d mydb -f script.sql -s
+
+# Multiple files (executed in order)
+psql -d mydb -f init.sql -f seed.sql -f verify.sql
+```
+
+#### SQL Script Best Practices
+
+```sql
+-- sample_script.sql
+
+-- Set execution mode
+\set ON_ERROR_STOP ON
+\set QUIET OFF
+
+-- Drop existing objects if needed
+DROP TABLE IF EXISTS temp_table;
+
+-- Create table
+CREATE TABLE temp_table (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL
+);
+
+-- Insert data
+INSERT INTO temp_table (name) VALUES
+ ('Record 1'),
+ ('Record 2'),
+ ('Record 3');
+
+-- Verify results
+SELECT * FROM temp_table;
+
+-- Cleanup
+DROP TABLE temp_table;
+
+-- Report
+\echo 'Script completed successfully!'
+```
+
+#### Dynamic SQL Scripts
+
+```bash
+#!/bin/bash
+
+# Bash script with psql variables
+DATABASE="myapp_db"
+TABLE_NAME="users"
+SCHEMA_NAME="public"
+
+# Execute with variable substitution
+psql -d $DATABASE -v table_name=$TABLE_NAME \
+ -v schema_name=$SCHEMA_NAME -c "
+ SELECT COUNT(*) FROM :schema_name.:table_name;
+"
+
+# Loop through databases
+for db in $(psql -l | awk '{print $1}'); do
+ if [[ ! "$db" =~ "template" ]]; then
+ echo "Backing up $db..."
+ pg_dump $db > /backups/$db.sql
+ fi
+done
+```
+
+### Useful .psqlrc Shortcuts
+
+```bash
+# Add to ~/.psqlrc for convenient shortcuts
+\set dbsize 'SELECT pg_size_pretty(pg_database_size(current_database()))'
+\set uptime 'SELECT now() - pg_postmaster_start_time() AS uptime'
+\set psql_version 'SELECT version()'
+\set table_sizes 'SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'\''.\'\'||tablename)) FROM pg_tables ORDER BY pg_total_relation_size(schemaname||'\''.\'\'||tablename) DESC'
+
+# Usage in psql:
+# :dbsize
+# :table_sizes
+```
+
+### Configuration File (~/.psqlrc)
+
+```bash
+# Auto-load on psql startup
+# Set default options
+\set QUIET ON
+\set SQLHISTSIZE 10000
+
+# Configure output
+\pset null '[NULL]'
+\pset border 2
+\pset linestyle unicode
+\pset expanded auto
+\pset pager always
+
+# Define useful variables
+\set conn_user 'SELECT current_user;'
+\set dbsize 'SELECT pg_size_pretty(pg_database_size(current_database()));'
+\set tables 'SELECT tablename FROM pg_tables WHERE schemaname = ''public'';'
+\set functions 'SELECT proname FROM pg_proc;'
+
+# Define shortcuts
+\set uptime 'SELECT now() - pg_postmaster_start_time() AS uptime;'
+\set locks 'SELECT pid, usename, pg_blocking_pids(pid) as blocked_by, query, state FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0;'
+
+# Set timing
+\timing ON
+
+# Connect to default database
+\c mydb
+```
+
+### Variable Substitution
+
+```sql
+-- Using :variable syntax
+\set table_name mytable
+SELECT * FROM :table_name;
+
+-- Using :'variable' for literal strings
+\set schema_name public
+SELECT * FROM :"schema_name".mytable;
+
+-- Using :'variable' syntax in string context
+\set username 'postgres'
+SELECT * FROM pg_tables WHERE tableowner = :'username';
+
+-- Using :' ' for identifier quoting
+\set id_name "customTable"
+SELECT * FROM :"id_name";
+```
+
+### Prompt Customization
+
+```bash
+# Set custom prompts
+psql -v PROMPT1='user@db> '
+psql -v PROMPT1='%/%R%# ' # database/role#
+
+# In .psqlrc
+\set PROMPT1 '%n@%m:%>/%/ %R%# '
+\set PROMPT2 '> '
+\set PROMPT3 '>> '
+```
+
+### Built-in Variables
+
+```bash
+# Prompt variables
+psql -v PROMPT1='%/%R%# ' # Set primary prompt
+psql -v PROMPT2='%R%# ' # Set continuation prompt
+psql -v PROMPT3='>> ' # Set output mode prompt
+
+# Prompt expansion codes:
+# %n = Database user name
+# %m = Database server hostname (first part)
+# %> = Database server hostname full
+# %p = Database server port
+# %d = Database name
+# %/ = Current schema
+# %~ = Like %/ but ~ if schema matches user name
+# %# = # if superuser, > otherwise
+# %? = Last query result status
+# %% = Literal %
+# %[..%] = Invisible characters (for terminal control sequences)
+```
+
+## Command-Line Flags
+
+### Connection Options
+
+```bash
+-h, --host=HOSTNAME # Server host name (default: localhost)
+-p, --port=PORT # Server port (default: 5432)
+-U, --username=USERNAME # PostgreSQL user name (default: $USER)
+-d, --dbname=DBNAME # Database name to connect
+-w, --no-password # Never prompt for password
+-W, --password # Force password prompt
+```
+
+### Output and Formatting Options
+
+```bash
+-A, --no-align # Unaligned table output mode
+-c, --command=COMMAND # Run single command and exit
+-C, --copy-only # (deprecated, use \copy instead)
+-d, --dbname=DBNAME # Specify database
+-E, --echo-hidden # Display internal queries
+-e, --echo-all # Display each command before sending
+-b, --echo-errors # Display failed commands
+-f, --file=FILENAME # Execute commands from file
+-F, --field-separator=CHAR # Set field separator for unaligned output
+-H, --html # HTML table output mode
+-l, --list # List available databases and exit
+-L, --log-file=FILENAME # Log session to file
+-n, --no-readline # Disable readline (line editing)
+-o, --output=FILENAME # Write results to file
+-P, --pset=VARIABLE=VALUE # Set printing option
+-q, --quiet # Run quietly (no banner, single-line mode)
+-R, --record-separator=CHAR # Set record separator for unaligned output
+-S, --single-step # Single-step mode (confirm each command)
+-s, --single-transaction # Execute file in single transaction
+-t, --tuples-only # Print rows only (no headers/footers)
+-T, --table-attr=STRING # Set HTML table tag attributes
+-v, --set=VARIABLE=VALUE # Set psql variable
+-V, --version # Show version and exit
+-x, --expanded # Expanded table output mode
+-X, --no-psqlrc # Do not read ~/.psqlrc startup file
+-1, --single-line # End of line terminates SQL command
+```
+
+### Other Options
+
+```bash
+-a, --all # (deprecated)
+-j, --job=NUM # (for parallel dumps with pg_dump)
+--help # Show help message
+--version # Show version
+--on-error-stop # Stop on first error
+```
+
+## Import and Export
+
+### COPY Commands
+
+```sql
+-- Server-side COPY (requires superuser for file operations)
+COPY users (id, name, email)
+TO '/tmp/users.csv'
+WITH (FORMAT CSV, HEADER TRUE, QUOTE '"', ESCAPE '\\');
+
+-- Import CSV
+COPY users (id, name, email)
+FROM '/tmp/users.csv'
+WITH (FORMAT CSV, HEADER TRUE, QUOTE '"', ESCAPE '\\');
+
+-- Tab-separated values
+COPY users TO '/tmp/users.tsv' WITH (FORMAT TEXT, DELIMITER E'\t');
+
+-- With NULL handling
+COPY users TO '/tmp/users.csv'
+WITH (FORMAT CSV, NULL 'N/A', QUOTE '"');
+```
+
+### Client-side COPY (\copy)
+
+```bash
+# Export to CSV (from psql)
+\copy users TO '/home/user/users.csv' WITH (FORMAT CSV, HEADER)
+
+# Export with query results
+\copy (SELECT id, name, email FROM users WHERE active = true) \
+ TO '/tmp/active_users.csv' WITH (FORMAT CSV, HEADER)
+
+# Import CSV
+\copy users (id, name, email) FROM '/tmp/users.csv' WITH (FORMAT CSV, HEADER)
+
+# Export to stdout (pipe to file)
+\copy users TO STDOUT WITH (FORMAT CSV, HEADER) > users.csv
+
+# Import from stdin
+cat users.csv | \copy users FROM STDIN WITH (FORMAT CSV, HEADER)
+```
+
+## Backup and Restore
+
+### Using pg_dump and pg_restore
+
+```bash
+# Dump entire database
+pg_dump -d mydb -U postgres > mydb_backup.sql
+
+# Dump with custom format (compressed)
+pg_dump -d mydb -Fc > mydb_backup.dump
+
+# Dump specific table
+pg_dump -d mydb -t users > users_backup.sql
+
+# Dump with data only
+pg_dump -d mydb -a > mydb_data.sql
+
+# Dump schema only
+pg_dump -d mydb -s > mydb_schema.sql
+
+# Restore from SQL file
+psql -d mydb_restored -f mydb_backup.sql
+
+# Restore from custom format
+pg_restore -d mydb_restored mydb_backup.dump
+
+# List contents of dump
+pg_restore -l mydb_backup.dump
+```
+
+### Backup and Recovery
+
+#### Database Backup Strategies
+
+```bash
+# Full database backup (custom format)
+pg_dump -d production_db -Fc -j 4 > backup.dump
+
+# Backup with compression
+pg_dump -d production_db -Fc -Z 9 > backup.dump
+
+# Parallel backup (faster for large databases)
+pg_dump -d production_db -Fd -j 4 -f backup_dir
+
+# Backup specific schemas
+pg_dump -d production_db -n public -n app > schemas.sql
+
+# Backup with custom format (allows selective restore)
+pg_dump -d production_db -Fc > backup.dump
+
+# View backup contents
+pg_restore -l backup.dump | less
+
+# Restore specific table
+pg_restore -d restored_db -t users backup.dump
+
+# List available backups
+pg_dump -U postgres -l -w postgres
+```
+
+#### Point-in-Time Recovery
+
+```bash
+# Full backup
+pg_dump -d mydb > base_backup.sql
+
+# Enable WAL archiving (in postgresql.conf)
+wal_level = replica
+archive_mode = on
+archive_command = 'cp %p /archive/%f'
+
+# Restore to point in time
+pg_restore -d recovered_db base_backup.sql
+# Then apply WAL files up to target time
+```
+
+## Performance and Debug Toolbox
+
+### Performance and Debugging
+
+#### Query Analysis
+
+```sql
+-- Show query execution plan
+EXPLAIN SELECT * FROM users WHERE id = 1;
+
+-- Detailed analysis with actual execution
+EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;
+
+-- Show more details
+EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
+ SELECT * FROM users WHERE active = true;
+
+-- JSON output for programmatic parsing
+EXPLAIN (FORMAT JSON, ANALYZE)
+ SELECT COUNT(*) FROM users;
+```
+
+#### Viewing Query Performance
+
+```sql
+-- Current queries
+SELECT pid, usename, state, query FROM pg_stat_activity;
+
+-- Long-running queries
+SELECT pid, usename, now() - query_start AS duration, query
+FROM pg_stat_activity
+WHERE state != 'idle'
+ORDER BY query_start;
+
+-- Blocking queries
+SELECT blocked_pid, blocking_pid, blocked_statement, blocking_statement
+FROM pg_stat_statements;
+
+-- Table sizes
+SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
+FROM pg_tables
+ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
+
+-- Database size
+SELECT pg_size_pretty(pg_database_size('mydb'));
+```
+
+#### Setting Timing
+
+```bash
+# Enable query timing
+\timing ON
+
+# Disable query timing
+\timing OFF
+
+# In batch mode
+psql -d mydb -c "\timing ON" -f script.sql
+```
+
+#### Query Logging
+
+```bash
+# Log all queries to file
+psql -d mydb -L query.log -f script.sql
+
+# Show internal queries (system queries)
+psql -d mydb -E
+```
+
+## Troubleshooting
+
+### Connection Issues
+
+```bash
+# Verbose connection diagnostics
+psql -d mydb -v verbose=on --echo-queries
+
+# Check connection settings
+psql --version
+psql -d postgres -c "SHOW password_encryption;"
+
+# TCP/IP connectivity test
+psql -h hostname -d postgres -U postgres -c "SELECT 1;"
+```
+
+### Common Error Messages
+
+```
+FATAL: password authentication failed
+ β Check password, user exists, .pgpass has correct permissions (600)
+
+FATAL: no pg_hba.conf entry for host
+ β Database server's pg_hba.conf needs connection rule
+
+FATAL: database "name" does not exist
+ β Create database or check database name spelling
+
+ERROR: permission denied for schema
+ β Grant USAGE on schema to user
+
+ERROR: syntax error
+ β Check SQL syntax, use \h for help on commands
+```
+
+### Performance Issues
+
+```sql
+-- Find slow queries
+SELECT * FROM pg_stat_statements
+ORDER BY total_time DESC
+LIMIT 10;
+
+-- Check for missing indexes
+SELECT schemaname, tablename, attname
+FROM pg_stat_user_tables, pg_attribute
+WHERE pg_stat_user_tables.relid = pg_attribute.attrelid
+AND seq_scan > 0;
+
+-- Check cache efficiency
+SELECT
+ sum(heap_blks_read) as heap_read,
+ sum(heap_blks_hit) as heap_hit,
+ sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS ratio
+FROM pg_statio_user_tables;
+```
+
+## Resources
+
+- Official PostgreSQL Documentation: https://www.postgresql.org/docs/
+- psql Manual: https://www.postgresql.org/docs/current/app-psql.html
+- PostgreSQL Wiki: https://wiki.postgresql.org/
+- pgAdmin (GUI tool): https://www.pgadmin.org/
+- DBA Best Practices: https://www.postgresql.org/docs/current/sql-syntax.html
diff --git a/ai-skills/repo-map/REFERENCE.md b/ai-skills/repo-map/REFERENCE.md
new file mode 100644
index 0000000..33a8503
--- /dev/null
+++ b/ai-skills/repo-map/REFERENCE.md
@@ -0,0 +1,107 @@
+# Repo Map Reference β Connections and IaC
+
+This document provides deeper context on how the Nibiru repositories connect, specifically focusing on how `nibi-iac` (Infrastructure as Code) manages the deployment of application services.
+
+## Architecture and Deployment Flow
+
+The `nibi-iac` repository sits at the deployment layer. It consumes the artifacts or logic from application repositories and provisions the live cloud infrastructure (GCP), DNS, and endpoints.
+
+## Key Service Connections
+
+| App Repo | iac Module | Description |
+|----------|------------|-------------|
+| **nibi-go-hm** | `go-hm-graphql`, `heart-monitor` | Deploys the Heart Monitor indexer and its GraphQL interface. Defines `domain` for public access. |
+| **sai-keeper** | `sai-keeper` | Deploys the Sai Keeper GraphQL/REST API. Manages `domain_graphql` and `domain_rest`. |
+| **sai-trading-bot** | `sai-trading-bot`, `sai-trading-evm` | Deploys automation services for Sai exchange trading. |
+| **nibi-chain** | `validator`, `fullnode`, `cosmopilot` | Provisions various blockchain node types and their Kubernetes management. |
+| **nibi-installer** | `nibiru-installer` | Deploys the service behind `get.nibiru.fi`. |
+| **networks-info** | `networks-info` | Deploys the service behind `networks.nibiru.fi`. |
+
+### Supplementary Modules
+Other key modules in `terraform/_modules/`:
+- **Core**: `cloudsql` (PostgreSQL), `vpn-gateway`, `vault-cluster-gke`.
+- **DApps/Helpers**: `faucet`, `pricefeeder`, `relayer`, `passkey-bundler`, `dextools`.
+- **Operations**: `tenderduty`, `status-page`, `ping-pub`.
+
+## Important Infrastructure Paths in `nibi-iac`
+
+- **DNS Configuration**:
+ - Mainnet/Shared: `terraform/nibiru-gcp/02-dns.tf` (Zones: `nibiru.fi`, `nibiru.org`)
+ - Environment-specific (e.g. `*.internal.main.nibiru.fi`): `terraform/-gcp/02-dns.tf`
+- **Environment Entry Points**:
+ - `terraform/mainnet-gcp/`: Production environment
+ - `terraform/itn2-gcp/`: Public testnet
+ - `terraform/dev-gcp/`: Development and internal testnets
+- **Reusable Modules**:
+ - `terraform/_modules/`: Contains the logic for deploying each component (Sai, Indexer, Nodes).
+
+## Common Domain and Endpoint Patterns
+
+The following patterns describe how domains are structured across environments and services:
+
+### Public Domains
+Defined in `terraform/nibiru-gcp/02-dns.tf`. CNAMEs typically point to environment-specific ingresses.
+
+- **Mainnet**: `*.nibiru.fi` (e.g., `main.nibiru.fi`, `rpc.nibiru.fi`, `hm-graphql.nibiru.fi`, `sai-keeper.nibiru.fi`, `sai-api.nibiru.fi`).
+- **Mainnet Internal**: `*.internal.main.nibiru.fi` (Grafana, Prometheus, node internal endpoints).
+- **Testnet (itn2)**: `testnet-2.nibiru.fi`, `hm-graphql.testnet-2.nibiru.fi`, `sai-keeper.testnet-2.nibiru.fi`, `*.internal.test.nibiru.fi`.
+- **Dev**: `devnet-1.nibiru.fi`, `*.internal.dev.nibiru.fi`, `networks.devnet.nibiru.fi`.
+
+### Private DNS (.nibiru.local)
+Accessible via VPN or Tailscale:
+- **Mainnet**: `*.main.nibiru.local`
+- **Testnet**: `*.testnet.nibiru.local`
+- **Dev**: `*.dev.nibiru.local`
+
+### Key Service Domains and Repos
+- `hm-graphql.*.nibiru.fi` β Heart Monitor indexer GraphQL (`nibi-go-hm`)
+- `sai-keeper.*.nibiru.fi`, `sai-api.*.nibiru.fi` β Sai Keeper (`sai-keeper`)
+- `networks.*.nibiru.fi` β `networks-info` helper
+- `get.nibiru.fi` β `nibiru-installer`
+- `grafana.internal.*.nibiru.fi`, `prometheus.internal.*.nibiru.fi` β Internal monitoring
+
+## Environment Layout and Locations
+
+Infrastructure is split between a shared hub and environment-specific spokes:
+
+### `nibiru-gcp` (Shared Services)
+The "hub" environment that hosts common infrastructure used across all chain projects:
+- **Core DNS**: Managed zones for `nibiru.fi`, `nibiru.org`, `nibiscan.io`. Entry: `terraform/nibiru-gcp/02-dns.tf`.
+- **Shared Tools**: Terraform Cloud agent, SonarQube, VPN/Tailscale gateway, common observability resources.
+- **Note**: This environment **does not** host chain workloads (nodes, indexers).
+
+### Chain Environments (`mainnet-gcp`, `itn2-gcp`, `dev-gcp`)
+These spoke environments host chain workloads, indexers, and application-specific logic:
+- **`mainnet-gcp`**: Production mainnet chain and services (e.g., Cataclysm). Local domain: `main.nibiru.local`.
+- **`itn2-gcp`**: Public testnet chain and services (e.g., testnet-2). Local domain: `testnet.nibiru.local`.
+- **`dev-gcp`**: Internal devnets and early-stage testnets (e.g., devnet-1). Local domain: `dev.nibiru.local`.
+
+Each spokes is peered with the `nibiru-gcp` hub but remains isolated from other chain projects.
+
+## Grafana Dashboards
+
+Dashboards are version-controlled in this repository to ensure disaster recovery and consistency across environments:
+
+- **Mainnet**:
+ - General: `terraform/mainnet-gcp/grafana-resources/grafana_dashboards/`
+ - Sai Specific: `terraform/mainnet-gcp/grafana-resources/grafana_dashboards/sai/` (e.g., perp, slp, reserves, volume, liquidations).
+- **Testnet**: `terraform/itn2-gcp/grafana-resources/grafana_dashboards/`
+- **Dev**: `terraform/dev-gcp/grafana-resources/grafana_dashboards/`
+
+**Grafana UI Access (VPN Required)**:
+- Mainnet: `grafana.internal.main.nibiru.fi`
+- Testnet: `grafana.internal.test.nibiru.fi`
+- Dev: `grafana.internal.dev.nibiru.fi`
+
+## When to route to `nibi-iac`
+- Investigating live endpoint URLs or DNS record settings.
+- Changing GKE cluster configurations or resource limits.
+- Updating Grafana dashboards or PagerDuty alert policies.
+- Mapping how a new application service should be wired into the existing infrastructure.
+
+## Deployment Workflow
+
+- **Terraform Cloud**: State management and operations are centralized in Terraform Cloud. Merges to the `main` branch trigger automatic `terraform apply`. Pull requests automatically generate a `terraform plan` viewable in the Terraform Cloud workspace.
+- **Pre-Commit Hooks**: The repository uses `pre-commit` to run `terraform-docs`, `tflint`, security checks (`tfsec`, `checkov`), and cost estimation (`infracost`) before each commit.
+- **Modules**: Reusable modules in `terraform/_modules/` are published to Terraform Cloud and version-tagged for controlled reuse.
+
diff --git a/ai-skills/repo-map/SKILL.md b/ai-skills/repo-map/SKILL.md
new file mode 100644
index 0000000..938cc00
--- /dev/null
+++ b/ai-skills/repo-map/SKILL.md
@@ -0,0 +1,63 @@
+---
+name: repo-map
+description: >-
+ Maps and routes between key repositories on my machine (boku, nibi-chain,
+ nibi-geth, nibi-go-hm, nibi-ts-sdk, sai-website, sai-perps, nibi-iac,
+ gh-io-ud, wasm-cosmwasm, wasm-go-wasmvm). Use when the user asks which repo to look at,
+ where a feature lives, how repos connect, how services are deployed
+ (Terraform/IaC), when a task spans chain, EVM, indexer, SDK, and frontend, or
+ when the user asks about the personal site, blog posts, or Astro/gh-pages.
+---
+
+# Repo Map β Nibiru and Personal Repos
+
+Use this skill to quickly route questions/tasks to the correct repo(s) under path `$HOME/ki/`.
+
+## Quick routing
+
+### Blockchain and EVM Impl
+- `$HOME/ki/nibi-chain`: **Chain logic, modules, and Nibiru CLI (`nibid`)**. This repo
+defines the logic, and you can run `nibid tx`, `nibid query`, or other commands in
+the terminal to inspect exact functionality. Core L1 chain (Go). App wiring +
+custom modules. Includes `cmd/nibid`. | `x/`, `app/`, `proto/`, `cmd/nibid/`
+- `$HOME/ki/nibi-chain/x/evm`: **Nibiru EVM execution and precompiles live here.**. RPC is standard for the EVM.
+- `$HOME/ki/nibi-geth`: **EVM state transition internals that do not involve precompiles; go-ethereum logic**
+- `$HOME/ki/wasm-cosmwasm`: **CosmWasm repo**. Official GitHub name "CosmWasm/cosmwasm".
+Defines the Wasmer version used via `cosmwasm-vm`'s `Cargo.toml`.
+- `$HOME/ki/wasm-go-wasmvm`: **WasmVM repo**. Official GitHub name "CosmWasm/wasmvm".
+Nibiru connects to this repo via the `github.com/CosmWasm/wasmvm` dependency in
+`nibi-chain/go.mod` (currently version 1.5.9). `libwasmvm` depends on `cosmwasm`
+transitively to pull in `wasmer`. The relationship is: `nibi-chain` -> `wasmvm`
+-> `cosmwasm` -> `wasmer`.
+
+### Nibiru Indexer and TypeScript SDK
+- `$HOME/ki/nibi-go-hm`:
+ - **Indexer server (Heart Monitor) and GraphQL schema**. This is the Nibiru
+ Indexer schema's source of truth. `graphql/graph/*.graphqls` (e.g. `staking.graphqls`, `user.graphqls`)
+- `$HOME/ki/nibi-ts-sdk`: **TypeScript SDK (`@nibiruchain/nibijs`)** and Nibiru
+Indexer (HeartMonitor) client. Look for `HeartMonitor`, generated `GQL*` types,
+and query builders.
+
+### Sai Exchange
+- `$HOME/ki/sai-website/webapp`: **Sai: web app** (lives in monorepo). Web
+frontend and API integrations. This uses the "sai-keeper" GraphQL interface.
+- `$HOME/ki/sai-perps`: **Official Sai protocol contracts** (EVM + Wasm). This repo defines what gets deployed onchain; the web app and sai-keeper both talk to these contracts. Used by trading tools and integrations.
+- `$HOME/ki/sai-keeper`: Defines the "sai-keeper" GraphQL interface for Sai.
+- `$HOME/ki/nibi-chain/sai-trading`: **Sai: Trading bot automation service**
+- `$HOME/ki/sai-docs`: Public-facing user documentation for Sai. Great conceptual
+ resource.
+
+### Infrastructure (IaC)
+- `$HOME/ki/nibi-iac`: **Infrastructure as code (Terraform)**. Source of truth for live deployments,
+ DNS, and endpoints. Provisions GKE clusters, RPC/archive endpoints, indexer (Heart Monitor) GraphQL,
+ sai-keeper GraphQL/REST, monitoring (Grafana, PagerDuty). For detailed repo connections and
+ how nibi-iac deploys other services, see [REFERENCE.md](./REFERENCE.md). | `terraform/` (environments:
+ `dev-gcp`, `itn2-gcp`, `mainnet-gcp`, `nibiru-gcp`), `terraform/_modules/` (e.g. `sai-keeper`,
+ `go-hm-graphql`, `heart-monitor`)
+
+### Personal, Blog, Boku Workspace
+- `$HOME/ki/boku`: **Personal knowledge base and project hub.** Monorepo for sandboxing, backups, and external brain. Use when working on epics, specs, Nibiru notes, focustime, or scripts that span nibi-chain, sai-perps, sai-website. Contains: `epics/`, `nibi/` (addr-book, cook), `free/` (todos, journal), `sde/`, `jiyuu/` (focustime, mdtoc), `scripts/`. Bun/TS + Go + Rust. Run `just test`, `just install`.
+- `$HOME/ki/gh-io-ud`: **Personal site (GitHub Pages).** Astro 4 + Tailwind. Blog
+ content in `src/content/post/` (schema in `src/content/config.ts`). Site config
+ `src/config.yaml`, nav `src/navigation.js`. Run `just dev`, build `just b`,
+ deploy `just deploy`.
diff --git a/ai-skills/sai-db/SKILL.md b/ai-skills/sai-db/SKILL.md
new file mode 100644
index 0000000..92a345a
--- /dev/null
+++ b/ai-skills/sai-db/SKILL.md
@@ -0,0 +1,169 @@
+---
+name: sai-db
+description: >-
+ Comprehensive reference for the sai-keeper Postgres database schema,
+ migrations, tables, columns, and SQL types. Use when the user asks about the
+ database structure, specific tables, field meanings, or how on-chain data is
+ stored in the Sai exchange.
+metadata:
+ tags: ["sai-perps", "sai-keeper", "indexer"]
+ agent_skills: # related skills
+ - sai-keeper-graphql
+ - sai-rest-api
+---
+
+# Sai DB Reference
+
+## How to use this skill
+
+1. **Look up tables/columns**: See [schema.md](schema.md) for a domain-by-domain breakdown (Oracle, Perp, LP, Referral, Stats).
+2. **Understand data patterns**: See [references/conventions.md](references/conventions.md) for common coordinates (block, tx, event), units, and partitioning.
+3. **Find where data surfaces**: See [references/api-surfacing.md](references/api-surfacing.md) for the mapping between DB tables and API endpoints.
+
+### Direct Database Access (psql)
+
+Use direct SQL when validating endpoint behavior, unit semantics, and aggregate freshness.
+
+```bash
+SAI_DB="sai_keeper" # or "sai_keeper_2" depending on time
+```
+
+- **Local default DB name**: `sai_keeper` (override as needed) OR `sai_keeper_2`
+ if specified by user.
+- **Basic connect**: `psql -d sai_keeper`
+- **Explicit connect**: `psql -U -h -p 5432 -d sai_keeper`
+- **Run one query inline**: `psql -d sai_keeper -c "SELECT 1 AS ok;"`
+
+Useful flags:
+
+- `-X`: do not read `~/.psqlrc` (reproducible output)
+- `-P pager=off`: disable pager for long results
+- `-A -t -F ','`: script-friendly unaligned output
+- `-c ""`: execute a SQL command non-interactively
+
+Suggested debugging workflow:
+
+1. Verify row freshness first (`MAX(ts)`, `COUNT(*) WHERE ts=current_date`).
+2. Compare API metric source table to a live recompute query.
+3. Break totals by `collateral_id`/`market_id` to locate outliers quickly.
+
+## Connections to APIs
+
+This database powers two primary APIs. For usage instructions and query patterns, see their respective skills:
+
+- **[sai-keeper-graphql]($HOME/.cursor/skills/sai-keeper-graphql/SKILL.md)**: Most tables are exposed via GraphQL queries and subscriptions.
+- **[sai-rest-api]($HOME/.cursor/skills/sai-rest-api/SKILL.md)**: High-level metrics (volume, OI, TVL) are served via REST primarily from `stats_*` table queries (with `stats_cache` as legacy/auxiliary context).
+
+## Quick Start
+
+Use [schema.md](schema.md) to see all available tables with explanations for each column.
+
+
+### Data Source
+
+The **sai-keeper** database schema is the source of truth for the Sai exchange's indexed data. The schema is defined by SQL migrations in the `sai-keeper` repository.
+
+- **Schema Location**: `$HOME/ki/sai-keeper/models/migrations/`
+- **Routines/Stored Procs**: `$HOME/ki/sai-keeper/models/routines/` (maintains stats/aggregates)
+
+## High-value SQL Snippets
+
+### 1) Open positions vs current-day OI snapshot
+
+```sql
+SELECT
+ (SELECT COUNT(*) FROM perp_trade WHERE is_open = true AND trade_type = 'trade') AS open_positions,
+ (SELECT COALESCE(SUM(oi_long_usd + oi_short_usd), 0) FROM stats_perp_oi_daily WHERE ts = current_date) AS oi_today_usd;
+```
+
+### 2) Check daily OI freshness
+
+```sql
+SELECT
+ current_date AS today,
+ MAX(ts) AS latest_oi_day,
+ COUNT(*) FILTER (WHERE ts = current_date) AS rows_for_today
+FROM stats_perp_oi_daily;
+```
+
+### 3) Compare "today only" vs latest available OI
+
+```sql
+SELECT
+ COALESCE((
+ SELECT SUM(oi_long_usd + oi_short_usd)
+ FROM stats_perp_oi_daily
+ WHERE ts = current_date
+ ), 0) AS oi_today_usd,
+ COALESCE((
+ SELECT SUM(oi_long_usd + oi_short_usd)
+ FROM stats_perp_oi_daily
+ WHERE ts = (SELECT MAX(ts) FROM stats_perp_oi_daily WHERE ts <= current_date)
+ ), 0) AS oi_latest_available_usd;
+```
+
+### 4) OI by collateral on latest day
+
+```sql
+WITH d AS (
+ SELECT MAX(ts) AS ts
+ FROM stats_perp_oi_daily
+ WHERE ts <= current_date
+)
+SELECT
+ s.ts,
+ s.collateral_id,
+ ot.base AS collateral_symbol,
+ SUM(s.oi_long_usd + s.oi_short_usd) AS oi_usd
+FROM stats_perp_oi_daily s
+JOIN d ON s.ts = d.ts
+LEFT JOIN oracle_token ot ON ot.id = s.collateral_id
+GROUP BY s.ts, s.collateral_id, ot.base
+ORDER BY oi_usd DESC;
+```
+
+### 5) Top users by cumulative volume
+
+```sql
+SELECT
+ user_address,
+ SUM(volume_usd_long + volume_usd_short) AS cumulative_volume_usd
+FROM stats_perp_by_user
+GROUP BY user_address
+ORDER BY cumulative_volume_usd DESC
+LIMIT 50;
+```
+
+### 6) Top users by 30-day volume
+
+```sql
+SELECT
+ user_address,
+ SUM(volume_usd_long + volume_usd_short) AS volume_30d_usd
+FROM stats_perp_by_user
+WHERE ts >= current_date - INTERVAL '30 days'
+GROUP BY user_address
+ORDER BY volume_30d_usd DESC
+LIMIT 50;
+```
+
+## Known Pitfalls / Interpretation Notes
+
+- `open_positions` and `open_interest` can diverge:
+ - `open_positions` is live from `perp_trade`
+ - `open_interest` in REST stats comes from daily snapshots (`stats_perp_oi_daily`)
+ - If current day rows are missing, `open_interest` can read as `0` while open positions are non-zero.
+- Unit semantics:
+ - Base units are `bigint` (`collateral_amount`, `tvl`, etc).
+ - USD metrics require conversion: `base_units / 1e6 * price_usd` (this codebase currently assumes 6 decimals in several routines).
+- OI spikes can come from snapshot inclusion semantics:
+ - Daily OI procedure logic can include positions opened and closed on the same day.
+ - That inflates daily OI vs strict end-of-day "still open" interpretation.
+- `perp_borrowing` OI and `stats_perp_oi_daily` OI are related but not guaranteed identical at any instant; they are populated by different paths and timings.
+
+## Playbook for User Questions
+
+- **"What fields are in the perp_trade table?"** -> Consult `schema.md#perp_trade`.
+- **"How is volume calculated?"** -> Consult `conventions.md#units` and `surfacing.md#rest-api` (derived from `stats_perp_by_user`).
+- **"What fields are in the stats_perp_by_user table?"** -> Consult `schema.md#stats_perp_by_user`. Includes volume, PnL, and trade counts per user/market/collateral.
+- **"Where are liquidations stored?"** -> Liquidations are records in `perp_trade_history` with `trade_change_type = 'position_liquidated'`. See `schema.md#perp_trade_history`.
diff --git a/ai-skills/sai-db/references/api-surfacing.md b/ai-skills/sai-db/references/api-surfacing.md
new file mode 100644
index 0000000..f2bd327
--- /dev/null
+++ b/ai-skills/sai-db/references/api-surfacing.md
@@ -0,0 +1,46 @@
+# Skill sai-db: API Surfacing (GraphQL & REST)
+
+This document maps database tables to their respective API exposures in GraphQL and REST.
+
+## 1. GraphQL Surfacing
+The **sai-keeper-graphql** skill covers the API usage. Most DB tables map directly to GraphQL types.
+
+| DB Table | GraphQL Type / Root Field | Domain |
+|----------|---------------------------|--------|
+| `perp_trade` | `Trade` / `trade`, `trades` | Perp |
+| `perp_trade_history` | `TradeHistory` / `tradeHistory` | Perp |
+| `perp_borrowing` | `Borrowing` / `borrowing`, `borrowings` | Perp |
+| `lp_vault` | `Vault` / `vaults` | LP |
+| `lp_deposit_history` | `DepositHistory` / `depositHistory` | LP |
+| `oracle_price` | `TokenPriceUsd` / `tokenPricesUsd` | Oracle |
+| `perp_fee_charged_history` | `FeeTransaction` / `feeTransactions` | Fee |
+| `referral_code` | `ReferralCode` / `referralCodes` | Referral |
+
+**Note**: For live updates (Subscriptions), GraphQL watchers monitor these tables and push updates when rows change or are added.
+
+## 2. REST API Surfacing
+The **sai-rest-api** skill covers high-level metrics. In current code paths, `/dexpal/v1/stats` and `/dexpal/v1/metrics` are derived primarily from `stats_*` table queries in the API service layer.
+
+| DB Table | REST Endpoint | Metric(s) |
+|----------|---------------|-----------|
+| `stats_perp_by_user` | `/dexpal/v1/stats`, `/dexpal/v1/metrics` (indirect) | 24h/all-time volume, trades, users |
+| `stats_perp_oi_daily` | `/dexpal/v1/stats`, `/dexpal/v1/metrics` | Open interest (daily snapshot sum) |
+| `stats_perp_fee_daily` | `/dexpal/v1/stats`, `/dexpal/v1/metrics` | 24h/all-time accrued trading fees |
+| `lp_vault` + `stats_oracle_price` | `/dexpal/v1/stats`, `/dexpal/v1/metrics` | TVL (vault units converted to USD) |
+| `stats_slp_vault` | `/dexpal/v1/yield` | Vault APY, TVL, and descriptions |
+| `volume_leaderboard` | `/dexpal/v1/leaderboard` (if active) | Top traders by volume |
+| `stats_cache` | Legacy/auxiliary | Global cached metrics (not primary path for current stats endpoint implementation) |
+
+## 3. Data Refreshers
+Aggregated tables in the `stats` domain are maintained by stored procedures in `models/routines/`. These procs run periodically (e.g., via `statsig`) to pull data from "State" and "History" tables into the "Stats" tables.
+
+Key Procs:
+- `proc_update_stats_perp_by_user`: Aggregates trades into daily user stats.
+- `update_stats_perp_oi_daily`: Computes daily open interest snapshots in USD.
+- `proc_update_stats_perp_fee_daily`: Aggregates fee history into daily USD fees.
+- `proc_update_volume_leaderboard`: Maintains the leaderboard from trade history.
+- `func_refresh_stats_cache`: Updates `stats_cache` (legacy/auxiliary path).
+
+## See Also
+- [sai-keeper-graphql]($HOME/.cursor/skills/sai-keeper-graphql/SKILL.md)
+- [sai-rest-api]($HOME/.cursor/skills/sai-rest-api/SKILL.md)
diff --git a/ai-skills/sai-db/references/conventions.md b/ai-skills/sai-db/references/conventions.md
new file mode 100644
index 0000000..3f61cc6
--- /dev/null
+++ b/ai-skills/sai-db/references/conventions.md
@@ -0,0 +1,43 @@
+# Skill sai-db Conventions
+
+This document explains common patterns and semantics used across the `sai-keeper` database tables.
+
+## 1. Event Coordinates
+Most historical tables (ending in `_history`) use a set of coordinates to uniquely identify an on-chain event:
+- **block**: The block height.
+- **tx_index**: The index of the transaction within the block.
+- **event_index**: The index of the event within the transaction.
+
+Together, `(block, tx_index, event_index)` provide a stable order and ensure uniqueness for indexed events.
+
+## 2. Timestamps and Dates
+- **timestamptz**: Used for real-world time (e.g., `block_ts` in the `block` table).
+- **date**: Used in `stats_*` tables for daily buckets (e.g., `ts` in `stats_perp_by_user`).
+- **block**: Often used as a proxy for time in `_history` tables.
+
+## 3. "History" vs. "State" Tables
+- **State Tables** (e.g., `perp_trade`, `lp_vault`): Store the *current* state of an entity. They are updated in place as new events arrive.
+- **History Tables** (e.g., `perp_trade_history`, `lp_deposit_history`): Store an immutable log of changes. Each row represents a specific action or event.
+
+## 4. Units and Decimals
+- **Base Units (Int/Bigint)**: Fields like `collateral_amount`, `tvl`, and `available_assets` are stored in base units (e.g., 6 decimals for USDC, 18 for some EVM tokens).
+- **USD Values (Float8/Numeric)**: Fields like `price_usd`, `volume_usd`, and `realized_pnl_usd` are human-readable floats or high-precision numerics already scaled to USD.
+- **Percentages (Float8)**: Fields like `pnl_pct` or `spread_p` are stored as decimals (e.g., `0.01` for 1%).
+
+## 5. Partitioning
+The `oracle_price_history` table is partitioned by `block` range using the `pg_partman` extension. This ensures that queries over historical prices remain performant as the dataset grows.
+
+Migrations for partitioned tables usually include a call to `partman.create_parent`:
+```sql
+SELECT partman.create_parent(
+ p_parent_table => 'public.oracle_price_history',
+ p_control => 'block',
+ p_type => 'native',
+ p_interval => '100000',
+ ...
+);
+```
+
+## 6. Identifiers
+- **IDs**: Many protocol entities use `bigint` IDs assigned by the smart contracts (e.g., `perp_market_id`, `trade_id`).
+- **Addresses**: Trader and vault addresses are stored as `text` (e.g., Bech32 `nibi1...` or hex `0x...`).
diff --git a/ai-skills/sai-db/schema.md b/ai-skills/sai-db/schema.md
new file mode 100644
index 0000000..f6de2dd
--- /dev/null
+++ b/ai-skills/sai-db/schema.md
@@ -0,0 +1,432 @@
+# Sai DB Schema Reference
+
+This document provides a detailed breakdown of the tables and types in the `sai-keeper` database.
+
+## 1. Common (001_common.sql)
+
+### Tables
+- **schema_version**: Tracks the current migration version.
+ - `version`: bigint (PK)
+- **historical_sync**: Tracks blocks synced during historical indexing.
+ - `id`: bigserial (PK)
+ - `batch_start_block`: bigint
+ - `batch_end_block`: bigint
+ - `created_at`: timestamptz
+- **exchange_state**: Current operating state of the exchange.
+ - `state`: exchange_status (active, paused, close_only)
+ - `last_updated_block`: bigint
+- **exchange_config**: Protocol-wide configuration parameters.
+ - `max_pnl`, `max_sl`, `liquidation_threshold`: float8
+ - `fee_tiers`, `price_impact`: jsonb
+ - `last_updated_block`: bigint
+- **block**: Maps block numbers to timestamps.
+ - `block`: bigint (PK)
+ - `block_ts`: timestamptz
+- **tx**: Maps transaction hashes to blocks and indexes.
+ - `block`: bigint, `tx_index`: bigint (Composite PK)
+ - `tx_hash`: text, `evm_hash`: text
+
+### Types
+- **string_pair**: (key1 text, key2 text)
+- **exchange_status**: `active`, `paused`, `close_only`
+
+---
+
+## 2. Oracle (002_oracle.sql)
+
+### Tables
+- **oracle_token**: Metadata for assets tracked by the oracle.
+ - `id`: bigint (PK)
+ - `base`: text, `permission_group`: bigint
+- **oracle_token_info**: Visual/display metadata for tokens.
+ - `oracle_token_id`: bigint (PK, FK)
+ - `logo_url`: text, `symbol`: text, `trading_view_symbol`: text
+- **oracle_token_price_source**: External sources for token prices.
+ - `oracle_token_id`: bigint, `price_source`: text (Composite PK)
+ - `symbol`: text
+- **oracle_price**: Latest market price for each token.
+ - `oracle_token_id`: bigint (PK)
+ - `price_usd`: float8, `last_updated_block`: bigint
+- **oracle_price_history**: Time-series of oracle prices.
+ - `oracle_token_id`: bigint, `block`: bigint (Composite PK)
+ - `price_usd`: float8
+ - *Partitioned by block range.*
+
+---
+
+## 3. Perpetuals (003_perp.sql)
+
+### Tables
+- **perp_collateral**: Supported collateral assets.
+ - `oracle_token_id`: bigint (PK), `is_active`: boolean
+- **perp_market**: Trading pairs (e.g. BTC/USDC).
+ - `id`: bigint (PK)
+ - `base_token_id`, `quote_token_id`: bigint
+ - `spread_p`, `max_leverage`: float8
+ - `perp_market_group_id`, `perp_fee_id`: bigint
+- **perp_market_group**: Groups of markets (e.g. Majors, Altcoins).
+ - `id`: bigint (PK)
+ - `name`: text, `min_leverage`, `max_leverage`: float8
+- **perp_market_group_vault**: Maps market groups to specific collateral vaults.
+ - `perp_market_group_id`, `collateral_id`: bigint (Composite PK)
+ - `vault`: text (contract address)
+- **perp_market_price**: Latest price for a perp market.
+ - `perp_market_id`: bigint (PK)
+ - `price`: float8, `last_updated_block`: bigint
+- **perp_market_visibility**: Toggles market visibility in frontend.
+ - `perp_market_id`: bigint (PK, FK), `visible`: boolean
+- **perp_borrowing_group**: Borrowing rate metrics for a group and collateral.
+ - `group_id`, `collateral_id`: bigint (Composite PK)
+ - `acc_fees_long`, `acc_fees_short`: float8
+ - `oi_long`, `oi_short`, `oi_max`: bigint
+ - `fee_per_block`, `fee_exponent`: float8
+ - `acc_last_updated_block`, `last_updated_block`, `last_updated_tx_index`, `last_updated_event_index`: bigint
+- **perp_borrowing**: Borrowing rate metrics for a specific market and collateral.
+ - Same fields as `perp_borrowing_group` plus `perp_market_id`, `borrowing_group_id`.
+- **perp_trade**: Current state of open and pending trades.
+ - `trader`: text, `id`: bigint (Composite PK)
+ - `trade_type`: perp_trade_type, `perp_market_id`: bigint, `collateral_id`: bigint
+ - `collateral_amount`, `open_collateral_amount`: bigint
+ - `is_long`, `is_open`: boolean, `leverage`, `open_price`, `close_price`: float8
+ - `tp`, `sl`: float8, `referral_code`: text
+ - `open_block`, `close_block`: bigint, `last_updated_*`: bigint
+- **perp_trade_history**: Event log of trade changes (opens, closes, liquidations).
+ - `id`: bigserial (PK)
+ - `trader`: text, `trade_id`: bigint, `trade_change_type`: perp_trade_change_type
+ - `realized_pnl_pct`: float8, `block`, `tx_index`, `event_index`: bigint
+- **perp_trade_state**: Derived trade metrics (real-time PnL, liq price).
+ - `trader`: text, `id`: bigint (Composite PK)
+ - `pnl_pct`, `pnl_collateral`, `borrowing_fee_*`, `closing_fee_*`, `pnl_collateral_after_fees`: float8
+ - `remaining_collateral_after_fees`, `liquidation_price`: float8
+- **perp_trade_trigger_history**: History of limit/stop order executions.
+ - `id`: bigserial (PK)
+ - `trader`: text, `trade_id`: bigint, `executor`: text
+ - `trigger_type`: perp_trade_trigger_type
+ - `trade_trigger_price`, `market_price`: float8
+ - `trigger_execution_block`, `trigger_executed_block`: bigint, `is_success`: boolean, `error_msg`: text
+- **perp_fee**: Fee configuration.
+ - `id`: bigint (PK), `open_fee_p`, `close_fee_p`, `trigger_order_fee_p`: float8, `min_position_size_usd`: bigint
+- **perp_fee_charged_history**: Detailed breakdown of fees paid per trade event.
+ - `id`: bigserial (PK)
+ - `trader`: text, `trade_id`: bigint, `fee_type`: perp_fee_type
+ - `total_fee_charged`, `vault_fee`, `trigger_fee`, `gov_fee`, `net_gov_fee`, `referrer_allocation`: bigint
+ - `fee_multiplier`: float8, `catalyst_address`: text, `collateral_left`, `bad_debt`: bigint
+ - `open_fee_component`, `trigger_fee_component`, `triggerer_reward`, `final_closing_fee`: bigint
+ - `block`, `tx_index`, `event_index`: bigint
+- **perp_open_interest_history**: Time-series of OI per market/group.
+ - `id`: bigint, `id_type`: perp_id_type, `collateral_id`: bigint, `block`, `tx_index`, `event_index` (Composite PK)
+ - `oi_long`, `oi_short`: bigint
+- **perp_pending_acc_fees_history**: Time-series of accumulated fees.
+ - `id`: bigint, `id_type`: perp_id_type, `collateral_id`: bigint, `block`, `tx_index`, `event_index` (Composite PK)
+ - `acc_fee_long`, `acc_fee_short`: float8
+- **perp_balance_history**: History of contract balances.
+ - `block`: bigint, `denom`: text (Composite PK), `balance`: bigint
+- **perp_blacklist**: Blacklisted traders.
+ - `trader`: text (PK), `blacklisted_ts`: timestamptz, `reason`: text
+
+### Types
+- **perp_id_type**: `market`, `group`
+- **perp_trade_type**: `trade`, `stop`, `limit`
+- **perp_trade_change_type**: `position_opened`, `limit_order_created`, `stop_order_created`, `order_triggered`, `position_closed_tp`, `position_closed_sl`, `position_liquidated`, `position_closed_user`, `order_closed_user`, `tp_updated`, `sl_updated`, `leverage_updated`
+- **perp_trade_trigger_type**: `limit_open`, `stop_open`, `tp_close`, `sl_close`, `liq_close`
+- **perp_fee_type**: `opening`, `closing`
+
+---
+
+## 4. LP (004_lp.sql)
+
+### Tables
+- **lp_vault**: Current state of SLP vaults.
+ - `address`: text (PK)
+ - `shares_denom`, `shares_erc20`, `collateral_denom`, `collateral_erc20`: text
+ - `collateral_id`, `available_assets`, `tvl`, `net_profit`, `rewards`, `closed_pnl`, `liabilities`, `current_epoch_positive_open_pnl`, `current_epoch`, `epoch_start`: bigint
+ - `share_price`: float8
+- **lp_vault_history**: Historical snapshot of vault metrics.
+ - `vault`: text, `block`: bigint (Composite PK)
+ - Same fields as `lp_vault`.
+- **lp_deposit_history**: Log of deposit and withdrawal events.
+ - `id`: bigserial (PK)
+ - `action`: lp_action_type, `depositor`: text, `vault`: text
+ - `amount`, `shares`: bigint, `block`, `tx_index`, `event_index`: bigint
+- **lp_vault_assets_received_history**: External asset transfers to vaults (fees, etc).
+ - `vault`: text, `block`: bigint, `tx_index`: bigint, `event_index`: bigint (Composite PK)
+ - `sender`: text, `denom`: text, `amount`, `assets_less_deplete`: bigint
+- **lp_vault_pnl_history**: History of PnL changes and distribution to LPs.
+ - `vault`: text, `block`: bigint, `tx_index`: bigint, `event_index`: bigint (Composite PK)
+ - `current_epoch`, `prev_positive_open_pnl`, `new_positive_open_pnl`, `current_epoch_positive_open_pnl`: bigint
+ - `acc_pnl_per_token_used`: float8
+- **lp_withdraw_request**: Pending user withdrawal requests.
+ - `depositor`: text, `vault`: text, `unlock_epoch`: bigint (Composite PK)
+ - `shares`: bigint, `auto_redeem`: boolean
+
+### Types
+- **lp_action_type**: `deposit`, `create_withdraw_request`, `cancel_withdraw_request`, `redeem`
+
+---
+
+## 5. Referral (005_referral.sql)
+
+### Tables
+- **referral_code**: Registered referral codes.
+ - `code`: text (PK), `referrer`: text, `description`: text, `created_block`: bigint, `is_active`: boolean
+- **referral_code_creation_history**: Log of code creation.
+ - `id`: bigserial (PK), `code`, `referrer`, `description`: text, `block`, `tx_index`, `event_index`: bigint
+- **referral_redeem_history**: Log of users redeeming codes.
+ - `id`: bigserial (PK), `trader`, `code`, `referrer`: text, `block`, `tx_index`, `event_index`: bigint
+- **referral_claim_history**: History of referral commission claims.
+ - `id`: bigserial (PK), `referrer`: text, `amount`: bigint, `denom`: text, `coin_breakdown`: jsonb, `chain`: text, `block`, `tx_index`, `event_index`: bigint
+
+---
+
+## 6. Stats (006_stats.sql)
+
+### stats_block_ranges
+Maps time buckets to block ranges.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_block_ranges"
+```
+- `granularity` (stats_granularity): Bucket size (`1m`, `1h`, `1d`, etc); part of composite PK.
+- `ts` (timestamptz): Bucket timestamp; part of composite PK.
+- `start_block` (bigint): First chain block included in the bucket.
+- `end_block` (bigint): Last chain block included in the bucket.
+
+### stats_oracle_price
+Aggregated oracle prices over time.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_oracle_price"
+```
+- `granularity` (stats_granularity): Aggregation interval; part of composite PK.
+- `ts` (timestamptz): Bucket timestamp; part of composite PK.
+- `oracle_token_id` (bigint): Oracle token ID; part of composite PK.
+- `avg_price_usd` (float8): Average token price in USD for the bucket.
+- `min_price_usd` (float8): Minimum token price in USD for the bucket.
+- `max_price_usd` (float8): Maximum token price in USD for the bucket.
+- `open_price_usd` (float8): First observed token price in USD for the bucket.
+- `close_price_usd` (float8): Last observed token price in USD for the bucket.
+
+### stats_cache
+Singleton cache for global exchange metrics.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_cache"
+```
+- `id` (integer): Singleton row identifier (`CHECK (id = 1)`).
+- `trading_volume_24h` (numeric): Rolling 24h trading volume in USD.
+- `trading_volume_all_time` (numeric): Cumulative trading volume in USD.
+- `total_trades_24h` (bigint): Rolling 24h trade count.
+- `total_trades_all_time` (bigint): Cumulative trade count.
+- `open_interest` (numeric): Current open interest in USD.
+- `total_users_24h` (bigint): Distinct active users in last 24h.
+- `total_users_all_time` (bigint): Cumulative distinct users.
+- `total_open_positions` (bigint): Count of currently open positions.
+- `tvl` (numeric): Total value locked in USD.
+- `accrued_trading_fees_24h` (numeric): Rolling 24h accrued trading fees in USD.
+- `accrued_trading_fees_all_time` (numeric): Cumulative accrued trading fees in USD.
+- `last_updated` (timestamptz): Last cache refresh timestamp.
+
+### stats_perp_by_user
+User-level trading stats (daily).
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_perp_by_user"
+```
+- `ts` (date): Snapshot date.
+- `user_address` (text): User wallet address.
+- `market_id` (bigint): Perp market ID.
+- `collateral_id` (bigint): Collateral token ID.
+- `volume_usd_long` (numeric): Long-side volume in USD.
+- `volume_usd_short` (numeric): Short-side volume in USD.
+- `positive_pnl_usd` (numeric): Cumulative profits (positive PnL) in USD.
+- `negative_pnl_usd` (numeric): Cumulative losses (negative PnL) in USD.
+- `trades_count_long` (bigint): Total count of long trades.
+- `trades_count_short` (bigint): Total count of short trades.
+- `updated_at` (timestamptz): Timestamp of the last record refresh.
+
+### stats_perp_liq_daily
+Daily liquidation volume and counts per market.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_perp_liq_daily"
+```
+- `ts` (date): Snapshot date; part of composite PK.
+- `market_id` (bigint): Perp market ID; part of composite PK.
+- `collateral_id` (bigint): Collateral token ID; part of composite PK.
+- `trades_count_long` (bigint): Number of liquidated long positions.
+- `trades_count_short` (bigint): Number of liquidated short positions.
+- `volume_long_usd` (numeric): Liquidated long notional volume in USD.
+- `volume_short_usd` (numeric): Liquidated short notional volume in USD.
+- `total_fee_charged_usd` (numeric): Total liquidation-related fees charged in USD.
+- `updated_at` (timestamptz): Timestamp of the last record refresh.
+
+### stats_perp_oi_daily
+Daily Open Interest snapshots per market.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_perp_oi_daily"
+```
+- `ts` (date): Snapshot date; part of composite PK.
+- `market_id` (bigint): Perp market ID; part of composite PK.
+- `collateral_id` (bigint): Collateral token ID; part of composite PK.
+- `oi_long_usd` (numeric): Long-side open interest in USD.
+- `oi_short_usd` (numeric): Short-side open interest in USD.
+- `updated_at` (timestamptz): Timestamp of the last record refresh.
+
+### stats_slp_deposit_by_user
+User LP stats (daily).
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_slp_deposit_by_user"
+```
+- `ts` (date): Snapshot date; part of composite PK.
+- `user_address` (text): User wallet address; part of composite PK.
+- `vault` (text): SLP vault address; part of composite PK.
+- `amount_deposited_usd` (numeric): USD value deposited during this day.
+- `amount_withdrawn_usd` (numeric): USD value withdrawn during this day.
+- `cumulative_amount_deposited_usd` (numeric): Running cumulative deposited USD for user/vault.
+- `cumulative_amount_withdrawn_usd` (numeric): Running cumulative withdrawn USD for user/vault.
+- `updated_at` (timestamptz): Timestamp of the last record refresh.
+
+### stats_perp_sl_tp_daily
+Daily SL/TP execution stats.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_perp_sl_tp_daily"
+```
+- `ts` (date): Snapshot date; part of composite PK.
+- `market_id` (bigint): Perp market ID; part of composite PK.
+- `collateral_id` (bigint): Collateral token ID; part of composite PK.
+- `sl_trades_count_long` (bigint): Number of long trades closed via stop-loss.
+- `sl_trades_count_short` (bigint): Number of short trades closed via stop-loss.
+- `sl_losses_prevented_long_usd` (numeric): Estimated long-side losses prevented by stop-loss in USD.
+- `sl_losses_prevented_short_usd` (numeric): Estimated short-side losses prevented by stop-loss in USD.
+- `tp_trades_count_long` (bigint): Number of long trades closed via take-profit.
+- `tp_trades_count_short` (bigint): Number of short trades closed via take-profit.
+- `tp_profit_realized_long_usd` (numeric): Realized long-side take-profit in USD.
+- `tp_profit_realized_short_usd` (numeric): Realized short-side take-profit in USD.
+- `updated_at` (timestamptz): Timestamp of the last record refresh.
+
+### stats_perp_fee_daily
+Daily fee breakdown per market.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_perp_fee_daily"
+```
+- `ts` (date): Snapshot date; part of composite PK.
+- `market_id` (bigint): Perp market ID; part of composite PK.
+- `collateral_id` (bigint): Collateral token ID; part of composite PK.
+- `opening_fee_count` (integer): Count of opening-fee events.
+- `opening_fee_total` (bigint): Total opening fees in base collateral units.
+- `opening_fee_total_usd` (float8): Total opening fees in USD.
+- `opening_vault_fee` (bigint): Opening-fee portion allocated to vaults (base units).
+- `opening_vault_fee_usd` (float8): Opening-fee portion allocated to vaults (USD).
+- `opening_trigger_fee` (bigint): Opening trigger-order fees (base units).
+- `opening_trigger_fee_usd` (float8): Opening trigger-order fees (USD).
+- `opening_gov_fee` (bigint): Opening-fee protocol/governance allocation (base units).
+- `opening_gov_fee_usd` (float8): Opening-fee protocol/governance allocation (USD).
+- `opening_referrer_fee` (bigint): Opening-fee referrer allocation (base units).
+- `opening_referrer_fee_usd` (float8): Opening-fee referrer allocation (USD).
+- `closing_fee_count` (integer): Count of closing-fee events.
+- `closing_fee_total` (bigint): Total closing fees in base collateral units.
+- `closing_fee_total_usd` (float8): Total closing fees in USD.
+- `closing_vault_fee` (bigint): Closing-fee portion allocated to vaults (base units).
+- `closing_vault_fee_usd` (float8): Closing-fee portion allocated to vaults (USD).
+- `closing_trigger_fee` (bigint): Closing trigger-order fees (base units).
+- `closing_trigger_fee_usd` (float8): Closing trigger-order fees (USD).
+- `closing_gov_fee` (bigint): Closing-fee protocol/governance allocation (base units).
+- `closing_gov_fee_usd` (float8): Closing-fee protocol/governance allocation (USD).
+- `closing_referrer_fee` (bigint): Closing-fee referrer allocation (base units).
+- `closing_referrer_fee_usd` (float8): Closing-fee referrer allocation (USD).
+- `total_bad_debt` (bigint): Aggregate bad debt amount in base collateral units.
+- `updated_at` (timestamptz): Timestamp of the last record refresh.
+
+### stats_referrals_daily
+Daily referrer earnings and volume.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_referrals_daily"
+```
+- `ts` (date): Snapshot date; part of composite PK.
+- `referrer` (text): Referrer address; part of composite PK.
+- `trades_count` (bigint): Number of referred trades for the day.
+- `earnings_usd` (float8): Referrer earnings in USD for the day.
+- `volume_usd` (float8): Referred trading volume in USD for the day.
+- `updated_at` (timestamptz): Timestamp of the last record refresh.
+
+### stats_slp_vault
+Aggregated SLP vault metrics (APY, TVL, volume).
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_slp_vault"
+```
+- `ts` (timestamptz): Bucket timestamp; part of composite PK.
+- `granularity` (stats_granularity): Bucket size; part of composite PK.
+- `vault` (text): SLP vault address; part of composite PK.
+- `share_price` (float8): Vault share price at snapshot time.
+- `apy` (float8): Annual percentage yield estimate.
+- `deposits` (float8): Deposited collateral amount in token units for the bucket.
+- `deposits_usd` (float8): Deposited amount converted to USD.
+- `withdrawals` (float8): Withdrawn collateral amount in token units for the bucket.
+- `withdrawals_usd` (float8): Withdrawn amount converted to USD.
+- `volume` (float8): Total vault activity volume in token units.
+- `volume_usd` (float8): Total vault activity volume in USD.
+- `users` (bigint): Count of unique users active in the bucket.
+- `new_users` (bigint): Count of first-time users in the bucket.
+- `assets_usd` (float8): Vault assets under management in USD.
+- `closed_pnl` (float8): Closed PnL attributable to the vault in USD.
+- `liabilities` (float8): Vault liabilities in USD.
+- `updated_at` (timestamptz): Timestamp of the last record refresh.
+
+### stats_perp_trade_snapshots
+Snapshots of active trades for PnL tracking.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_perp_trade_snapshots"
+```
+- `granularity` (stats_granularity): Snapshot interval; part of composite PK.
+- `ts` (timestamptz): Snapshot timestamp; part of composite PK.
+- `trader` (text): Trader address; part of composite PK.
+- `trade_id` (bigint): Trade ID scoped to trader; part of composite PK.
+- `collateral_amount` (bigint): Trade collateral amount in base units.
+- `leverage` (float8): Position leverage at snapshot time.
+- `pending_pnl_usd` (float8): Unrealized (pending) PnL in USD at snapshot time.
+- `realized_pnl_usd` (float8): Realized PnL in USD attributable in the interval.
+- `opened_in_period` (boolean): True if position opened during this interval.
+- `closed_in_period` (boolean): True if position closed during this interval.
+
+### stats_user_portfolio
+Time-series of user PnL and volume.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d stats_user_portfolio"
+```
+- `granularity` (stats_granularity): Aggregation interval; part of composite PK.
+- `ts` (timestamptz): Bucket timestamp; part of composite PK.
+- `user_address` (text): User wallet address; part of composite PK.
+- `realized_pnl_usd` (float8): Realized PnL in USD in this bucket.
+- `realized_pnl_usd_cumulative` (float8): Running cumulative realized PnL in USD.
+- `pending_pnl_usd_cumulative` (float8): Running cumulative unrealized (pending) PnL in USD.
+- `volume_usd` (float8): Trading volume in USD in this bucket.
+- `volume_usd_cumulative` (float8): Running cumulative trading volume in USD.
+- `trades_count` (bigint): Number of trades in this bucket.
+- `trades_count_cumulative` (bigint): Running cumulative trade count.
+
+### leaderboard
+Global PnL and volume leaderboard.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d leaderboard"
+```
+- `trader` (text): Trader address (PK).
+- `volume_usd` (numeric): Cumulative trading volume in USD.
+- `realized_pnl_usd` (numeric): Cumulative realized PnL in USD.
+- `collateral_used_usd` (numeric): Cumulative collateral usage in USD.
+- `last_tracked_block` (bigint): Last block included in leaderboard refresh.
+
+### volume_leaderboard
+Volume-only leaderboard.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d volume_leaderboard"
+```
+- `trader` (text): Trader address (PK).
+- `volume_usd` (numeric): Cumulative trading volume in USD for ranking.
+- `last_tracked_block` (bigint): Last block included in refresh.
+- `threshold_block` (bigint): Optional lower-bound block used for scoped volume windows.
+
+### statsig_last_tracked_block
+Tracks progress of stats refreshers.
+```bash
+psql -d sai_keeper_2 -X -P pager=off -c "\d statsig_last_tracked_block"
+```
+- `event_type` (text): Stats event/process identifier (PK).
+- `last_tracked_block` (bigint): Latest processed block for that event type.
+
+### Types
+- **stats_granularity**: `1s`, `1m`, `5m`, `15m`, `1h`, `4h`, `6h`, `12h`, `1d`, `1w`, `1mo`
diff --git a/ai-skills/sai-keeper-graphql/SKILL.md b/ai-skills/sai-keeper-graphql/SKILL.md
new file mode 100644
index 0000000..f888b58
--- /dev/null
+++ b/ai-skills/sai-keeper-graphql/SKILL.md
@@ -0,0 +1,151 @@
+---
+name: sai-keeper-graphql
+description: >-
+ Query the Sai GraphQL API (sai-keeper) for perp trading, LP vaults, oracle
+ prices, and fees on Nibiru. Use when working with Sai exchange data, perp trades,
+ liquidity pools, oracle prices, or fee analytics.
+metadata:
+ tags: ["sai-perps", "sai-website", "sai-keeper", "indexer"]
+ agent_skills: # related skills
+ - sai-db
+---
+
+# Sai Keeper GraphQL
+
+## Quick Facts
+
+- **sai-keeper** = Sai exchange GraphQL API. Source schema lives in `$HOME/ki/sai-keeper/graphql/graph/*.graphqls`.
+- **HTTP endpoint**: Root path is `/query` (e.g. `https://sai-keeper.nibiru.fi/query`).
+- **Endpoint Discrepancy**: Public/proxy deployments may use `/graphql` for HTTP and WS. Always verify the actual deployment path.
+- **Playground**: Root URLs `https://sai-keeper.nibiru.fi/` and `https://sai-keeper.testnet-2.nibiru.fi/`.
+- **WebSocket**: Same host with path `/query`; e.g. `wss://sai-keeper.nibiru.fi/query`.
+
+## Environments
+
+| Environment | HTTP (GraphQL) | Playground | WebSocket |
+|-------------|----------------|------------|-----------|
+| Mainnet | `https://sai-keeper.nibiru.fi/query` | `https://sai-keeper.nibiru.fi/` | `wss://sai-keeper.nibiru.fi/query` |
+| Testnet | `https://sai-keeper.testnet-2.nibiru.fi/query` | `https://sai-keeper.testnet-2.nibiru.fi/` | `wss://sai-keeper.testnet-2.nibiru.fi/query` |
+
+## Core Concepts
+
+- **Sai Protocol**: Decentralized perpetual futures exchange on Nibiru Chain.
+- **Contract Stack**: 5 Wasm contracts: Perp Manager, LP Vault, Oracle, Fee Manager, and Governance.
+- **Data Domains**:
+ - `perp`: Perpetual trading positions, history, and borrowing rates.
+ - `lp`: Liquidity pool metrics, user deposits, and withdrawal requests.
+ - `oracle`: Real-time and historical token prices.
+ - `fee`: Fee transactions, daily statistics, and protocol revenue analytics.
+- **Key Terms**:
+ - **Collateral**: Assets used as margin for trades.
+ - **Liquidation**: Forced closure of positions with insufficient margin.
+ - **Borrowing Fees**: Sai's mechanism for balancing markets (funding rate).
+ - **Epoch**: Time period for LP operations (deposits/withdrawals).
+ - **TVL**: Total Value Locked in a vault (available assets + open position assets).
+
+## Amount and Type Semantics
+
+- **Amounts**: Numeric values like `collateralAmount` or `tvl` are `Int` in base units (e.g., 6 decimals for USDC/stNIBI).
+- **Percentages/Prices**: Use `Float` (e.g., `pnlPct`, `priceUsd`, `sharePrice`).
+- **Time**: Represented by the `Time` scalar (RFC3339 format).
+- **Token Decimals**: `Token` (from Oracle) does not expose `decimals`. Use `TokenInfo` (in `Balance` via `userBalances` subscription) to get decimal metadata.
+
+## Root Types and API Reference
+
+### Perp
+
+- **trade(trader, id)**: Get a specific trade.
+- **trades(where!)**: List trades for a trader. `PerpTradesFilter` requires `trader`.
+- **tradeHistory**: Detailed event log of trade modifications (opens, closes, liquidations, order triggers).
+- **borrowing(marketId, collateralId)**: Get detailed market metrics.
+- **borrowings**: List available markets.
+- **referralInfo(userAddress)**: Get referral redemption details for a user.
+- **referralCodes(owner)**: List referral codes owned by an address.
+- **referralRedemption(user)**: Check if a user has redeemed a code.
+
+**PerpTradeChangeType**: `position_opened`, `position_closed_user`, `position_closed_tp`, `position_closed_sl`, `position_liquidated`, `limit_order_created`, `stop_order_created`, `order_triggered`, `order_closed_user`, `tp_updated`, `sl_updated`.
+
+### LP (Liquidity Provision)
+
+- **vaults**: Metrics for SLP vaults (single-collateral model).
+- **deposits**: Active LP positions for a user.
+- **depositHistory**: Historical deposit and withdrawal events.
+- **withdrawRequests**: Pending withdrawal requests with `unlockEpoch`.
+- **RevenueInfo**: Tracks `NetProfit`, `TraderLosses`, `ClosedPnl`, `Liabilities`, and `CurrentEpochPositiveOpenPnl`.
+
+### Oracle
+
+- **tokens**: List metadata for all supported assets.
+- **token(id)**: Specific asset metadata.
+- **tokenPricesUsd**: Current market prices. Always check `lastUpdatedBlock` for freshness (warn if > 60s old).
+
+### Fee
+
+- **feeTransactions**: Detailed breakdown of every fee paid.
+- **feeDailyStats**: Aggregated daily data by collateral and trader.
+- **feeAnalytics**: Enhanced statistics including `avgFeeMultiplier`.
+- **traderFeeSummary** / **protocolFeeSummary**: Aggregated revenue over time periods.
+
+## Filters and Pagination
+
+Common parameters across most list fields:
+
+- **where**: Filter object (e.g., `trader`, `perpMarketId`, `isOpen`, `depositor`, `vault`).
+- **order_by**: Domain-specific sorting (e.g., `sequence`, `trade_id`, `id`, `name`).
+- **order_desc**: Boolean for descending order.
+- **limit** / **offset**: Standard pagination (default limits apply).
+
+**Filter Types**:
+- `StringFilter`: `{ eq, like }`
+- `IntFilter`: `{ eq, gt, gte, lt, lte }`
+- `TimeFilter`: `{ eq, gt, gte, lt, lte }`
+
+## Subscriptions
+
+Real-time updates via WebSocket (`wss://`).
+
+| Subscription | Filter (`where`) | Notes |
+|--------------|------------------|-------|
+| `tokenPricesUsd` | optional `SubTokenPriceUsdFilter` | Price feed for all or specific token. |
+| `perpTrades` | `SubPerpTradesFilter!` | Real-time position updates for a trader. |
+| `perpTradeHistory` | `SubPerpTradeHistoryFilter!` | Trade event updates for a trader. |
+| `perpBorrowing` | `marketId!, collateralId!` | Single market borrowing rate updates. |
+| `perpBorrowings` | none | All market availability updates. |
+| `lpVaults` | none | Live TVL and APY updates for all vaults. |
+| `lpDeposits` | `SubLpDepositsFilter!` | User LP share balance updates. |
+| `lpDepositHistory` | `SubLpDepositHistoryFilter!` | Real-time deposit/withdrawal events. |
+| `lpWithdrawRequests` | `SubLpWithdrawRequestsFilter!` | Withdrawal status updates. |
+| `userBalances` | `SubUserBalancesFilter!` | Live wallet balance updates for all assets. Includes `TokenInfo` with `decimals`. |
+
+## REST API
+
+When running `sai-keeper` with the `-api` flag:
+
+- **GET /api/stats**: Exchange statistics (JSON). Includes `trading_volume_24h`, `total_trades_24h`, `open_interest`, `tvl`, and `accrued_trading_fees_24h`.
+- **GET /health**: Service health status.
+- **GET /**: API overview and available endpoints.
+
+## Calculations and Formulas
+
+- **Position Value**: `Collateral Amount * Leverage` (USD if collateral price is known).
+- **Borrowing APR**: `feesPerHour * 24 * 365 * 100`.
+- **Deposit Value**: `(shares * sharePrice) / 10^decimals`.
+- **APY**: `(NetProfit / TVL) * (365 / epochDurationDays) * 100`.
+- **Liquidation (Long)**: `Entry Price * (1 - 1/Leverage + maintenance_margin)`.
+- **Liquidation (Short)**: `Entry Price * (1 + 1/Leverage - maintenance_margin)`.
+- **Fee**: `Total Fee = Vault Fee + Gov Fee + Trigger Fee`.
+- **Dynamic Fee**: `Actual Fee = Base Fee * feeMultiplier`.
+
+## Related Repos and Sources
+
+- **sai-keeper** (`$HOME/ki/sai-keeper`): GraphQL schema and backend implementation.
+- **sai-docs** (`$HOME/ki/sai-docs`):
+ - API References: `dev/sai-keeper/api-*.md`
+ - Core Concepts: `dev/sai-keeper/core-concepts.md`
+ - Deployment/Setup: `dev/sai-keeper/README.md`
+- **sai-perps** (`$HOME/ki/sai-perps`): Core protocol logic (Borrowing, Fees).
+- **sai-website/webapp**: Web frontend; reference for query implementation.
+
+## Additional Documentation
+
+For exhaustive details on GraphQL enums, accounting variables, fee tiers, and entity relationships, refer to the **[reference.md](./reference.md)** file in this skill directory.
diff --git a/ai-skills/sai-keeper-graphql/reference.md b/ai-skills/sai-keeper-graphql/reference.md
new file mode 100644
index 0000000..524a214
--- /dev/null
+++ b/ai-skills/sai-keeper-graphql/reference.md
@@ -0,0 +1,149 @@
+# Sai Keeper GraphQL Reference
+
+This document provides a deep dive into the GraphQL schema of Sai Keeper, including detailed descriptions of enums, types, and the underlying contract logic that drives them.
+
+## 1. Enum Catalog
+
+### PerpTradeChangeType
+Emitted during various stages of a trade's lifecycle:
+- `position_opened`: Emitted when a market order is successfully executed.
+- `limit_order_created`: A limit order is placed on the books.
+- `stop_order_created`: A stop order is placed on the books.
+- `order_triggered`: A limit or stop order has been hit and executed.
+- `position_closed_tp`: Closed via Take Profit trigger.
+- `position_closed_sl`: Closed via Stop Loss trigger.
+- `position_liquidated`: Forced closure due to insufficient margin.
+- `position_closed_user`: User manually closed the position.
+- `order_closed_user`: User manually cancelled a pending order.
+- `tp_updated` / `sl_updated`: User modified trigger prices.
+
+### FeeType
+Categorizes the timing of fee collection:
+- `OPENING`: Fees paid when entering a position.
+- `CLOSING`: Fees paid when exiting a position.
+
+### TokenType
+Defines the source of the asset:
+- `bank`: Native Cosmos SDK coin (e.g., `unibi`).
+- `erc20`: Token residing on the Nibiru EVM (e.g., bridged USDC).
+
+### Order Enums (Sorting)
+Common keys for `order_by` parameters:
+- `sequence`: Global monotonically increasing event index.
+- `trade_id`: The ID assigned to a specific trade.
+- `id`: Primary key of the record.
+- `name`: Alphabetical sorting for tokens.
+- `depositor` / `vault`: Sorting for LP-related lists.
+
+---
+
+## 2. Advanced Accounting: LP Domain
+
+The `lp` domain queries reflect the state of the Sai SLP vaults. Key variables from the accounting model:
+
+### Share Pricing
+- **`Ο` (share_to_assets_price)**: The conversion rate from shares to assets.
+ - Formula: `Ο = 1 + Ο β max(0, Ο_used)`
+- **`Ο` (acc_rewards_per_token)**: Cumulative rewards earned by LPs per share.
+- **`Ο` and `Ο_used` (acc_pnl_per_token)**: Realized and unrealized PnL from traders, scaled per share.
+
+### Withdrawal Timelocks
+Withdrawals are subject to epochs and timelocks based on collateralization (`collat_p`):
+- **1 Epoch**: Standard delay when vault is well-collateralized.
+- **2-3 Epochs**: Extended delay when the vault is under-collateralized or has high liabilities.
+- **Epoch Length**: Standard is 72 hours (48h open for requests, 24h locked for settlement).
+
+---
+
+## 3. Market Dynamics: Perp Domain
+
+### Borrowing Mechanism
+Borrowing fees (funding rates) are calculated per block based on Open Interest (OI) imbalance:
+- **Net OI**: `|OI Long - OI Short|`.
+- **Fee Sensitivity**: Driven by a `fee_exponent` (usually 1.0 to 3.0). Higher exponent means fees increase faster as imbalance grows.
+- **Borrowing Groups**: Multiple markets (e.g., BTC/USD, ETH/USD) can be grouped to share a single `oiMax` limit and borrowing rate parameters.
+
+### Liquidation Logic
+Positions are liquidated when the `remainingCollateralAfterFees` drops below the maintenance margin.
+- **`liquidationPrice`**: Calculated including the estimated `borrowingFeeCollateral` and `closingFeeCollateral`.
+
+---
+
+## 4. Fee Structure and Tiers
+
+Sai implements a tiered fee system based on trading "points" (usually 1:1 with volume):
+
+| Tier | Points Threshold | Multiplier |
+|------|------------------|------------|
+| 0 | 6,000,000 | 0.975 |
+| 1 | 20,000,000 | 0.950 |
+| 2 | 50,000,000 | 0.925 |
+| ... | ... | ... |
+| 7 | 2,000,000,000 | 0.600 |
+
+- **Governance Split**: Collected fees are split between the **LP Vault** (~80-90%) and **Governance** (~10-20%).
+- **Trigger Fees**: A portion of the closing fee is awarded to the keeper that triggers a TP/SL/Liquidation.
+
+---
+
+## 5. Units and Precision
+
+- **Base Units**: All numeric amounts (e.g., `collateralAmount`, `tvl`) are integers in the token's base units.
+ - *Example*: USDC has 6 decimals, so `1,000,000` = $1.00.
+- **Decimal Strings**: GraphQL returns `Decimal` types as strings (e.g., `"0.95"`) to preserve precision that standard JSON floats might lose.
+- **Float types**: Fields like `pnlPct` or `priceUsd` are native GraphQL Floats.
+
+---
+
+## 6. Type Relationship Map
+
+```mermaid
+graph TD
+ User["User Address"] -->|"owns"| PerpTrade
+ User -->|"owns"| LpDeposit
+ User -->|"has"| ReferralInfo
+
+ PerpTrade -->|"maps to"| PerpBorrowing
+ PerpBorrowing -->|"uses"| Token
+ PerpTrade -->|"emits"| PerpTradeHistoryItem
+
+ LpDeposit -->|"held in"| LpVault
+ LpVault -->|"tracks"| RevenueInfo
+ LpVault -->|"references"| Token
+
+ FeeTransaction -->|"linked to"| PerpTrade
+ ProtocolFeeSummary -->|"aggregates"| FeeTransaction
+```
+
+---
+
+## 7. Subscription Payloads
+
+### `userBalances`
+Returns an array of `Balance` objects:
+```json
+{
+ "amount": "10000000",
+ "token_info": {
+ "symbol": "USDC",
+ "decimals": 6,
+ "type": "erc20",
+ "bank_denom": "erc20/0x...",
+ "erc20_contract_address": "0x..."
+ }
+}
+```
+
+### `perpTrades`
+Returns the updated `PerpTrade` object including the real-time `PerpTradeState`:
+```json
+{
+ "id": 123,
+ "isOpen": true,
+ "state": {
+ "pnlPct": 0.05,
+ "positionValue": 10500000,
+ "liquidationPrice": 42500.5
+ }
+}
+```
diff --git a/ai-skills/sai-perps-query/SKILL.md b/ai-skills/sai-perps-query/SKILL.md
new file mode 100644
index 0000000..fb45c88
--- /dev/null
+++ b/ai-skills/sai-perps-query/SKILL.md
@@ -0,0 +1,241 @@
+---
+name: sai-perps-query
+description: >-
+ Run read-only queries against Sai mainnet CosmWasm contracts (Perp, Oracle,
+ Vault) using Nibiru CLI (nibid) or the Nibiru TypeScript SDK. Use when the user
+ asks to query Sai perp contracts, inspect open interest, check OI limits, look up
+ MarketIndex/TokenIndex/GroupIndex, run get_borrowing_group_oi or
+ get_borrowing_pair_oi, call queryContractSmart, or inspect Sai markets and
+ collaterals.
+metadata:
+ tags: ["sai-perps", "sai-website"]
+ agent_skills: # related skills
+ - sai-keeper-graphql
+ - sai-db
+ - sai-rest-api
+---
+
+# Sai Perps: Contract Query Playbook
+
+Source of truth for query message shapes: `$HOME/ki/sai-website/webapp/pages/easy.tsx` (`QUERY_CONFIG`).
+Full validated CLI notes: `$HOME/ki/boku/epics/epic-sai/26-02-10-sai-mainnet-query.md`.
+
+## Mainnet addresses
+
+| Contract | Address |
+|----------|---------|
+| **Perp** | `nibi1ntmw2dfvd0qnw5fnwdu9pev2hsnqfdj9ny9n0nzh2a5u8v0scflq930mph` |
+| **Oracle** | `nibi1xfwyfwtdame6645lgcs4xvf4u0hpsuvxrcelfwtztu0pv7n4l6hqw5a8gj` |
+| Vault β Group 0 / USDC | `nibi193m2a00pmdsvkcvugrfewqzhtq6k0srkjzvxp2sk357vlpspx5vqxu8d7p` |
+| Vault β Group 0 / stNIBI | `nibi1mrplvu3scplnrgns96kg0j8pk3l2p9c7eaz0qdedx0kt3vmcujyqrjkfej` |
+| Vault β Group 1 / USDC | `nibi1waf5c8z55qvjay4de8wkm9cxyt6wa8zdnrvlexjrq77lqgqf258q3yn7l8` |
+| Vault β Group 1 / stNIBI | `nibi1pgurgas0za436c3fm2km99zkzutfx0jwpn7meespv6szv8c8g39qjz2tvj` |
+
+Source: `$HOME/ki/sai-website/webapp/config/env.ts` (`SaiContractsMainnet`).
+
+Group 0 = main crypto markets (BTC, ETH, SOL, β¦). Group 1 = Real Estate (Coded Estate).
+
+## Strongly Related Skills
+
+- `sai-keeper-graphql`: Same domain (Sai exchange) except via GraphQL. Use when
+you need indexed data on perp trades, SLP vaults, oracle prices, or any
+information used in the end user application. Instead of live smart contract
+queries.
+- `sai-db`: Explains how Sai data ends up in Postgres. The sai-keeper repo
+GraphQL reads from this DB. Use when you need schema details, migrations info, to debug indexer logic, or to design and edit queries over indexed data.
+- `sai-rest-api`: Broad interface for aggregated stats, yield. For quick metric
+inspection on different dates.
+
+## CLI setup
+
+```bash
+PERP="nibi1ntmw2dfvd0qnw5fnwdu9pev2hsnqfdj9ny9n0nzh2a5u8v0scflq930mph"
+ORACLE="nibi1xfwyfwtdame6645lgcs4xvf4u0hpsuvxrcelfwtztu0pv7n4l6hqw5a8gj"
+NODE="https://rpc.archive.nibiru.fi:443"
+# helper alias
+sai_perps_q() { nibid query wasm contract-state smart "$1" "$2" --node "$NODE" --output json; }
+```
+
+## Rules of thumb
+
+- Query keys are **snake_case** (CosmWasm serde). `GetMarket` β `get_market`.
+- Some args use **wrapped string indices**: `"index": "MarketIndex(0)"`, `"index": "GroupIndex(0)"`, `"group_index": "GroupIndex(1)"`, `"collateral_index": "TokenIndex(1)"`.
+- Some args use **plain integers**: `collateral_index: 1`, `market_index: 0`, `group_index: 0`.
+ - When both arg names are plain integers, use integers. When the field name takes a typed index (e.g. `get_vault_address`), use the wrapped string form.
+ - Check `QUERY_CONFIG` in `easy.tsx` for the exact template for each query.
+- **Never use `collateral_index: 0`**. On mainnet `TokenIndex(0)` is the quote/USD placeholder β not a collateral. Valid collaterals are `TokenIndex(1)` (USDC) and `TokenIndex(2)` (stNIBI). Confirm first with `list_collaterals`.
+- OI values (`long`, `short`, `max`) are in **collateral token base units** and represent **position size (margin Γ leverage)**, not margin only. USDC and stNIBI both have 6 decimals β divide by `1e6` to get human units.
+
+## Discovery sequence
+
+Run these first when you need to understand available markets/groups/collaterals:
+
+```bash
+sai_perps_q "$PERP" '{"list_markets":{}}'
+sai_perps_q "$PERP" '{"list_groups":{}}'
+sai_perps_q "$PERP" '{"list_collaterals":{}}'
+sai_perps_q "$ORACLE" '{"list_tokens":{"limit":30}}'
+```
+
+`list_tokens` is paginated (default 10, max 30). For >30 tokens use `"start_after": `.
+
+To map a market to its group:
+
+```bash
+sai_perps_q "$PERP" '{"get_market":{"index":"MarketIndex(16)"}}'
+# β {"data":{"base":"TokenIndex(17)","quote":"TokenIndex(0)","group_index":"GroupIndex(0)",...}}
+```
+
+## Collaterals (mainnet)
+
+| TokenIndex | Name |
+|-----------|------|
+| 1 | USDC |
+| 2 | stNIBI (ampNIBI) |
+
+### stNIBI price in USD (mainnet quick path)
+
+On Sai mainnet Oracle, `id: 2` is `stnibi` and `id: 1` is `usdc`. These token IDs
+are stable on mainnet and can be reused directly.
+
+```bash
+# sanity check (optional)
+sai_perps_q "$ORACLE" '{"get_token_by_id":{"id":2}}'
+# β {"data":{"id":2,"base":"stnibi",...}}
+
+# direct stNIBI/USD oracle price
+sai_perps_q "$ORACLE" '{"get_price":{"index":2}}'
+
+# optional cross-check against USDC and ratio
+sai_perps_q "$ORACLE" '{"get_price":{"index":1}}'
+sai_perps_q "$ORACLE" '{"get_exchange_rate":{"base":2,"quote":1}}'
+```
+
+## Common MarketIndex β asset (mainnet)
+
+| MarketIndex | Base |
+|-------------|------|
+| 0 | BTC |
+| 1 | ETH |
+| 16 | SOL |
+| 17 | XRP |
+| 18 | SUI |
+| 29 | BNB |
+| 48 | NIBI |
+
+Full list: all markets belong to `GroupIndex(0)` except real-estate markets (2β12) which belong to `GroupIndex(1)`.
+
+## OI queries
+
+Two layers of limits. A trade is rejected if **either** is exceeded (`ExposureLimitReached`).
+
+```bash
+# Pair OI β limit for a specific market
+sai_perps_q "$PERP" '{"get_borrowing_pair_oi":{"collateral_index":1,"market_index":0}}'
+# β {"data":{"long":"...","short":"...","max":"10000000000000"}}
+
+# Group OI β shared limit across all markets in the group
+sai_perps_q "$PERP" '{"get_borrowing_group_oi":{"collateral_index":1,"group_index":0}}'
+# β {"data":{"long":"...","short":"...","max":"10000000000"}}
+```
+
+Recorded mainnet snapshot (Group 0 + USDC): pair max = 10,000,000 USDC, group max = 10,000 USDC. Group is usually the binding constraint.
+
+## Curated query reference
+
+Query message JSON templates, organized by contract. Source: `easy.tsx` `QUERY_CONFIG`.
+
+### Perp contract
+
+```json
+{"list_markets":{}}
+{"list_groups":{}}
+{"list_collaterals":{}}
+{"get_market":{"index":"MarketIndex(0)"}}
+{"get_group":{"index":"GroupIndex(0)"}}
+{"get_collateral":{"index":1}}
+{"get_fees":{"index":"FeeIndex(0)"}}
+{"get_pair_custom_max_leverage":{"index":0}}
+{"get_borrowing_pair":{"collateral_index":1,"market_index":0}}
+{"get_borrowing_pair_oi":{"collateral_index":1,"market_index":0}}
+{"get_borrowing_pair_group":{"collateral_index":1,"market_index":0}}
+{"get_borrowing_group":{"collateral_index":1,"group_index":0}}
+{"get_borrowing_group_oi":{"collateral_index":1,"group_index":0}}
+{"get_vault_address":{"group_index":"GroupIndex(0)","collateral_index":"TokenIndex(1)"}}
+{"get_trade":{"trader":"nibi1...","index":0}}
+{"get_trades":{"trader":"nibi1..."}}
+{"get_trade_info":{"trader":"nibi1...","index":0}}
+{"get_trade_infos":{"trader":"nibi1..."}}
+{"get_trade_data":{"trader":"nibi1...","index":0}}
+{"get_trade_pnl":{"trader":"nibi1...","index":0}}
+{"get_liquidation_price":{"trade_id":"UserTradeIndex(0)","trader":"nibi1...","include_borrowing_fees":true}}
+{"get_perp_prices":{"market_index":"MarketIndex(1)","collateral_index":"TokenIndex(1)"}}
+{"get_trader_fee_multiplier":{"trader":"nibi1..."}}
+{"is_trader_stored":{"trader":"nibi1..."}}
+{"get_fee_tiers":{}}
+{"get_pending_gov_fees":{"index":0}}
+{"get_oracle_address":{}}
+{"get_trading_activated":{}}
+{"get_oi_windows_settings":{}}
+{"get_windows":{"windows_duration":3600,"market_index":0,"current_window_id":0}}
+{"get_pair_depth":{"index":0}}
+{"get_user_deposit":{"user":"nibi1...","collateral_index":1}}
+{"list_user_deposits":{"user":"nibi1..."}}
+```
+
+### Oracle contract
+
+```json
+{"list_tokens":{"start_after":null,"limit":null}}
+{"get_token_by_id":{"id":3}}
+{"get_token_by_name":{"name_raw":"btc"}}
+{"get_price":{"index":3}}
+{"get_exchange_rate":{"base":3,"quote":4}}
+{"get_permission_group":{"group_id":2}}
+{"list_permission_groups":{"start_after":null,"limit":null}}
+{"expiration_time":{}}
+{"ownership":{}}
+```
+
+### Vault contract
+
+```json
+{"tvl":{}}
+{"available_assets":{}}
+{"market_cap":{}}
+{"current_epoch":{}}
+{"get_current_epoch_start":{}}
+{"config":{}}
+{"vault_snapshot":{}}
+{"get_revenue_info":{}}
+{"collateralization_p":{}}
+{"withdraw_epochs_timelock":{}}
+{"get_vault_share_denom":{}}
+{"get_collateral_denom":{}}
+{"max_mint":{}}
+{"max_deposit":{}}
+{"max_redeem":{"depositor":"nibi1..."}}
+{"max_withdraw":{"depositor":"nibi1..."}}
+{"total_shares_being_withdrawn":{"depositor":"nibi1..."}}
+{"user_withdraw_requests":{"user":"nibi1..."}}
+{"get_locked_deposit":{"deposit_id":0}}
+{"all_locked_deposits":{"start_after":null,"limit":null}}
+{"user_locked_deposits":{"user":"nibi1..."}}
+{"lock_discount_p":{"collat_p":"1.0","lock_duration":0}}
+{"user_vault_state":{"user":"nibi1..."}}
+```
+
+## TypeScript (NibiruQuerier) alternative
+
+```typescript
+import { Mainnet, NibiruQuerier } from "@nibiruchain/nibijs"
+
+const querier = await NibiruQuerier.connect(Mainnet().endptTm)
+
+const result = await querier.wasmClient.queryContractSmart(
+ "nibi1ntmw2dfvd0qnw5fnwdu9pev2hsnqfdj9ny9n0nzh2a5u8v0scflq930mph",
+ { get_borrowing_group_oi: { collateral_index: 1, group_index: 0 } },
+)
+```
+
+Address constants live in `$HOME/ki/sai-website/webapp/config/env.ts` (`SaiContractsMainnet`).
diff --git a/ai-skills/sai-perps-query/reference.md b/ai-skills/sai-perps-query/reference.md
new file mode 100644
index 0000000..e746c97
--- /dev/null
+++ b/ai-skills/sai-perps-query/reference.md
@@ -0,0 +1,168 @@
+---
+name: sai-perps-query-reference
+description: Full Sai mainnet TokenIndex + MarketIndex lookup (from epic doc).
+---
+
+# Sai Perps (Mainnet): Tokens & Markets Reference
+
+This file is the **full lookup** companion to `SKILL.md`.
+
+- [Mainnet addresses](#mainnet-addresses)
+- [Collaterals (mainnet)](#collaterals-mainnet)
+- [TokenIndex reference (mainnet)](#tokenindex-reference-mainnet)
+- [MarketIndex reference (mainnet)](#marketindex-reference-mainnet)
+
+## Mainnet addresses
+
+| Contract | Address |
+|---|---|
+| **Perp** | `nibi1ntmw2dfvd0qnw5fnwdu9pev2hsnqfdj9ny9n0nzh2a5u8v0scflq930mph` |
+| **Oracle** | `nibi1xfwyfwtdame6645lgcs4xvf4u0hpsuvxrcelfwtztu0pv7n4l6hqw5a8gj` |
+| SLP-USDC Vault, Group 0. | `nibi193m2a00pmdsvkcvugrfewqzhtq6k0srkjzvxp2sk357vlpspx5vqxu8d7p` |
+| SLP-stNIBI Vault, Group 0. | `nibi1mrplvu3scplnrgns96kg0j8pk3l2p9c7eaz0qdedx0kt3vmcujyqrjkfej` |
+| SLP-USDC Vault, Group 1 | `nibi1waf5c8z55qvjay4de8wkm9cxyt6wa8zdnrvlexjrq77lqgqf258q3yn7l8` |
+| SLP-stNIBI Vault, Group 1. | `nibi1pgurgas0za436c3fm2km99zkzutfx0jwpn7meespv6szv8c8g39qjz2tvj` |
+
+**Group meanings (Perp `GroupIndex`):**
+- `GroupIndex(0)` = main crypto markets
+- `GroupIndex(1)` = real estate (coded estate)
+
+## Collaterals (mainnet)
+
+Never use `collateral_index: 0`. Mainnet collaterals are:
+
+| TokenIndex | base | Notes |
+|---:|---|---|
+| 1 | `usdc` | collateral |
+| 2 | `stnibi` | collateral (contract denom contains `ampNIBI`) |
+
+---
+
+## TokenIndex reference (mainnet)
+
+Per the epic doc, **`TokenIndex(0)` is used by Perp as quote (USD)** and is **not**
+present in Oracle (`get_token_by_id` returns "token id 0 does not exist").
+
+
+| TokenIndex | base | permission_group |
+|---:|---|---:|
+| 0 | (USD quote placeholder; not in Oracle) | - |
+| 1 | `usdc` | 2 |
+| 2 | `stnibi` | 2 |
+| 3 | `btc` | 2 |
+| 4 | `eth` | 2 |
+| 5 | `atom` | 2 |
+| 6 | `dubai` | 1 |
+| 7 | `los-angeles` | 1 |
+| 8 | `new-york` | 1 |
+| 9 | `chicago` | 1 |
+| 10 | `washington` | 1 |
+| 11 | `pittsburgh` | 1 |
+| 12 | `miami-beach` | 1 |
+| 13 | `boston` | 1 |
+| 14 | `brooklyn` | 1 |
+| 15 | `austin` | 1 |
+| 16 | `denver` | 1 |
+| 17 | `sol` | 2 |
+| 18 | `xrp` | 2 |
+| 19 | `sui` | 2 |
+| 20 | `ena` | 2 |
+| 21 | `arb` | 2 |
+| 22 | `trx` | 2 |
+| 23 | `apt` | 2 |
+| 24 | `pol` | 2 |
+| 25 | `ton` | 2 |
+| 26 | `ada` | 2 |
+| 27 | `ltc` | 2 |
+| 28 | `doge` | 2 |
+| 29 | `aster` | 2 |
+| 30 | `bnb` | 2 |
+| 31 | `pump` | 2 |
+| 32 | `avax` | 2 |
+| 33 | `aave` | 2 |
+| 34 | `pengu` | 2 |
+| 35 | `link` | 2 |
+| 36 | `bonk` | 2 |
+| 37 | `shib` | 2 |
+| 38 | `trump` | 2 |
+| 39 | `near` | 2 |
+| 40 | `kaito` | 2 |
+| 41 | `mnt` | 2 |
+| 42 | `ip` | 2 |
+| 43 | `virtual` | 2 |
+| 44 | `hype` | 2 |
+| 45 | `zec` | 2 |
+| 46 | `purr` | 2 |
+| 47 | `mon` | 2 |
+| 48 | `fartcoin` | 2 |
+| 49 | `nibi` | 2 |
+| 50 | `pokemon-card-index` | 1 |
+
+## MarketIndex reference (mainnet)
+
+Every market has the following fields.
+- `quote: TokenIndex(0)` (USD placeholder)
+- `spread_p: 0`
+- `fee_index: FeeIndex(0)`
+
+Each market has base, quote (always TokenIndex(0) = USD), spread_p, group_index,
+fee_index. Base token names resolved via [TokenIndex
+reference](#tokenindex-reference-mainnet) above.
+```rust
+#[cw_serde]
+pub struct MarketInfo {
+ pub base: TokenIndex,
+ pub quote: TokenIndex,
+ pub spread_p: Decimal,
+ pub group_index: GroupIndex,
+ pub fee_index: FeeIndex,
+}
+```
+
+All markets have `MarketInfo.quote=TokenIndex(0)` (USD).
+
+| MarketIndex | base (TokenIndex) | base (name) | spread_p | group_index | fee_index |
+|---|---|---|---|---|---|
+| 0 | 3 | `btc` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 1 | 4 | `eth` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 2 | 6 | `dubai` | 0 | `GroupIndex(1)` | `FeeIndex(0)` |
+| 3 | 7 | `los-angeles` | 0 | `GroupIndex(1)` | `FeeIndex(0)` |
+| 4 | 8 | `new-york` | 0 | `GroupIndex(1)` | `FeeIndex(0)` |
+| 5 | 9 | `chicago` | 0 | `GroupIndex(1)` | `FeeIndex(0)` |
+| 6 | 10 | `washington` | 0 | `GroupIndex(1)` | `FeeIndex(0)` |
+| 7 | 11 | `pittsburgh` | 0 | `GroupIndex(1)` | `FeeIndex(0)` |
+| 8 | 12 | `miami-beach` | 0 | `GroupIndex(1)` | `FeeIndex(0)` |
+| 9 | 13 | `boston` | 0 | `GroupIndex(1)` | `FeeIndex(0)` |
+| 10 | 14 | `brooklyn` | 0 | `GroupIndex(1)` | `FeeIndex(0)` |
+| 11 | 15 | `austin` | 0 | `GroupIndex(1)` | `FeeIndex(0)` |
+| 12 | 16 | `denver` | 0 | `GroupIndex(1)` | `FeeIndex(0)` |
+| 16 | 17 | `sol` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 17 | 18 | `xrp` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 18 | 19 | `sui` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 19 | 20 | `ena` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 20 | 21 | `arb` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 21 | 22 | `trx` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 22 | 23 | `apt` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 23 | 24 | `pol` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 24 | 25 | `ton` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 25 | 26 | `ada` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 26 | 27 | `ltc` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 27 | 28 | `doge` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 28 | 29 | `aster` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 29 | 30 | `bnb` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 30 | 31 | `pump` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 31 | 32 | `avax` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 32 | 33 | `aave` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 33 | 34 | `pengu` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 34 | 35 | `link` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 35 | 36 | `bonk` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 36 | 37 | `shib` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 37 | 38 | `trump` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 38 | 39 | `near` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 39 | 40 | `kaito` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 40 | 41 | `mnt` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 41 | 42 | `ip` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 42 | 43 | `virtual` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 43 | 44 | `hype` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 44 | 45 | `zec` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
+| 48 | 49 | `nibi` | 0 | `GroupIndex(0)` | `FeeIndex(0)` |
diff --git a/ai-skills/sai-rest-api/SKILL.md b/ai-skills/sai-rest-api/SKILL.md
new file mode 100644
index 0000000..fec0b9a
--- /dev/null
+++ b/ai-skills/sai-rest-api/SKILL.md
@@ -0,0 +1,239 @@
+---
+name: sai-rest-api
+description: Query the Sai REST API (DexPal) for aggregated metrics, stats, yield, markets, referrals, and health checks. Use when working with Sai REST endpoints, /dexpal/v1/*, sai-api.nibiru.fi, or when needing quick stats instead of full GraphQL.
+---
+
+# Sai: REST API Notes
+
+This guide covers the **REST API** β DexPal aggregated metrics and feeds (`/dexpal/v1/*`, `/health`, `/`). Use it for quick stats and health checks.
+
+- [Selecting the Right API](#selecting-the-right-api)
+- [Available Endpoints](#available-endpoints)
+ - [24h Time Window Semantics](#24h-time-window-semantics)
+- [Usage Examples](#usage-examples)
+ - [GET /](#get-)
+ - [GET /dexpal/v1/stats](#get-dexpalv1stats)
+ - [GET /dexpal/v1/metrics](#get-dexpalv1metrics)
+ - [GET /dexpal/v1/referrals](#get-dexpalv1referrals)
+ - [GET /dexpal/v1/markets](#get-dexpalv1markets)
+ - [GET /dexpal/v1/markets/details](#get-dexpalv1marketsdetails)
+ - [GET /dexpal/v1/yield](#get-dexpalv1yield)
+ - [GET /health](#get-health)
+- [References](#references)
+
+## Selecting the Right API
+
+The **GraphQL API** is the canonical, comprehensive Sai API (perp, lp, oracle, fee, subscriptions). Use GraphQL for the full API.
+
+| Environment | URL |
+| ----------- | --- |
+| **Mainnet GraphQL** | https://sai-keeper.nibiru.fi |
+| **Testnet GraphQL** | https://sai-keeper.testnet-2.nibiru.fi |
+| **Mainnet** | https://sai-api.nibiru.fi |
+| **Testnet** | https://sai-api.testnet-2.nibiru.fi |
+
+**Related skill**: For the full Sai API (perp trades, LP positions, oracle prices, fees, subscriptions), use the **sai-keeper-graphql** skill.
+
+## Available Endpoints
+
+| Path | Description |
+|------|-------------|
+| [/](#get-) | API info. Lists all queries. |
+| [/dexpal/v1/stats](#get-dexpalv1stats) | Detailed exchange statistics (volume 24h, OI, TVL, fees, etc.) |
+| [/dexpal/v1/referrals](#get-dexpalv1referrals) | DexPal referral reports |
+| [/dexpal/v1/markets/details](#get-dexpalv1marketsdetails) | Detailed market data for all trading pairs (volume 24h longs/shorts, OI, etc.) |
+| [/dexpal/v1/yield](#get-dexpalv1yield) | Yield earning opportunities (LP vaults) |
+| [/dexpal/v1/markets](#get-dexpalv1markets) | DexPal markets feed |
+| [/dexpal/v1/metrics](#get-dexpalv1metrics) | DexPal aggregate metrics |
+| [/health](#get-health) | Health check |
+
+### 24h Time Window Semantics
+
+- **`/dexpal/v1/stats`** (optimized path): 24h window = `NOW() - 24h` (wall clock at cache refresh)
+- **`/dexpal/v1/markets/details`**: 24h window = latest block timestamp - 24h (blockchain time)
+
+## Usage Examples
+
+Examples use `curl -s`; the `-s` (silent) flag suppresses curl progress output so piping to `jq` works cleanly.
+
+### GET /
+
+**Request:**
+```bash
+curl -s https://sai-api.nibiru.fi/ | jq .
+```
+
+**Response shape:**
+```json
+{
+ "service": "SAI Keeper Statistics API",
+ "version": "1.0.0",
+ "endpoints": {
+ "/dexpal/v1/markets": "GET - DexPal markets feed",
+ "/dexpal/v1/metrics": "GET - DexPal aggregate metrics",
+ "/dexpal/v1/stats": "GET - Detailed exchange statistics",
+ "/dexpal/v1/markets/details": "GET - Detailed market data for all trading pairs",
+ "/dexpal/v1/yield": "GET - Yield earning opportunities data",
+ "/dexpal/v1/referrals": "GET - DexPal referral reports",
+ "/health": "GET - Health check"
+ }
+}
+```
+
+### GET /dexpal/v1/stats
+
+**Request:**
+```bash
+# Live statistics (current window)
+curl -s https://sai-api.nibiru.fi/dexpal/v1/stats | jq .
+
+# Historical statistics for a specific date
+curl -s "https://sai-api.nibiru.fi/dexpal/v1/stats?date=2026-02-25" | jq .
+```
+
+**Query Parameters:**
+- `date` (optional): The date for which to retrieve statistics in `YYYY-MM-DD` format (UTC).
+ - Omitted or `date=today`: Returns live exchange statistics.
+ - Past date: Returns historical aggregate statistics for that specific day.
+ - Future date: Returns a `400 Bad Request` error.
+ - Invalid format: Returns a `400 Bad Request` error.
+
+**Response shape:**
+```json
+{
+ "trading_volume_24h": 2584.5,
+ "trading_volume_all_time": 200418.54,
+ "total_trades_24h": 11,
+ "total_trades_all_time": 488,
+ "open_interest": 0,
+ "total_users_24h": 6,
+ "total_users_all_time": 39,
+ "total_open_positions": 31,
+ "tvl": 357801.98,
+ "accrued_trading_fees_24h": 128.86,
+ "accrued_trading_fees_all_time": 1949.43
+}
+```
+*(Note: For historical dates, `tvl` and `total_open_positions` may be `null` as they are not currently tracked in daily snapshots.)*
+
+### GET /dexpal/v1/metrics
+
+**Request:**
+```bash
+curl -s https://sai-api.nibiru.fi/dexpal/v1/metrics | jq .
+```
+
+**Response shape:**
+```json
+{
+ "volume_24h": 2256.93,
+ "open_interest_24h": 251.9,
+ "fees_24h": 2.2,
+ "volume_all_time": 200418.54,
+ "tvl": 357801.98,
+ "traders_24h": 4,
+ "timestamp": "2026-02-18T23:33:04.883348856Z"
+}
+```
+
+### GET /dexpal/v1/referrals
+
+**Request:**
+```bash
+curl -s https://sai-api.nibiru.fi/dexpal/v1/referrals | jq .
+```
+
+**Response shape:**
+```json
+{
+ "reports": null
+}
+```
+(`reports` can be null or an array of report objects when data exists.)
+
+### GET /dexpal/v1/markets
+
+**Request:**
+```bash
+curl -s https://sai-api.nibiru.fi/dexpal/v1/markets | jq .
+```
+
+**Response shape:**
+```json
+{
+ "markets": [],
+ "timestamp": "2026-02-19T00:15:27.502648961Z"
+}
+```
+
+### GET /dexpal/v1/markets/details
+
+**Request:**
+```bash
+curl -s https://sai-api.nibiru.fi/dexpal/v1/markets/details | jq .
+```
+
+**Response shape:**
+```json
+{
+ "markets": null
+}
+```
+(`markets` can be null or an array of market objects with base_currency, quote_currency, trading_volume_24h_longs, trading_volume_24h_shorts, open_interest, etc.)
+
+### GET /dexpal/v1/yield
+
+**Request:**
+```bash
+curl -s https://sai-api.nibiru.fi/dexpal/v1/yield | jq .
+```
+
+**Response shape:**
+```json
+{
+ "yield_opportunities": [
+ {
+ "name": "SAI Liquidity Vault - nibi1m...kfej",
+ "type": "vault",
+ "description": "Provide liquidity to SAI perpetual futures trading and earn yield from trading fees and protocol revenue. Epoch-based withdrawals ensure stable liquidity.",
+ "network": "nibiru",
+ "contract_address": "nibi1mrplvu3scplnrgns96kg0j8pk3l2p9c7eaz0qdedx0kt3vmcujyqrjkfej",
+ "accepted_deposits": ["stnibi", "usdc"],
+ "tvl": 356356.08,
+ "average_apy": 0.06,
+ "reward_token": "NUSD",
+ "reward_frequency": "per_block",
+ "min_deposit": 10,
+ "max_deposit": 1000000,
+ "withdrawal_lockup_days": 9,
+ "early_withdrawal_penalty": 0,
+ "date_listed": "2025-12-19T19:29:05.788346Z",
+ "details_link": "https://app.sai.zone/vault/nibi1mrplvu3scplnrgns96kg0j8pk3l2p9c7eaz0qdedx0kt3vmcujyqrjkfej"
+ }
+ ]
+}
+```
+
+### GET /health
+
+**Request:**
+```bash
+curl -s https://sai-api.nibiru.fi/health
+```
+(Plain text response; omit jq.)
+
+**Response shape:**
+```
+OK
+```
+
+## References
+
+- **The `sai-keeper` repo**: `$HOME/ki/sai-keeper` β root `README.md` documents stats, health, and /; full endpoint list is in `api/server.go`
+- **The `nibi-iac` repo**: `$HOME/ki/nibi-iac` β `terraform/_modules/sai-keeper` for deployment
+
+The sai-keeper Terraform module (in the `nibi-iac` repo) exposes two domains: `domain_graphql` (sai-keeper.*) for GraphQL, `domain_rest` (sai-api.*) for this REST API.
+
+Source: the `nibi-iac` repo β mainnet in `mainnet-gcp/06-mainnet.tf` (domain_rest), testnet in `itn2-gcp/07-testnet-2.tf`. DNS: `nibiru-gcp/02-dns.tf`.
+
+Notes on querying the Sai REST API (the `sai-keeper` repo run with `-api` flag). Endpoints from the `nibi-iac` repo; paths from the `sai-keeper` repo.
diff --git a/aictx/README.md b/ctxcat/README.md
similarity index 68%
rename from aictx/README.md
rename to ctxcat/README.md
index 3e6c1f4..7f6ed5c 100644
--- a/aictx/README.md
+++ b/ctxcat/README.md
@@ -1,4 +1,4 @@
-# aictx
+# ctxcat
File to context converter for passing files to feed LLMs. Combines multiple files into a single, well-formatted context with proper syntax highlighting.
@@ -6,24 +6,24 @@ File to context converter for passing files to feed LLMs. Combines multiple file
```bash
# Process current directory
-aictx .
+ctxcat .
# Process specific files
-aictx main.go src/lib.rs README.md
+ctxcat main.go src/lib.rs README.md
# Use glob patterns
-aictx "src/*.go" "**/*.md"
+ctxcat "src/*.go" "**/*.md"
# Limit recursion depth
-aictx --level 2 .
+ctxcat --level 2 .
# Output to file
-aictx --output context.md src/
+ctxcat --output context.md src/
```
## Key Concepts
-**Recursive Processing**: By default, `aictx` walks entire directory trees. Use `--level` to limit depth.
+**Recursive Processing**: By default, `ctxcat` walks entire directory trees. Use `--level` to limit depth.
**Gitignore Respect**: Automatically finds and applies `.gitignore` rules from your repo root. No need to manually exclude `node_modules`, `target/`, etc.
@@ -35,25 +35,25 @@ aictx --output context.md src/
```bash
# Get full project context for an LLM
-aictx . --output full-context.md
+ctxcat . --output full-context.md
# Just the source code, skip deep nesting
-aictx src/ --level 3
+ctxcat src/ --level 3
# Specific file patterns across the project
-aictx "*.go" "*.md" "Dockerfile*"
+ctxcat "*.go" "*.md" "Dockerfile*"
# Multiple directories with different purposes
-aictx src/ docs/ scripts/ --output project-overview.md
+ctxcat src/ docs/ scripts/ --output project-overview.md
```
## Installation
```bash
-go install github.com/Unique-Divine/jiyuu/aictx@latest
+go install github.com/Unique-Divine/jiyuu/ctxcat@latest
# Or build from source
-cd jiyuu/aictx && just install
+cd jiyuu/ctxcat && just install
```
## Flags
@@ -63,11 +63,11 @@ cd jiyuu/aictx && just install
```bash
# Save context to a file for later use
-aictx . -o project-context.md
+ctxcat . -o project-context.md
# Only traverse 2 levels deep in directories
-aictx src/ --level 2
+ctxcat src/ --level 2
# Combine both: shallow traversal saved to file
-aictx . --level 1 --output overview.md
+ctxcat . --level 1 --output overview.md
```
diff --git a/aictx/go.mod b/ctxcat/go.mod
similarity index 84%
rename from aictx/go.mod
rename to ctxcat/go.mod
index fc2c50c..598b522 100644
--- a/aictx/go.mod
+++ b/ctxcat/go.mod
@@ -1,4 +1,4 @@
-module github.com/Unique-Divine/jiyuu/aictx
+module github.com/Unique-Divine/jiyuu/ctxcat
go 1.22.12
diff --git a/aictx/go.sum b/ctxcat/go.sum
similarity index 100%
rename from aictx/go.sum
rename to ctxcat/go.sum
diff --git a/aictx/justfile b/ctxcat/justfile
similarity index 100%
rename from aictx/justfile
rename to ctxcat/justfile
diff --git a/aictx/main.go b/ctxcat/main.go
similarity index 70%
rename from aictx/main.go
rename to ctxcat/main.go
index a29975b..b08820c 100644
--- a/aictx/main.go
+++ b/ctxcat/main.go
@@ -5,11 +5,11 @@ import (
"fmt"
"os"
- aictx "github.com/Unique-Divine/jiyuu/aictx/src"
+ ctxcat "github.com/Unique-Divine/jiyuu/ctxcat/src"
)
func main() {
- appCmd := aictx.NewAppCmd()
+ appCmd := ctxcat.NewAppCmd()
if err := appCmd.Run(context.Background(), os.Args); err != nil {
fmt.Fprintln(appCmd.ErrWriter, "error: ", err)
os.Exit(1)
diff --git a/aictx/src/const.go b/ctxcat/src/const.go
similarity index 93%
rename from aictx/src/const.go
rename to ctxcat/src/const.go
index 265205c..188b4b3 100644
--- a/aictx/src/const.go
+++ b/ctxcat/src/const.go
@@ -1,4 +1,4 @@
-package aictx
+package ctxcat
const (
codeMarkerDepth4 string = "````"
diff --git a/aictx/src/aictx.go b/ctxcat/src/ctxcat.go
similarity index 98%
rename from aictx/src/aictx.go
rename to ctxcat/src/ctxcat.go
index 5b66038..1302850 100644
--- a/aictx/src/aictx.go
+++ b/ctxcat/src/ctxcat.go
@@ -1,4 +1,4 @@
-package aictx
+package ctxcat
import (
"bytes"
@@ -11,13 +11,13 @@ import (
"path/filepath"
"strings"
- "github.com/Unique-Divine/jiyuu/aictx/src/gitignore"
+ "github.com/Unique-Divine/jiyuu/ctxcat/src/gitignore"
cli "github.com/urfave/cli/v3"
)
func NewAppCmd() *cli.Command {
return &cli.Command{
- Name: "aictx",
+ Name: "ctxcat",
Usage: "Combine files into a single LLM-friendly output",
ArgsUsage: " [path2 ...]",
Flags: []cli.Flag{
@@ -57,7 +57,7 @@ func actionFunc(goCtx context.Context, c *cli.Command) error {
rawCmdArgs := c.Args().Slice()
if len(rawCmdArgs) == 0 {
_, err := c.ErrWriter.Write(
- []byte("aictx requires at least one path (file, directory, or glob)\n\n"),
+ []byte("ctxcat requires at least one path (file, directory, or glob)\n\n"),
)
if err != nil {
return err
diff --git a/aictx/src/aictx_test.go b/ctxcat/src/ctxcat_test.go
similarity index 98%
rename from aictx/src/aictx_test.go
rename to ctxcat/src/ctxcat_test.go
index 34eabad..cc900fd 100644
--- a/aictx/src/aictx_test.go
+++ b/ctxcat/src/ctxcat_test.go
@@ -1,4 +1,4 @@
-package aictx
+package ctxcat
import (
"bytes"
@@ -280,9 +280,9 @@ func (s *S) TestCLI_LevelLimitIntegration() {
app, outBuf, errBuf := NewTestingCLI()
// Call the CLI as if from the shell:
- // aictx --level 1
+ // ctxcat --level 1
args := []string{
- "aictx", // argv[0] β binary name
+ "ctxcat", // argv[0] β binary name
"--level=1", // global flag
root, // positional arg
}
@@ -301,7 +301,7 @@ func (s *S) TestCLI_LevelLimitIntegration() {
func (s *S) TestCLI_NoArgs_ShowsError() {
app, outBuf, errBuf := NewTestingCLI()
- err := app.Run(context.Background(), []string{"aictx"})
+ err := app.Run(context.Background(), []string{"ctxcat"})
s.Require().NoErrorf(err, "stderr: %s", errBuf) // from cli.Exit
s.Contains(errBuf.String(), "requires at least one path")
s.Contains(outBuf.String(), "Combine files into a single LLM-friendly output")
diff --git a/aictx/src/gitignore/ignore.go b/ctxcat/src/gitignore/ignore.go
similarity index 100%
rename from aictx/src/gitignore/ignore.go
rename to ctxcat/src/gitignore/ignore.go
diff --git a/aictx/src/gitignore/ignore_node_test.go b/ctxcat/src/gitignore/ignore_node_test.go
similarity index 100%
rename from aictx/src/gitignore/ignore_node_test.go
rename to ctxcat/src/gitignore/ignore_node_test.go
diff --git a/aictx/src/gitignore/ignore_test.go b/ctxcat/src/gitignore/ignore_test.go
similarity index 100%
rename from aictx/src/gitignore/ignore_test.go
rename to ctxcat/src/gitignore/ignore_test.go
diff --git a/aictx/src/languages.go b/ctxcat/src/languages.go
similarity index 99%
rename from aictx/src/languages.go
rename to ctxcat/src/languages.go
index c7a9c09..cbf0f53 100644
--- a/aictx/src/languages.go
+++ b/ctxcat/src/languages.go
@@ -1,4 +1,4 @@
-package aictx
+package ctxcat
import (
"path/filepath"
diff --git a/justfile b/justfile
index 6718785..920008b 100644
--- a/justfile
+++ b/justfile
@@ -16,3 +16,18 @@ install:
# Format with prettier
fmt:
bun run prettier --write .
+
+# Sync repo ai-skills into ~/.cursor/skills. Flags: --force, --dry-run
+skills-pull *args:
+ #!/usr/bin/env bash
+ bash scripts/skills-pull.sh {{args}}
+
+# Sync ~/.cursor/skills into repo ai-skills. Flags: --force, --dry-run
+skills-push *args:
+ #!/usr/bin/env bash
+ bash scripts/skills-push.sh {{args}}
+
+# Show the diff between repo ai-skills and ~/.cursor/skills
+skills-diff *args:
+ #!/usr/bin/env bash
+ bash scripts/skills-diff.sh {{args}}
diff --git a/scripts/Cargo.toml b/scripts/Cargo.toml
deleted file mode 100644
index e624231..0000000
--- a/scripts/Cargo.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[package]
-name = "scripts"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
diff --git a/scripts/skills-diff.sh b/scripts/skills-diff.sh
new file mode 100644
index 0000000..36edf4c
--- /dev/null
+++ b/scripts/skills-diff.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+# View ai-skills diff: compare repo ai-skills against ~/.cursor/skills.
+# Run via: just skills-diff
+
+set -euo pipefail
+
+print_usage() {
+ cat <<'EOF'
+Usage: skills-diff.sh
+
+Print the recursive diff between repo ai-skills and ~/.cursor/skills.
+
+Exit codes:
+ 0 No differences
+ 1 Differences found
+ 2 Error
+EOF
+}
+
+if [[ $# -gt 0 ]]; then
+ case "$1" in
+ -h|--help)
+ print_usage
+ exit 0
+ ;;
+ *)
+ echo "Unknown argument: $1" >&2
+ print_usage >&2
+ exit 2
+ ;;
+ esac
+fi
+
+skills_dir="${CURSOR_SKILLS_DIR:-$HOME/.cursor/skills}"
+repo_skills_dir="${REPO_SKILLS_DIR:-ai-skills}"
+
+if [[ ! -d "$repo_skills_dir" ]]; then
+ echo "Expected repo skills directory \"$repo_skills_dir\" does not exist." >&2
+ exit 2
+fi
+
+if [[ ! -e "$skills_dir" ]]; then
+ echo "Local skills directory \"$skills_dir\" does not exist." >&2
+ exit 1
+fi
+
+diff_exit_code=0
+diff -ruN "$repo_skills_dir/" "$skills_dir/" || diff_exit_code=$?
+
+if [[ $diff_exit_code -le 1 ]]; then
+ exit $diff_exit_code
+fi
+
+echo "diff failed while comparing \"$repo_skills_dir\" and \"$skills_dir\"" >&2
+exit 2
diff --git a/scripts/skills-pull.sh b/scripts/skills-pull.sh
new file mode 100644
index 0000000..875b658
--- /dev/null
+++ b/scripts/skills-pull.sh
@@ -0,0 +1,90 @@
+#!/usr/bin/env bash
+# Sync ai-skills: pull from repo ai-skills into ~/.cursor/skills.
+# Run via: just skills-pull [--force] [--dry-run]
+
+set -euo pipefail
+
+print_usage() {
+ cat <<'EOF'
+Usage: skills-pull.sh [--force] [--dry-run]
+
+Pull repo ai-skills into ~/.cursor/skills.
+
+Options:
+ -f, --force Overwrite without prompting
+ -n, --dry-run Preview the overwrite without changing files
+ -h, --help Show this help text
+EOF
+}
+
+force=false
+dry_run=false
+
+for arg in "$@"; do
+ case "$arg" in
+ -f|--force)
+ force=true
+ ;;
+ -n|--dry-run)
+ dry_run=true
+ ;;
+ -h|--help)
+ print_usage
+ exit 0
+ ;;
+ *)
+ echo "Unknown argument: $arg" >&2
+ print_usage >&2
+ exit 2
+ ;;
+ esac
+done
+
+skills_dir="${CURSOR_SKILLS_DIR:-$HOME/.cursor/skills}"
+repo_skills_dir="${REPO_SKILLS_DIR:-ai-skills}"
+
+if [[ ! -d "$repo_skills_dir" ]]; then
+ echo "Expected repo skills directory \"$repo_skills_dir\" does not exist." >&2
+ exit 1
+fi
+
+diff_exit_code=1
+if [[ -d "$skills_dir" ]]; then
+ diff_exit_code=0
+ diff -ruN "$repo_skills_dir/" "$skills_dir/" >/dev/null || diff_exit_code=$?
+fi
+
+if [[ $diff_exit_code -eq 0 ]]; then
+ echo "AI skills already match. No sync needed."
+ exit 0
+fi
+
+if [[ $diff_exit_code -gt 1 ]]; then
+ echo "diff failed while comparing \"$repo_skills_dir\" and \"$skills_dir\"" >&2
+ exit 2
+fi
+
+if [[ "$dry_run" == true ]]; then
+ if [[ -d "$skills_dir" || -f "$skills_dir" ]]; then
+ echo "Dry run: would replace \"$skills_dir\" with \"$repo_skills_dir\"."
+ else
+ echo "Dry run: would create \"$skills_dir\" from \"$repo_skills_dir\"."
+ fi
+ exit 0
+fi
+
+if [[ "$force" != true ]]; then
+ echo "Differences found between \"$repo_skills_dir\" and \"$skills_dir\"." >&2
+ echo "Run \`just skills-diff\` to inspect the changes." >&2
+ echo "Run \`just skills-pull --force\` to overwrite local Cursor skills from the repo." >&2
+ echo "Run \`just skills-pull --dry-run\` to preview the overwrite without changing files." >&2
+ exit 1
+fi
+
+if [[ -d "$skills_dir" || -f "$skills_dir" ]]; then
+ echo "Clearing old skills from \"$skills_dir\""
+ rm -rf "$skills_dir"
+fi
+
+echo "Injecting skills from path $repo_skills_dir"
+cp -r "$repo_skills_dir" "$skills_dir"
diff --git a/scripts/skills-push.sh b/scripts/skills-push.sh
new file mode 100644
index 0000000..ece4cbc
--- /dev/null
+++ b/scripts/skills-push.sh
@@ -0,0 +1,90 @@
+#!/usr/bin/env bash
+# Sync ai-skills: push from ~/.cursor/skills into repo ai-skills.
+# Run via: just skills-push [--force] [--dry-run]
+
+set -euo pipefail
+
+print_usage() {
+ cat <<'EOF'
+Usage: skills-push.sh [--force] [--dry-run]
+
+Push ~/.cursor/skills into repo ai-skills.
+
+Options:
+ -f, --force Overwrite without prompting
+ -n, --dry-run Preview the overwrite without changing files
+ -h, --help Show this help text
+EOF
+}
+
+force=false
+dry_run=false
+
+for arg in "$@"; do
+ case "$arg" in
+ -f|--force)
+ force=true
+ ;;
+ -n|--dry-run)
+ dry_run=true
+ ;;
+ -h|--help)
+ print_usage
+ exit 0
+ ;;
+ *)
+ echo "Unknown argument: $arg" >&2
+ print_usage >&2
+ exit 2
+ ;;
+ esac
+done
+
+skills_dir="${CURSOR_SKILLS_DIR:-$HOME/.cursor/skills}"
+repo_skills_dir="${REPO_SKILLS_DIR:-ai-skills}"
+
+if [[ ! -d "$skills_dir" ]]; then
+ echo "Expected local skills directory \"$skills_dir\" does not exist." >&2
+ exit 1
+fi
+
+diff_exit_code=1
+if [[ -d "$repo_skills_dir" ]]; then
+ diff_exit_code=0
+ diff -ruN "$skills_dir/" "$repo_skills_dir/" >/dev/null || diff_exit_code=$?
+fi
+
+if [[ $diff_exit_code -eq 0 ]]; then
+ echo "AI skills already match. No sync needed."
+ exit 0
+fi
+
+if [[ $diff_exit_code -gt 1 ]]; then
+ echo "diff failed while comparing \"$skills_dir\" and \"$repo_skills_dir\"" >&2
+ exit 2
+fi
+
+if [[ "$dry_run" == true ]]; then
+ if [[ -d "$repo_skills_dir" || -f "$repo_skills_dir" ]]; then
+ echo "Dry run: would replace \"$repo_skills_dir\" with \"$skills_dir\"."
+ else
+ echo "Dry run: would create \"$repo_skills_dir\" from \"$skills_dir\"."
+ fi
+ exit 0
+fi
+
+if [[ "$force" != true ]]; then
+ echo "Differences found between \"$skills_dir\" and \"$repo_skills_dir\"." >&2
+ echo "Run \`just skills-diff\` to inspect the changes." >&2
+ echo "Run \`just skills-push --force\` to overwrite repo ai-skills from local Cursor skills." >&2
+ echo "Run \`just skills-push --dry-run\` to preview the overwrite without changing files." >&2
+ exit 1
+fi
+
+if [[ -d "$repo_skills_dir" || -f "$repo_skills_dir" ]]; then
+ echo "Clearing old skills from \"$repo_skills_dir\""
+ rm -rf "$repo_skills_dir"
+fi
+
+echo "Injecting skills from path $skills_dir"
+cp -r "$skills_dir" "$repo_skills_dir"
diff --git a/scripts/src/main.rs b/scripts/src/main.rs
deleted file mode 100644
index e7a11a9..0000000
--- a/scripts/src/main.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-fn main() {
- println!("Hello, world!");
-}