AI optimization from your terminal. AIR (Artificial Intelligence Ranking) by airanks makes AI
optimization visible — a 0–10 score for how often and how well AI answers cite a site. Look up any
domain's AIR score live at airanks.net, or install the browser toolbar at
airanks.net/toolbar. This repo is air, the native Rust build of
the AIR command-line client — same API, same auth, same output, just compiled instead of interpreted.
$ air airanks.net
🌐 airanks.net 🏆 AIR 7/10 ██████████████████████░░░░░░
📊 tracked — 412 occurrences · 38 phrases · 19 brands
📄 llms.txt ✔ llms-full ✔ ai.txt ✘ robots.txt ✔ json-ld ✔ (Organization)
🛡 ai crawlers: 6 allowed · 1 partial · 2 blocked of 9
- 🤔 Why a native build?
- 📦 Install
- ⚡ Quickstart
- 🧭 Commands
- 🔄 How a lookup works
- 🔍 Search mode
- 🔐 Shared authentication — one login, every AIR client
- 🎛️ Environment variables
- 🚦 Exit codes
- 🎨 Output modes
- ⬆️ Auto-update
- 🛠️ Development
- 🌐 Other AIR / airanks clients
- 📜 License
Most of what people call "SEO for AI" is guesswork — nobody outside the model vendors can see which pages actually got cited. AIR is built from real, replayable observations of what AI answers actually pointed to, not a simulation of what might rank. This client puts that number one command away — in a shell, a CI pipeline, or a git pre-push hook — instead of only a browser tab.
air is the Rust port of the reference node-cli: same wire contract, same auth file, same rendered
output — down to the Font Awesome glyphs and the truecolor score gauge — just compiled to a single
small binary (opt-level = "z", LTO, stripped) with no CLI-parsing framework. Flag handling is the
same handful of starts_with('-') checks the Node reference implementation uses — see
src/main.rs.
Needs a Rust toolchain (rustup.rs).
cargo install --path . # from a checkout of this repo
cargo install air-cli # from crates.io, once published🍺 Homebrew
The airanks-net/tap Homebrew formula currently builds air from go-cli (brew install airanks-net/tap/air), not this Rust crate — the auto-updater's fallback hint
(brew upgrade air (or) cargo install air-cli --force) assumes either build is interchangeable
since both ship the same air binary name and wire contract. If you want the Rust build
specifically, use cargo install.
Either install drops an air binary on your $PATH.
air airanks.net # domain lookup
air login # required — anonymous lookups now get a 401That's it — a domain that's never been looked up before gets hydrated on the spot; air polls
quietly in the background until the score is ready.
| Command | What it does |
|---|---|
air <domain> |
Look up a domain's AIR score. Polls while it's still being gathered. |
air "<keyword phrase>" |
Multi-word input searches domains/brands/phrases instead. |
air -d <input> / --domain |
Force domain mode. |
air -k <input> / --keyword / -p / --phrase |
Force search mode. |
air <input> --json |
Machine-readable JSON on stdout. |
air <input> --txt |
Plain text — no color, no icons (same as TERM=dumb, CI=true, NO_COLOR). |
air login |
Paste an API key, or log in via your browser (PKCE). |
air logout |
Remove the locally saved token (local only — server-side token still valid until it expires). |
air whoami |
Show who you're logged in as, and on what tier. |
air --help / -h |
Usage text. |
On first run with nothing configured (no AIR_API_KEY, no saved login) and an interactive TTY, air
shows a one-time setup wizard — paste a key, log in via browser, or continue anonymously — see
should_run_setup_wizard. Note: "continue anonymously" just skips the wizard, it
doesn't get you working lookups — anonymous requests now get a 401 from the API, so you'll need to
run air login before a lookup will actually succeed.
GET /v1/domains/{host} always 200s. The first time anyone looks up a host, the server dispatches
hydration and reports ai_files.status: "pending" — the CLI just polls quietly until it flips to
ready or a ~180s wall-clock budget runs out (AIR_POLL_MAX_MS), honoring Retry-After on a 429 along
the way. A live braille spinner runs on stderr while it waits.
flowchart TD
A["$ air <input>"] --> B{login / logout / whoami?}
B -- yes --> Z[run auth command, exit]
B -- no --> C{looks like a hostname?<br/>-d forces domain, -k/-p forces search}
C -- no --> S["GET /v1/search?q=..."] --> S2[render domains · brands · phrases]
C -- yes --> D["GET /v1/domains/{host}"]
D --> E{ai_files.status == pending?}
E -- no --> F[render AIR score + gauge]
E -- yes --> G{deadline left?<br/>~180s budget}
G -- yes --> H["sleep AIR_POLL_MS,<br/>spinner on stderr"] --> D
G -- no --> I["render 'still gathering'<br/>exit code 2"]
Anything that doesn't parse as a single hostname (spaces, or forced with -k/-p) hits
GET /v1/search?q=... and prints up to 5 matches each across domains, brands, and
phrases, each with its own AIR score.
air "best crm software"A free account is now required to use the API — grab a token at
airanks.net/tokens and set it via AIR_API_KEY or air login. The
official browser toolbar is the only caller exempt from this; every CLI/SDK client needs a token.
air is one of several official AIR clients — Node, Rust, Go, and a PHP Composer package — and they
all resolve auth identically, per API-CONTRACT.md:
AIR_API_KEYenv var, if set — always wins, always attaches (explicit intent).- Otherwise
~/.config/air/auth.json(mode0600, dir0700) — written by whichever client you last ranloginin. A file-sourced token only rides to the host it was minted for, so it can't leak if you repointAIR_API_BASEsomewhere else. - Otherwise you're anonymous — and the API now rejects that with a 401. Run
air loginfirst.
Run air login once, in any AIR client, and every other AIR client on the machine picks up the
same session — same token, same file, same host-scoped attach rule.
🔑 Browser (PKCE S256) login sequence
sequenceDiagram
autonumber
participant CLI as air login
participant API as airanks.net API
participant Browser as your browser
CLI->>CLI: generate PKCE verifier + S256 challenge
CLI->>API: POST /v1/cli/auth {code_challenge}
API-->>CLI: {nonce, browser_url, confirmation_code, interval, expires_in}
CLI->>Browser: open browser_url (same-origin only)
Note over CLI,Browser: user enters confirmation_code, approves
loop every `interval`s until expires_in
CLI->>API: POST /v1/cli/auth/poll {nonce, code_verifier}
API-->>CLI: {status: pending | ok | denied | expired | forbidden}
end
API-->>CLI: status "ok" → {token, user, expires_at}
CLI->>CLI: write ~/.config/air/auth.json (0600)
Over SSH, or headless Linux with no $DISPLAY, air prints the URL instead of trying to auto-open
it. It also refuses to auto-open a cross-origin browser_url — see open_browser.
| Variable | Effect |
|---|---|
AIR_API_KEY |
Bearer token, wins over any saved login (see above). |
AIR_API_BASE |
Override the API base — default https://airanks.net/api/v1. |
AIR_POLL_MS |
Interval between pending polls (ms). Default 20000. |
AIR_POLL_MAX_MS |
Total wall-clock budget for a lookup (ms) — polls and 429 backoff both count against it. Default 180000. |
AIR_NO_UPDATE |
Set (any value) to disable the once-a-day auto-update check. |
NO_COLOR |
Drop ANSI color; icons/glyphs stay. |
TERM=dumb / CI=true |
Same as --txt — plain text, no color, no icons. |
COLUMNS |
Clamps wrapped output width (max 74 cols). |
| Code | Meaning |
|---|---|
0 |
Normal result. |
1 |
Hard failure — bad input, API error, or a rate-limit that outlasted the poll deadline. |
2 |
Domain is still being gathered at the deadline — re-run in a bit. |
- Default — truecolor score gauge, Font Awesome glyphs (Nerd Font recommended), OSC-8 clickable links in supporting terminals.
--json— the raw APIdataobject, pretty-printed, for scripts and CI.--txt— no color, no glyphs, no gauge. Same asTERM=dumb,CI=true, or a non-TTY stdout.
Once a day, on an interactive run only, air checks GitHub Releases
(api.github.com/repos/airanks-net/rust-cli) for a newer version and — if found — best-effort runs
cargo install air-cli --force in the background (60s timeout, then killed). Every failure mode
(offline, GitHub down, a slow install) is swallowed; your actual command always proceeds either way.
Opt out with AIR_NO_UPDATE. See src/update.rs.
cargo build
cargo test
cargo run -- airanks.net📦 Dependencies (kept deliberately small)
| Crate | Why |
|---|---|
reqwest (blocking, rustls) |
HTTP client |
serde / serde_json |
Wire format |
url |
RFC-3986-correct hostname parsing |
sha2 / rand / base64 |
PKCE (S256 challenge, verifier) |
chrono |
Timestamps (auth.json, whoami expiry) |
No CLI-parsing crate — argument handling is a handful of flag checks mirroring the Node reference
implementation. Unit tests live inline (#[cfg(test)]) in hostname.rs,
auth.rs, and update.rs.
Every client speaks the same API-CONTRACT.md and shares the same login.
| Repo | What it is |
|---|---|
node-cli |
Reference implementation — the CLI this one ports. |
go-cli |
Go build of air (also what the Homebrew tap builds today). |
composer-package |
airanks-net/api-client — PHP library, not a CLI. |
chrome-extension |
The AIR toolbar — any site's AI Rank in your browser. |
mcp-server |
MCP server exposing AIR lookups as agent tools. |
js-sdk |
JS/TS SDK for Node & browser. |
python-sdk |
Python SDK (pip install airanks). |
homebrew-tap |
brew install airanks-net/tap/air. |
MIT — see LICENSE.
Made to keep AI optimization honest — one terminal command, one shared login, zero dashboard tabs. airanks.net · airanks.net/toolbar 🚀