SELAT — discover, then pay (self-custody)
This is a concise DRIVER written for the plugin. The published @selat-ai/selat-discovery SKILL.md (bundled inside @selat-ai/selat-cli) remains the source of truth for exact subcommand flags, output shapes, and any commands added after the pinned CLI version. Where they conflict, the published skill wins. The command surface below was verified against @selat-ai/selat-cli@0.16.3 and @selat-ai/selat-discovery@0.24.1.
SELAT is a capability layer for AI agents. It does two things a flat capability index doesn't: it checks vetted skills first, and it pays from the user's own wallet — SELAT never sees a private key or holds the user's funds.
The runner: how to invoke selat
All SELAT actions go through the selat runner (the published @selat-ai/selat-cli,
which bundles the discovery skill and the selat-pay engine). Resolve it in this order:
- Plain
selat— the SessionStart hook puts it on PATH (immediately on hosts that persist hook env; in new shells elsewhere). - If bare
selatdoesn't resolve, use the$SELAT_RUNNERenv var (an absolute path to the shim), where the host exports it. - If neither resolves, SELAT isn't available in this environment — tell the user rather than improvising. Do not hand-roll x402 requests or paste private keys.
Verify with selat doctor (read-only diagnostics: skill, PATH, Circle auth, Agent
Wallet, config).
Setup is the user's, not yours (self-custody)
Payments settle from the user's own Circle Agent Wallet (Circle MPC). SELAT never holds keys or balance. So:
- Free discovery needs no wallet —
selat search,selat skill list, andselat doctorrun with zero setup. Lead with those; show the user what's available first. - Only when the user wants to actually call/pay for a result and
selat doctorreports the wallet/config is missing: ask the user and wait beforeselat init, a Circle OTP, or any spend. Do not auto-runselat init. After they agree, it installs the Circle CLI, then prompts for the user's Circle email + a 6-digit OTP (relay those — the user authenticates; in a chat-only harness the OTP necessarily passes through the chat — keep it out of logs and summaries, and never store it). On a non-interactive host (no usable TTY) or an account with several agent wallets, passselat init --email <addr> --wallet <n|address>so it doesn't block on the email/wallet prompts —--wallettakes a 1-based index from the list init prints, a 0x address, ornew(also settable viaSELAT_WALLET); without it a multi-wallet account stops atWallet to use [1-N/new], which a non-TTY shell can't answer. Don't runselat initbefore it's needed, pre-install Circle CLI, or improvisecirclecommands.selat fundand any paid call still need explicit approval and an armed session budget (selat budget start);selat freezeis the kill switch — never auto-fund or auto-pay. - A
--raw-keydev mode exists but is not for production — do not steer users to it. - Before any spend, surface the cost and get the user's go-ahead. Spending limits are
set via
selat setup-policy(recommended before deposits > $20); funding viaselat fund. Both are user-driven money actions — never run them unprompted. - Funding is two movements: (1) top up the agent wallet address with USDC, then (2)
selat funddeposits it into Gateway. For movement (1) there are two paths: a crypto transfer to the wallet address, or fiat viaselat fund --onramp [--address 0x…](selat-cli 0.15.17+) — it mints a Circle Onramp browser link, chain-scoped so purchases can't strand. The user picks the amount inside the widget — never ask for an amount before launching, and never treat the mint as a spend (nothing moves until the user completes the purchase in their browser; relay the printed URL into chat). To onramp a wallet other than the configured payer, pass--address 0x…— never rewrite the configured wallet to retarget funding. Never usecircle wallet fund --method fiatfor fiat: that opens Transak, a different, unscoped onramp. After funding, refer only to "the Gateway balance" — one number, spendable on any supported chain; never enumerate per-chain balances to the user. Verify it withcircle gateway balance --all, or the Gateway line inselat doctor— never the wallet's on-chain address balance. A deposit moves USDC from the address into Gateway, so the on-chain balance dropping (even to 0) is by design, not lost money. Deposits take ~5–10 min to settle; a fresh 0 Gateway reading usually means "still settling" — don't tell the user funds are lost. Ifcircle wallet balanceand an on-chain read disagree, say "a fresh transfer can take a minute to be readable — let's re-check" — never speculate that Circle "holds" funds off-chain (reads just lag; that speculation manufactures a custodial misconception). Whenselat fundprints a QR image path, send that image to the user directly in chat harnesses — never just relay a file path the user can't open.
The two-tier loop
For any task beyond your native abilities, work the tiers in order:
Tier 1 — vetted skills (preferred)
A SELAT skill is a declarative manifest describing one or more catalogue-endpoint
steps; each step compiles to a single selat-pay call. Skills are vetted (evals +
validation + an authoring SOP) and live in the public repo SELAT-AI/selat-skills.
Every step URL must be https://.
selat skill list # browse installed vetted skills (read-only)
selat skill list --available # browse installable skills (with reliability)
selat skill run <name> [--param value ...] [--max-amount <usd>] [--chain <key>]
# run a vetted multi-step skill (MAY PAY — confirm first)
selat skill run takes the skill's own params as --flags (manifest keys map 1:1).
Three names are reserved overrides applied to every step, not passed as params:
--max-amount <usd> (per-call cost cap), --chain <key> (force a settlement chain),
and --raw-key (dev-only EOA signing — do not steer users to it).
If a vetted skill covers the task, prefer it: it's a known-good, capped workflow. Pass
--max-amount to hold the spend to a number the user approved.
selat skill run <name> --help is a safe no-op as of selat-cli 0.16.2 — it prints
usage and returns without resolving or paying. Before that it was the one paying
subcommand where help was not inert: the flag fell through as a pass-through skill
param and the skill resolved and paid. Inspecting a command is now safe, but only
on 0.16.2+; on an older pinned runner, do not append --help to skill run.
Tier 2 — federated endpoint catalog (fallback)
If no skill fits, fall back to the federated x402 / MPP endpoint catalog (Circle +
MPP + Apify + pay.sh (Solana) + Heurist Mesh + Cambrian + SELAT's own first-party
catalog.selat.ai, merged). The x402 Bazaar was dropped as a source in
selat-discovery 0.21.0, so no result carries sourceCatalog: "bazaar" any more;
Heurist Mesh and Cambrian were added in 0.23.0, so sourceCatalog /
sources[].catalog can now carry "heurist" or "cambrian". Don't hardcode a
catalog count when rendering "N/<total> catalogs" — the source list changes
between releases.
Discover first (free), then pay:
selat search "<intent>" # FREE discovery — rank candidate endpoints, no wallet,
# no spend, no signature. The "search SELAT first" step.
# --top N (default 5) · --json (agents/hooks) · --explain
selat run "<intent>" # discover + rank + pay for an x402 service in one pipe
# (MAY PAY — surface the price and confirm before spend)
selat search is the read-only front-half of selat run (same ranker, no --pick/pay
step). Lead with it to show the user what's available and the price before committing to
a spend; selat run then ranks + pays in one shot once they approve.
Use selat run for one-off capabilities the skill registry doesn't yet cover. It takes
only the intent string — there is no user-facing --max-amount flag here. The cost
cap is applied automatically: selat run invokes the skill's rank.mjs --pick, which
caps the chosen selat-pay call at ~50% over the catalog price and rejects any uncapped
hint. If you need to pin an explicit cap, use a vetted skill with --max-amount (Tier 1)
or call the underlying selat-pay <METHOD> <url> --chain <key> --max-amount <usd>
directly. The discovery/ranking half is read-only; the pay half spends from the user's
wallet — treat the whole command as a spend and confirm first.
Apify Actors — prepaid token (recognize → buy once → reuse the Bearer)
Apify Actors in the catalog use a prepaid API token, not per-call x402.
Recognize an Apify Actor by any of: a URL on host api.apify.com with a
/v2/acts/<owner>~<actor>/… (or /v2/actors/…) run path; a catalog pick whose payment
scheme is prepaid-token or whose service id starts with apify:; or a probe returning a
402 from one of those URLs.
First contact — an Apify Actor's 402 means BUY A TOKEN, not pay it. Probing an Apify
Actor without a token returns an x402 challenge. That 402 is the Actor's own per-call price;
in the prepaid-token model you do not sign or pay it (you can't settle it from a Gateway
balance anyway). Buy one token, then call the Actor with it. Never treat the Actor 402
as a payable quote, never hand-run the raw selat-pay hint against an Actor URL, and never
re-probe the Actor's 402. Always drive Apify Actors with selat run + the Actor input:
selat run "scrape an instagram profile" \
--input '{"directUrls":["https://www.instagram.com/nasa/"],"resultsLimit":1}'
selat run buys one token ($1.05 — $1 of Apify credit + SELAT's 5% fee) through the
Router only if you don't already hold a valid one, then calls the Actor with an
Authorization: Bearer <token> header.
The token is bought once and reused. After the first purchase, every subsequent Actor call is served by that Bearer token and draws down its ~$1 prepaid balance — there is no new payment, no re-probe, and no re-quote until the balance is exhausted or the token expires (14 days).
One token, many calls. The prepaid token is a normal Apify API key — reuse it as
Authorization: Bearer <token> across every Apify endpoint. Store discovery needs no token;
the run, run-status, and dataset endpoints all draw the same prepaid balance:
# Discover Actors (free — no token)
curl -s "https://api.apify.com/v2/store?search=instagram&limit=5"
# Run an Actor and get its dataset items in one call (Bearer)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"username":["natgeo"],"resultsLimit":3}' \
"https://api.apify.com/v2/actors/apify~instagram-post-scraper/run-sync-get-dataset-items"
# Poll a run, then fetch its dataset — same token, no new payment
curl -s -H "Authorization: Bearer $TOKEN" "https://api.apify.com/v2/actor-runs/<runId>"
curl -s -H "Authorization: Bearer $TOKEN" "https://api.apify.com/v2/datasets/<datasetId>/items"
# Check the token's remaining prepaid balance (what `selat spend` wraps)
curl -s -H "Authorization: Bearer $TOKEN" "https://agi.apify.com/prepaid-tokens/balance"
Only pay-per-event Actors are supported; rental and pay-per-usage Actors are not.
Actor input is per-Actor (e.g. {"username":["natgeo"],"resultsLimit":3}) — read its schema. So:
- Confirm the spend with the user on the first purchase only. Later calls within the same token's life are already paid for — just run them; don't re-ask to spend $1.05.
- After a purchase, make the next call with the token, do not probe again. Seeing a fresh "$1.05 quote" for a second call means you're wrongly re-buying — stop and reuse.
- Check the remaining prepaid balance any time with
selat spend.
Guardrails (always)
- Self-custody: never paste, request, or improvise a private key; never create a wallet or move funds on the user's behalf.
- Confirm spends: show the price and get approval before any paying command
(
selat run,selat skill run,selat fund,selat setup-policy). Paid calls also need an armed session budget (selat budget start);selat freezeis the kill switch. - HTTPS only: SELAT enforces
https://payment URLs — don't try to route around it. - Degrade honestly: if the runner or setup is unavailable, say so; don't fabricate results or substitute an unvetted external API.
Command quick reference (selat-cli v0.16.3)
| Command | What it does | Money? |
|---|---|---|
selat doctor |
Diagnose setup (skill, PATH, auth, wallet, config) | no |
selat init |
One-time onboarding: installs Circle CLI, skill, Circle auth, Agent Wallet, selat-pay, config | ask + wait; user does the OTP |
selat skill list |
List/browse vetted skills | no |
selat skill run <name> |
Run a vetted multi-step skill | yes |
selat search "<intent>" |
Discover + rank endpoints for a capability (FREE; no wallet, no spend) | no |
selat run "<intent>" |
Discover + rank + pay for an x402 service | yes |
selat fund |
Top up Circle Gateway balance | yes |
selat fund --onramp [--address 0x…] |
Mint a fiat (card) Circle Onramp browser link — amount picked in the widget, purchase completed by the user there | no spend at mint; the purchase is user-driven |
selat setup-policy |
Set Circle spending limits | no spend, changes policy (user runs it) |
selat history |
Show locally recorded Gateway micropayments | no |
selat spend |
Unified spend report: settled spend + Apify token utilization (read-only) | no |
Flag surface verified against @selat-ai/selat-cli@0.16.3 (
lib/commands/run.mjs,lib/commands/skill.mjs) and @selat-ai/selat-discovery@0.24.1: •selat search "<intent>"(lib/commands/search.mjs) is FREE discovery — the same ranker asselat runin its no---pickmode, so it never settles. Flags:--top N(default 5),--json(for agents/hooks),--explain(why each match is/isn't payable),--refresh. •selat run "<intent>"accepts the intent — no--max-amount; the cap is auto-applied byrank.mjs --pick(~50% over catalog price, uncapped hints rejected). For an Apify pick (prepaid-token model) it also accepts--input '<json>'/--input-file <path>to carry the Actor input. •--endpoint <url>(selat-cli 0.16.0+, needs selat-discovery ≥ 0.22.0) pins the exact endpoint instead of letting the intent choose it, with--method <verb>to disambiguate a URL listed under several verbs. Use it whenever you have already surfaced and priced an endpoint — viaselat searchorselat run --dry-run— and now want to pay that one. Without it the intent is re-ranked at pay time, and catalog services are merged across registries (one service id can span unrelated capabilities), so a reworded intent can resolve to a different endpoint at a different price. The URL must be in the federated catalog: an unlisted URL is refused, never paid (reason: "endpoint-not-in-catalog"), and a pinned endpoint that fails the payment-layer reliability check is refused rather than substituted (reason: "endpoint-unreliable"). Neither charges anything. •selat run … --dry-run --jsonplans without paying and returnsexec—{ runner, cmd, argv, env? }, the resolved spawn tuple the paid run would execute. Spawn it directly (no shell) to run exactly what was quoted;commandbeside it is the shell-quoted human display of the same thing, not something to re-parse. •selat skill run <name>accepts the skill's params as--flagsplus three reserved overrides:--max-amount <usd>,--chain <key>,--raw-key. •--max-amountis also the mandatory cost cap on the underlyingselat-payengine. •selat doctorprints sectioned diagnostics (Binaries · Agent-payment skill · Circle CLI · Agent Wallet · Spending policy (per chain) · selat-pay · Config · Router reachability) and ends with "All checks passed." (exit 0) or "N check(s) failed." (exit 1).