Daimon

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

01set a budget
suiUSDC → allowance
Once: deposit USDC and move a budget into an allowance bound to a spend key that never leaves your machine.
02sign a voucher
localno transaction
Per call: sign identity, allowance, a fresh voucher id, max_cost and a short validity window with the spend key.
03run the task
talus protocolSUI fees
Schedule the one-vertex DAG with the voucher and the prompt. The tool calls the model through OpenRouter.
04settle
suiactual cost
The real cost (x 1.20, at least 0.005 USDC, at most max_cost) is paid from the allowance; the chain verifies your signature and burns the voucher id.
05cancel
suithe rest back
Revoke stops new calls at once; 24 h later finish returns what is left to your account, and withdraw moves it to you.

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

NameValue
Tooltech.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 type0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC
Clock0x6
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.

PartPaid byNotes
Schedule transaction gas + Talus Protocol execution feesyour SUIThe task prepay you lock is 0.1 SUI; the unused part returns on close.
Close transaction gasyour SUIReleases the task and refunds the unused prepay.
Identity, deposit + allowance, revoke, finishyour SUIOnce 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 portJSONNotes
identity_idstringYour Daimon identity id.
authobject{"kind":"voucher","allowance_id":0, "voucher":{"id":1792000000000000,"max_cost":2000,"issued_ms":1792000000000,"expiry_ms":1792000300000,"sig":"<128 hex>"}}
modelstring | nullOpenRouter slug; null = the default, qwen/qwen3-235b-a22b-2507.
promptstring | nullExactly one of prompt and messages; can come from an upstream vertex.
messagesarray | null[{role, content}], roles system, user, assistant, at least one user.
thread_idstring | null1 to 128 of letters, digits and _ : . - ; sticky routing and prompt cache.
optionsobject | nullsystem (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.

coderetryableMeaning / fix
bad_inputnoThe request shape: prompt or messages, roles, thread_id, an unknown option. Fix it.
auth_kind_unsupportednoOnly auth.kind "voucher" is served.
auth_invalidnoBad 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_expirednoThe voucher expired. Sign a new one.
auth_usednoThe 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_missingyesThe allowance is not indexed yet (a few seconds after create_allowance), or the id is wrong.
allowance_closednoThe allowance is being revoked, closed or expired.
budget_exceedednomax_cost is above the allowance's per-call cap, or the budget left is below max_cost.
model_refusednoA :free or malformed slug, or a model outside the allowance's allow-list.
model_unavailablevariesThe model cannot serve now.
option_unsupportednoAn option the tool names but does not serve yet (tools, tool_choice, cache, provider, private, reasoning, job).
upstream_error / upstream_timeoutvariesThe model provider failed. The message says what was charged.
issuer_unavailable / rate_limitedyesOperator side, or more than 5 calls of one identity in flight. Retry shortly.
grants_disablednoA grant-paid call (auth kind grant) on a deployment with grants switched off; nothing was reserved. Pay with a voucher.
internalvariesOperator 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 port

Integers 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":null
nexus 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 prepay

One 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 false
sui 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.

CodeModuleMeaning
301llm_allowanceConfig paused (create and settle are blocked; revoke and finish are not)
302llm_allowanceSpend key is not 0 or 32 bytes, or is a weak (small-order) key
303llm_allowanceModel allow-list hash is not 32 bytes
304llm_allowanceBudget is 0 or above 50 USDC
305llm_allowancePer-call cap is 0 or above the 10 USDC hard ceiling
306llm_allowanceExpiry not in (now, now + 30 days]
307llm_allowanceNo such allowance
308llm_allowanceAllowance under another Config
309llm_allowancemax_cost above the allowance's per-call cap
310llm_allowanceSettled amount above max_cost
311llm_allowanceVoucher window empty or above 1 h
312llm_allowanceVoucher outlives its allowance
313llm_allowancePast the 24 h settle window
314llm_allowanceAllowance released (its grace is over)
315llm_allowanceBad voucher signature
316llm_allowanceVoucher already used
317llm_allowanceSettled amount above the budget left
318llm_allowanceAlready revoking
319llm_allowancefinish_revoke before the release time
320llm_allowanceNo such used-voucher marker
321llm_allowanceMarker not reapable yet
322llm_allowanceext above 256 bytes
323llm_allowanceGrant already reserved (nonce taken)
324llm_allowanceNo such grant
325llm_allowanceGrant already settled
326llm_allowanceGrant at or past its settle deadline
327llm_allowanceGrant not reapable yet
328llm_allowanceGrant operation not 1 to 32 bytes
329llm_allowanceNot this allowance's spender
330llm_allowanceGrant nonce not 1 to 64 bytes
331llm_allowanceInput hash neither empty nor 32 bytes
332llm_allowanceProvider mismatch
333llm_allowanceNot exactly one kind: a spend key and a spender, or neither
334llm_allowanceAllowance is revoking
335llm_allowanceGrant ext above 256 bytes
336llm_allowanceSettle memo above 256 bytes
337llm_allowanceWrong kind: a voucher on a grant-paid allowance, or a grant on a voucher-paid one
338llm_allowancePer-call cap above the account's per-charge cap (1 USDC on mainnet today)
1agentThe OwnerCap is not this identity's
102accountWrong coin type; only the USDC type above
103accountZero deposit
104accountNo account yet (deposit first, or in the same transaction)
105accountAccount bound to another Config
106accountThe deposit would take the account above 50 USDC
107accountBudget above the account balance
109accountBudget above what is left of the account's day cap
115accountIdentity 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.