talus protocol · chat@1 · live
Call Daimon's LLM tool from a Talus Protocol task.
Set a USDC budget once, sign a short-lived voucher per call, read the answer. The tool, tech.daimon.chat@1, settles the real cost from your budget; cancel any time and the rest comes back. You use your own Sui address; Daimon never holds your keys.
Status: live on Sui mainnet. The tool was registered on 2026-10-15. The other live tool, tech.daimon.llm@1, is paid per call instead of from a budget: developer guide.
Steps
How it works
The operator can only bill a call you signed, at most its max_cost, once: the chain checks the signature against the allowance's spend key and refuses a voucher id twice.
What is public
- Every input and output port is public on chain: identity_id, auth (the voucher), model, prompt or messages, options (including options.system), thread_id, and the answer. The operator also stores the answer for exactly-once replay. Do not send secrets.
- System prompts and thread ids too. Every input and output port of the tool is public on chain, including any system message (options.system) and the thread_id. Keep nothing private in a system prompt or a thread id.
- The front-run bound. A voucher does not bind the prompt, so a prompt can come from an upstream vertex of your DAG. The price of that: anyone who reads a voucher from a task input can spend it once, for another prompt, for at most its max_cost, until it expires; your own call then gets auth_used. The chain refuses a voucher window above 1 h; the client signs one voucher per call, for that call's worst case, valid for 5 minutes.
- The spend key only signs vouchers: whoever holds it can spend the allowance's budget on calls, but cannot withdraw it, revoke it or send it anywhere else, and a revoke stops it. Your Sui key and the OwnerCap stay where they are.
Setup
| Name | Value |
|---|---|
| Tool | tech.daimon.chat@1, host https://nexus.daimon.run |
| Daimon package, original id (types of the objects you own) | 0x670f3f97acf3e6a9a7e8a090e84e5c76d42d596ecf4e6178483aa1c091da9403 |
| Daimon package call target (v3 published-at, adds daimon::llm_allowance) | 0x2e9f5bfd91a1691d30c74f96061263d616f1ae4d6000b046343890a31cde33ac |
| Config (shared) | 0x2790f87aa70112d1f435e5777d0e5ddab07d4b89361a3d08653f45d5d298ebaa |
| USDC type | 0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC |
| Clock | 0x6 |
| Tool id (Talus Protocol mainnet registry) | 0xe28becf99c2bca438baf8112864e1e024db3bdb8064a80d728b6c1041b8222a7 |
| DAG (one vertex, immutable) | 0x947d002f348f75ddbab81be19e6af798730b170861eef328776200521494bb55; the file: daimon-chat-mainnet.json |
You need the sui CLI (1.81 tested) on mainnet with a funded address (SUI for fees, USDC for the budget) and the nexus CLI (rc.final) configured for mainnet:
gh release download v2.0.0-rc.final -R Talus-Network/nexus-sdk -p "nexus-cli-2.0.0-rc.final-aarch64-apple-darwin.tar.gz" curl -O https://storage.googleapis.com/production-talus-sui-objects/v2.0.0-rc.final/objects.mainnet.toml nexus conf set --sui.rpc-url https://fullnode.mainnet.sui.io --nexus.objects ./objects.mainnet.toml --sui.pk <base64 key> sui client switch --env mainnet sui client chain-identifier # must print Hex: 35834a8a nexus tool inspect --tool-fqn tech.daimon.chat@1 # "is registered"
If your sui client has no mainnet env yet:
sui client new-env --alias mainnet --rpc https://fullnode.mainnet.sui.io:443
The 1.81 CLI talks gRPC to that URL; a JSON-RPC-only provider fails with grpc-status header missing.
The client
packages/nexus-llm in the Daimon repo (TypeScript, runs with bun, no npm package yet) does all of this. It shells out to the sui and nexus CLIs, so your Sui key never enters it; it makes the spend key itself and keeps it in ~/.config/nexus-llm/spend (file 0600), never printed or sent.
alias nexus-llm="bun packages/nexus-llm/src/cli.ts" nexus-llm init --budget 5 # once: identity (if new), deposit, a 5 USDC allowance for 30 days nexus-llm ask --prompt "Say hello in five words." nexus-llm ask --model qwen/qwen3-235b-a22b-2507 --system "Answer in French." --prompt "Hello?" --thread chat-1 nexus-llm ask --prompt "…" --dry-run # every check, the task it would run; signs and sends nothing nexus-llm balance # account, allowances (left, cap, expiry, status), SUI nexus-llm voucher --max-cost 2000 # the identity_id and auth ports for a DAG of your own
- Before it signs anything it checks the model against the model list, the request shape and sizes, the allowance (open, its spend key, per-call cap, budget left), this machine's clock against the chain's, your balances and the chain id. A typo cannot cost you the SUI fee. Every command that signs or writes takes --dry-run.
- Each voucher's max_cost is the call's worst case (nexus-llm estimate), its id is fresh and never reused (saved before the signature exists), and it expires after 5 minutes.
- One task per call, closed straight away so the unused prepay comes back.
Price and limits
- Price = actual OpenRouter cost x 1.20, at least 0.005 USDC per call, settled from the allowance, at most the voucher's max_cost (a voucher below 0.005 USDC is refused). Nothing per voucher, nothing for the Talus Protocol invocation; its network fee in SUI is separate (below).
- Allowance: budget at most 50 USDC (an account holds at most 50 USDC), at most 1 USDC per call, lifetime at most 30 days. No per-day limit on calls. The account's daily limit (5 USDC on mainnet today, shared with the account's other Daimon payments) counts a budget once, on the day it is created, so a budget is at most what is left of it (abort 109). One spend key per allowance; more keys, more allowances.
- Voucher: single use, max_costat most the per-call cap, valid for at most 1 h (the client: 5 minutes, the call's worst case). It may not outlive its allowance.
- At most 1000 output tokens per call, 2000 for the models with a measured throughput floor (deepseek/deepseek-v4.1-flash, deepseek/deepseek-v4.1-flash-1017). Any paid OpenRouter slug; :free slugs are refused. Amounts are micro-USDC (1 USDC = 1,000,000).
- Sizes: one input port holds at most 61,440 bytes of JSON (larger inputs need Walrus-backed storage); options at most 16 KiB; at most 64 messages.
Network fee in SUI
Besides the USDC price, every call costs SUI, paid from the wallet that schedules the task. It goes to the Talus Protocol network and Sui validators, not to Daimon. A one-vertex run measured about 0.075 to 0.09 SUI net on mainnet (budget 0.08 SUI per call; it varies with the Sui gas price). The voucher is signed locally, so a call needs no transaction of its own besides scheduling and closing the task.
| Part | Paid by | Notes |
|---|---|---|
| Schedule transaction gas + Talus Protocol execution fees | your SUI | The task prepay you lock is 0.1 SUI; the unused part returns on close. |
| Close transaction gas | your SUI | Releases the task and refunds the unused prepay. |
| Identity, deposit + allowance, revoke, finish | your SUI | Once per budget, not per call. |
A call that is refused or fails still spends the schedule and close fees; the USDC is not charged unless the answer says so. Check everything before you schedule; the client does.
Models and prices
/docs/nexus/models lists every served model with its price per million tokens (OpenRouter + 20%), the output cap and what is excluded. The same data is a download: models.json, generated from the proxy's own rules.
Size max_cost for the worst case so the whole answer fits: ceil((prompt_bound x max(input, cache_write, 2 x input) + request_fee + T x max(output, reasoning)) x 1.2), with prompt_bound = request bytes + 64 + 32 + 16 x messages and T = min(1000, or the larger ceiling of a measured model above, the model's output limit). A lower cap cuts the answer short; the client refuses one (cap_too_low) unless you insist.
Ports and answers
| Input port | JSON | Notes |
|---|---|---|
| identity_id | string | Your Daimon identity id. |
| auth | object | {"kind":"voucher","allowance_id":0, "voucher":{"id":1792000000000000,"max_cost":2000,"issued_ms":1792000000000,"expiry_ms":1792000300000,"sig":"<128 hex>"}} |
| model | string | null | OpenRouter slug; null = the default, qwen/qwen3-235b-a22b-2507. |
| prompt | string | null | Exactly one of prompt and messages; can come from an upstream vertex. |
| messages | array | null | [{role, content}], roles system, user, assistant, at least one user. |
| thread_id | string | null | 1 to 128 of letters, digits and _ : . - ; sticky routing and prompt cache. |
| options | object | null | system (with prompt), max_tokens, temperature, top_p, stop, seed, response_format. |
The DAG feeds all seven from the task input, so send every one, null where unused. The answer is one of five variants; ok is the only answer:
{"ok":{"completion":"…","content":null,
"usage":{"cost_micro":545,"currency":"USDC","chain":"sui","prompt_tokens":9,"completion_tokens":7,
"cached_tokens":0,"finish_reason":"stop","model":"qwen/qwen3-235b-a22b-2507","provider":"…","latency_ms":2100},
"receipt":{"kind":"voucher","allowance_id":0,"voucher_id":1792000000000000},
"identity_id":"0x…","thread_id":null,"auth":{…the auth you sent…}}}
{"err":{"code":"auth_expired","message":"…","retryable":false,
"state":{"charged":false,"cost_micro":0,"phase":"validate"},"identity_id":"0x…","auth":{…}}}- receipt names the voucher that paid; usage.cost_micro is the settled price (0 = nothing charged), and the VoucherSettled event confirms it on chain a little later.
- pending (a job that outlives the call), tool_calls (only with options.tools) and list (an answer as items for a for_each) are reserved and never sent today. Treat any variant you do not know as not an answer.
Errors and the one retry
Every failure is err with a code, a retryable flag and a state that says whether money moved (charged, cost_micro, phase). The message is for people and may change.
A voucher pays for one charge, not one attempt. When an answer is retryable with charged: false, the same voucher works for exactly one more attempt, for any request. The client retries once with the same voucher (a new task, the same input), which also closes that window for anyone who copied the voucher. A charged failure, a second failure and a timeout are final.
| code | retryable | Meaning / fix |
|---|---|---|
| bad_input | no | The request shape: prompt or messages, roles, thread_id, an unknown option. Fix it. |
| auth_kind_unsupported | no | Only auth.kind "voucher" is served. |
| auth_invalid | no | Bad signature or shape, a window above 1 h, issued in the future, an ext that is not empty, or the voucher outlives the allowance. |
| auth_expired | no | The voucher expired. Sign a new one. |
| auth_used | no | The voucher was already spent, perhaps by someone who copied it from a task input. state.charged says whether that charge stands. Sign a new one. |
| allowance_missing | yes | The allowance is not indexed yet (a few seconds after create_allowance), or the id is wrong. |
| allowance_closed | no | The allowance is being revoked, closed or expired. |
| budget_exceeded | no | max_cost is above the allowance's per-call cap, or the budget left is below max_cost. |
| model_refused | no | A :free or malformed slug, or a model outside the allowance's allow-list. |
| model_unavailable | varies | The model cannot serve now. |
| option_unsupported | no | An option the tool names but does not serve yet (tools, tool_choice, cache, provider, private, reasoning, job). |
| upstream_error / upstream_timeout | varies | The model provider failed. The message says what was charged. |
| issuer_unavailable / rate_limited | yes | Operator side, or more than 5 calls of one identity in flight. Retry shortly. |
| grants_disabled | no | A grant-paid call (auth kind grant) on a deployment with grants switched off; nothing was reserved. Pay with a voucher. |
| internal | varies | Operator side. Report it with the task id. |
Cancel and get the rest back
nexus-llm revoke # no new calls from now on; prints when finish may run nexus-llm finish # 24 h later: what is left goes back to your account nexus-llm withdraw # from the account to your address
- Revoke stops new calls at once. Calls already served still settle during a 24 h grace, so revoking cannot walk away from an answer you already got.
- Finish returns what is left to your Daimon account once the grace is over (24 h after the revoke, or 24 h after the allowance expired). Nothing runs it for you; anyone may, and the money always goes to your account.
- Withdraw moves it from the account to your address (owner only).
Appendix: by hand
Everything the client does, with the CLIs only. Append --dry-run to any sui client ptb to see its effects without signing.
1. Identity
export PKG=0x2e9f5bfd91a1691d30c74f96061263d616f1ae4d6000b046343890a31cde33ac # v3 published-at: the call target export CONFIG=0x2790f87aa70112d1f435e5777d0e5ddab07d4b89361a3d08653f45d5d298ebaa export USDC=0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC export DAG=0x947d002f348f75ddbab81be19e6af798730b170861eef328776200521494bb55
sui client ptb --move-call $PKG::agent::new --gas-budget 50000000 sui client tx-block <digest> --json | jq -r '.objectChanges[] | select(.type=="created") | "\(.objectType) \(.objectId)"' # 0x670f…9403::agent::Aion <- AION # 0x670f…9403::agent::OwnerCap <- CAP export AION=0x… CAP=0x…
Keep CAP private: it can withdraw and revoke. The identity id is public and goes into every call.
2. Spend key
Make a fresh ed25519 key with any library and keep its seed private. Refuse a small-order public key: the tool verifies strictly, so an allowance bound to one could never be spent (a key from a random seed never is).
3. Deposit and create the allowance (one transaction)
export COIN=0x… # a USDC coin with at least the deposit (sui client balance --with-coins)
export SPEND_PK="[121,181,86,…]" # your 32-byte spend public key as a byte list
export EXPIRES_MS=… # at most now + 30 days, in ms
sui client ptb \
--split-coins @$COIN "[5000000]" --assign c \
--move-call $PKG::account::deposit "<$USDC>" @$CONFIG @$AION c.0 \
--make-move-vec "<u8>" "$SPEND_PK" --assign pk \
--move-call 0x1::option::none "<vector<u8>>" --assign allow \
--move-call $PKG::llm_allowance::create_allowance "<$USDC>" @$CONFIG @$AION @$CAP 5000000 1000000 $EXPIRES_MS pk none allow @0x6 \
--gas-budget 50000000
sui client tx-block <digest> --json | jq '.events[] | select(.type|endswith("::events::AllowanceCreated")) | .parsedJson'
export ALLOWANCE=0 # its "id"llm_allowance::create_allowance<USDC>(config, aion, owner_cap, budget, max_per_call, expires_ms, spend_pk, spender: Option<ID>, model_allow_hash: Option<vector<u8>>, clock): u64. spender is none for a voucher-paid allowance. The CLI does not print the return value; read the id from the AllowanceCreated event.
4. Sign a voucher
msg = "daimon.llm.voucher.v1" 21 ASCII bytes
‖ identity id 32 bytes
‖ u64le allowance_id ‖ u64le voucher_id ‖ u64le max_cost
‖ u64le issued_ms ‖ u64le expiry_ms 5 x 8 bytes
‖ bcs(ext) 0x00 (ext is empty) = 94 bytes
sig = ed25519(spend key, msg) 64 bytes, 128 hex in the auth portIntegers are little-endian; the identity id is left-padded to 32 bytes. Check your signer against this reference vector before you use it:
seed 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20 public key 79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664 identity 0xabababababababababababababababababababababababababababababababab allowance 1, voucher 7, max_cost 500000, issued_ms 1000000, expiry_ms 1900000 message 6461696d6f6e2e6c6c6d2e766f75636865722e7631abababababababababababababababababababababababababababababababab0100000000000000070000000000000020a107000000000040420f0000000000e0fd1c000000000000 signature b549aa00b8c1e8c20525057baeeb93c6c93b2b360e2aca699727e676b090a13fcd677168143e0e660b7fe8cb58bb73275291d3ae9fa9ccbc53581d7d9fbddf00
Use a voucher id you never used for this allowance (ids are burnt for good once settled; keep them below 2^53 so JSON stays exact) and leave ext out.
5. Run the task and read the answer
export AUTH='{"kind":"voucher","allowance_id":0,"voucher":{…}}' # signed for THIS call
nexus task schedule --dag-id $DAG --entry-group default \
--input-json '{"chat":{"identity_id":"'$AION'","auth":'"$AUTH"',"model":"qwen/qwen3-235b-a22b-2507",
"prompt":"Say hello in five words.","messages":null,"thread_id":null,"options":null}}' \
--prepay-amount-mist 100000000 --occurrence-budget-mist 100000000 \
--refund-recipient <your address> --now --sui-gas-coin <sui coin id>For chat history, send messages in the one-value form (a bare JSON list would be read as several values):
"messages":{"one":{"kind":"data","data":[{"role":"system","content":"…"},{"role":"user","content":"…"}]}},"prompt":nullnexus execution inspect --task-id <task id> --occurrence-id 0 --json \
| jq -r '.[] | select(.event_type=="EndStateReachedEvent") | .event.variant_ports_to_data.contents[]
| "\(.key.name) \(.value.One.value.InlineData_hex)"' \
| while read k h; do echo "$k = $(printf %s "$h" | xxd -r -p)"; done
nexus task close --task-id <task id> --sui-gas-coin <sui coin id> # refunds the unused prepayOne run used about 0.033 SUI of Talus Protocol fees, so 0.1 SUI of prepay is enough. The refund of unused prepay arrives as a SUI coin object, so a second task needs a new nexus gas deposit.
6. Balance, cancel, withdraw
curl -s https://graphql.mainnet.sui.io/graphql -H 'content-type: application/json' -d \
'{"query":"{object(address:\"'$AION'\"){dynamicFields(first:50){pageInfo{hasNextPage endCursor} nodes{name{type{repr} json} value{... on MoveValue{json}}}}}}"}'
# AllowanceKey {"id":"0"}: {"escrow":"…","budget":"…","max_per_call":"…","expires_ms":"…","closing_since_ms":null,…}
# one UsedVoucherKey per settled call: page with after:"<endCursor>" until hasNextPage is falsesui client ptb --move-call $PKG::llm_allowance::revoke_allowance "<$USDC>" @$AION @$CAP $ALLOWANCE @0x6 --gas-budget 50000000 # 24 h later; anyone may run it, the money goes to YOUR account, never to the caller: sui client ptb --move-call $PKG::llm_allowance::finish_revoke "<$USDC>" @$AION $ALLOWANCE @0x6 --gas-budget 50000000 sui client ptb --move-call $PKG::account::withdraw "<$USDC>" @$AION @$CAP <micro-USDC> @<recipient> --gas-budget 50000000
Move aborts
Read as MoveAbort(… module: "llm_allowance", "account" or "agent" …, code) in the failed transaction or dry-run.
| Code | Module | Meaning |
|---|---|---|
| 301 | llm_allowance | Config paused (create and settle are blocked; revoke and finish are not) |
| 302 | llm_allowance | Spend key is not 0 or 32 bytes, or is a weak (small-order) key |
| 303 | llm_allowance | Model allow-list hash is not 32 bytes |
| 304 | llm_allowance | Budget is 0 or above 50 USDC |
| 305 | llm_allowance | Per-call cap is 0 or above the 10 USDC hard ceiling |
| 306 | llm_allowance | Expiry not in (now, now + 30 days] |
| 307 | llm_allowance | No such allowance |
| 308 | llm_allowance | Allowance under another Config |
| 309 | llm_allowance | max_cost above the allowance's per-call cap |
| 310 | llm_allowance | Settled amount above max_cost |
| 311 | llm_allowance | Voucher window empty or above 1 h |
| 312 | llm_allowance | Voucher outlives its allowance |
| 313 | llm_allowance | Past the 24 h settle window |
| 314 | llm_allowance | Allowance released (its grace is over) |
| 315 | llm_allowance | Bad voucher signature |
| 316 | llm_allowance | Voucher already used |
| 317 | llm_allowance | Settled amount above the budget left |
| 318 | llm_allowance | Already revoking |
| 319 | llm_allowance | finish_revoke before the release time |
| 320 | llm_allowance | No such used-voucher marker |
| 321 | llm_allowance | Marker not reapable yet |
| 322 | llm_allowance | ext above 256 bytes |
| 323 | llm_allowance | Grant already reserved (nonce taken) |
| 324 | llm_allowance | No such grant |
| 325 | llm_allowance | Grant already settled |
| 326 | llm_allowance | Grant at or past its settle deadline |
| 327 | llm_allowance | Grant not reapable yet |
| 328 | llm_allowance | Grant operation not 1 to 32 bytes |
| 329 | llm_allowance | Not this allowance's spender |
| 330 | llm_allowance | Grant nonce not 1 to 64 bytes |
| 331 | llm_allowance | Input hash neither empty nor 32 bytes |
| 332 | llm_allowance | Provider mismatch |
| 333 | llm_allowance | Not exactly one kind: a spend key and a spender, or neither |
| 334 | llm_allowance | Allowance is revoking |
| 335 | llm_allowance | Grant ext above 256 bytes |
| 336 | llm_allowance | Settle memo above 256 bytes |
| 337 | llm_allowance | Wrong kind: a voucher on a grant-paid allowance, or a grant on a voucher-paid one |
| 338 | llm_allowance | Per-call cap above the account's per-charge cap (1 USDC on mainnet today) |
| 1 | agent | The OwnerCap is not this identity's |
| 102 | account | Wrong coin type; only the USDC type above |
| 103 | account | Zero deposit |
| 104 | account | No account yet (deposit first, or in the same transaction) |
| 105 | account | Account bound to another Config |
| 106 | account | The deposit would take the account above 50 USDC |
| 107 | account | Budget above the account balance |
| 109 | account | Budget above what is left of the account's day cap |
| 115 | account | Identity is retired |
Other
- sui client fails with -32601: the client points at a JSON-RPC URL that was switched off; use https://fullnode.mainnet.sui.io:443.
- Unknown tool or stale manifest: nexus tool inspect --tool-fqn tech.daimon.chat@1 must show it as registered.