Install
openclaw skills install @dlazyai/asale-sell-codexPut a Codex / ChatGPT subscription on the asale market: import it, set a price floor and a concurrency cap, and see which of its models are actually selling. 把 Codex / ChatGPT 订阅挂到 asale 市场上:导入账号、设定价格底价与并发上限,并查看它的哪些模型真的在卖。
openclaw skills install @dlazyai/asale-sell-codexPut a Codex / ChatGPT subscription on the asale market: import it, set a price floor and a concurrency cap, and see which of its models are actually selling.
Two separate things have to be true.
1. The local daemon must be running, and you need its token. asaled writes
~/.asale/daemon.token (mode 0600) on first run and requires it on every /rpc
call, loopback included.
asale status # is the daemon up, and on which port
asale start # start it if it is not
# Every /rpc call carries the daemon token — loopback included.
TOKEN=$(cat ~/.asale/daemon.token)
call() {
body=$2; [ -n "$body" ] || body='{}'
# --noproxy is not optional: a machine with HTTP_PROXY set sends even
# 127.0.0.1 through it, and the proxy answers 502 with an empty body,
# which reads exactly like a broken daemon.
curl -sS --noproxy 127.0.0.1 -X POST "http://127.0.0.1:9700/rpc/$1" \
-H 'content-type: application/json' -H "x-asale-token: $TOKEN" -d "$body"
}
A connection refused means the daemon is not running — say so and stop. Starting it is the user's call.
~/.asale/asaled.bind holds the port the last asale start used; fall back to
127.0.0.1:9700. A bind on 0.0.0.0 is not a destination — keep the port and
dial loopback.
2. Selling and buying need a signed-in asale account. The daemon mints the
consumer key and registers the seller against that session. There is no CLI
login: open the app (asale open) and sign in there. Without it you get
errors.session.signInToSell / errors.session.signInToBuy, which is not
something to work around.
curl -fsSL https://asale.ai/dl/install.sh | sh (Windows:
irm https://asale.ai/dl/install.ps1 | iex). asale update re-runs the same
installer.~/.asale/ — the token, the SQLite store and the daemon's logs.
$ASALE_DATA_DIR moves all of it.This skill talks to a daemon on your own machine. Nothing here reaches the asale servers directly; the daemon does that, over its own authenticated session. The token file is the only credential this skill reads, and it never leaves the loopback interface.
Selling is per subscription account, not per machine. The daemon holds its own copy of the account's credential (it never reads or writes the CLI's config on this side), declares the account's models to the market as lanes, and serves other people's requests from it — inside the limits you set: a price floor, how many requests at once, and an optional daily token cap.
A lane leaves the market on its own whenever the market price drops under your floor, and comes back when it recovers. Nothing has to be switched off and on again for that.
The credential comes from the macOS keychain entry Codex Auth or from
~/.codex/auth.json. Codex is the one vendor whose token will not be served
without the account id that goes with it (the ChatGPT backend requires a
chatgpt-account-id header), so the import carries that alongside the bearer —
a request missing it is a 401 that looks exactly like a revoked login.
call discovery_scan # what this machine holds — imports nothing
call import_cli_all # import everything found
call list_accounts # what is connected, and on what terms
A Codex subscription is found in the Codex Auth keychain entry or in
~/.codex/auth.json. The plan (chatgpt_plan_type) and the account email come
out of the token's own claims, so the row can name the subscription without any
upstream call.
Pick the row whose provider is codex. Its account_id is what every later call
names it by.
call set_account_sell '{"provider":"codex","accountId":"<id>","enabled":true,"minRatio":10,"concurrency":5,"dailyLimit":0}'
Every field past enabled is optional and keeps its current value when omitted.
dailyLimit is in tokens; 0 means no cap.
minRatio is a whole percentage of the vendor's list price — 60 means "I
will not sell below 60% of list". 100 is list price.
5 is the platform's floor. The market never prices anything below it, so a
floor of 5 never withholds anything.10 is the default a fresh account starts on.There is no "any price" setting: every account trades against a floor. Lowering one is a real decision about money — propose it, do not just do it.
call list_lanes '{"provider":"codex","accountId":"<id>"}'
One row per model: status, paused_reason, requires_user, ratio (what the
market pays now), min_ratio (your floor).
status | Means |
|---|---|
selling | On the market. |
withheld | Below your floor (paused_reason: "price"). Returns on its own. |
cooldown | A recent failure; back at resume_at. |
paused | Needs a person. Read paused_reason, fix it, then resume_lane. |
exhausted | The subscription's own quota window is spent. |
expired | The credential needs re-authenticating. |
call resume_lane '{"provider":"codex","accountId":"<id>","model":"<model>"}'
call set_account_sell '{"provider":"codex","accountId":"<id>","enabled":false}'
CRITICAL INSTRUCTION FOR AGENT:
list_accounts before every set_account_sell. The switch, the floor and
the concurrency are independent, and a value set from a stale reading silently
overwrites what the user chose.minRatio on your own initiative. Propose the number and the
reason, and let the user answer.resume_lane only after the thing paused_reason names is actually fixed.
Clearing a pause that still applies just puts the lane back into it, and each
round costs the seller's reputation.errors.session.signInToSell, stop and say so.| Code | Error Type | Example Message |
|---|---|---|
| — | Daemon not running | curl: (7) Failed to connect to 127.0.0.1 port 9700 |
| 401 | Bad or missing token | {"key":"errors.daemon.unauthorized","message":"unauthorized (missing or bad X-Asale-Token)"} |
| 400 | Not signed in (selling) | {"key":"errors.session.signInToSell","message":"sign in before selling"} |
| 400 | Not signed in (buying) | {"key":"errors.session.signInToBuy","message":"sign in before buying"} |
| 400 | Unknown account | {"message":"unknown account"} |
| 400 | Bad tool id | {"message":"unknown tool: <id>"} |
AGENT CRITICAL INSTRUCTION:
- Errors carry a
keyas well as amessage. Thekeyis a stable translation id — quote it, do not paraphrase the message.- On
errors.session.signInToSell/errors.session.signInToBuy, tell the user to sign in from the app (asale open) and stop. Do not retry, and do not look for another route to the same effect.- On a connection failure, report that the daemon is down and let the user decide whether to start it. Do not run
asale startunasked.
Visit https://asale.ai for more information. The same switches have a UI in the
desktop app and in any browser — asale open opens it, and anything this skill
does can be checked there.