talus protocol · developers
Call Daimon's LLM tool from a Talus Protocol task.
You use your own Sui address; Daimon never holds your keys. You pre-pay USDC into an on-chain escrow ticket, a Talus Protocol task calls tech.daimon.llm@1, the tool settles the real cost from the ticket, and the rest comes back to your Daimon account. Live on Sui mainnet.
Also live: tech.daimon.chat@1, where you set a USDC budget once and sign a short voucher per call instead of minting a ticket: chat@1 guide. Everything else on this page is how to call tech.daimon.llm@1.
Steps
The call, step by step
If the tool or operator never settles, anyone can return the escrow to your account after the ticket expires.
Constants
| Name | Value |
|---|---|
| Daimon package, original id (object types, pre-existing events) | 0x670f3f97acf3e6a9a7e8a090e84e5c76d42d596ecf4e6178483aa1c091da9403 |
| Daimon package call target of this guide (v2 published-at, has daimon::llm). Use for every call target | 0x57eee22c2765db36ebe7ab3b99678de8228959da47363224043fa78746983c1c |
| Config (shared) | 0x2790f87aa70112d1f435e5777d0e5ddab07d4b89361a3d08653f45d5d298ebaa |
| USDC type | 0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC |
| Clock | 0x6 |
| Tool | tech.daimon.llm@1, host https://nexus.daimon.run |
| Tool id (Talus Protocol mainnet registry) | 0x58ce31efb014578daf0d1099f72ad466f38eb7cfac172a4e588850a3704d7100 |
| Published DAG (one vertex, immutable) | 0xd688f096124d578039c307b1fcc1d91a6ec51b838eb23596bff77ae85623a840 |
Types of objects you create (Aion, OwnerCap) carry the original id. The new llm events (TicketMinted, TicketSettled, TicketReclaimed) carry the v2 id; Deposited and AgentCreated keep the original id. A future upgrade changes the call target only. Check the tool is registered before minting:
nexus tool inspect --tool-fqn tech.daimon.llm@1 # "is registered", Compatibility: current
Price and limits
- One call = one committed ticket = one request: identity_id, ticket_seq, model, and either prompt or messages (optional thread_id for provider prompt-cache stickiness).
- A paid OpenRouter slug that escrow serves (/docs/nexus/models). :free slugs are refused.
- 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). A model escrow does not serve is refused before any spend, ticket refunded in full: model_unbounded_reasoning (it may reason without being asked), model_unbounded_fees (it searches the web on its own), model_unpriced (a fee the cap cannot account for).
- Price = actual OpenRouter cost x 1.20, settled from the ticket and clipped to the ticket's max_cost. Whatever is left of the escrow returns to your Daimon account on settle. The Talus Protocol invocation price is 0, but every call also costs a network fee in SUI from your own wallet (about 0.08 SUI, see Network fee in SUI).
- Limits (Config): account balance at most 50 USDC, at most 1 USDC escrow per ticket, at most 5 USDC per UTC day (open escrows count toward the day until settled or refunded).
- Ticket TTL is 24 h. The tool serves a ticket only while at least 6 h of TTL remain, so run the task within 18 h of minting. max_cost is in micro-USDC (1 USDC = 1,000,000).
- Only committed tickets (kind 0) are served; open vouchers (kind 1) are refused by the tool.
- Do not send secrets. Every input and output port is public on chain: identity_id, ticket_seq, model, prompt or messages (including any system message), thread_id, and the result (completion, cost_micro, cached_tokens, or the error reason). The operator also stores the completion for exactly-once replay. Keep nothing private in a system prompt or a thread id.
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. One 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).
| 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. |
| Mint transaction gas | your SUI | A separate small Sui transaction before the task (not part of the 0.075 to 0.09 figure). |
| Create identity, deposit USDC | your SUI | Once per identity, not per call. |
A call that is refused or fails still spends the schedule and close fees. The USDC is refunded; the SUI is not. Check the model and the request before you mint.
Models and prices
/docs/nexus/models lists every model escrow serves 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, or is refused before any spend (cap_too_low, ticket refunded, SUI spent).
Setup
You need the sui CLI (1.81 tested), jq, python3, a funded mainnet address with SUI and USDC, and the nexus CLI (see Run the task). The commands use sui client ptb; append --dry-run to print the effects without signing. A full run (identity, deposit, mint, one task) costs about 0.1 SUI plus the USDC call cost; most of the SUI is the task prepay, and what the task does not use comes back when you close it.
export PKG=0x57eee22c2765db36ebe7ab3b99678de8228959da47363224043fa78746983c1c export CONFIG=0x2790f87aa70112d1f435e5777d0e5ddab07d4b89361a3d08653f45d5d298ebaa export USDC=0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC sui client switch --env mainnet sui client chain-identifier # must print Hex: 35834a8a
If your 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, so the official fullnode works; a JSON-RPC-only provider (for example sui-rpc.publicnode.com) fails with grpc-status header missing.
Create your identity
agent::new creates a shared Aion (your identity) and sends the OwnerCap to you.
sui client ptb --move-call $PKG::agent::new --gas-budget 50000000
Find the ids afterwards (<digest> is printed by the command):
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…
The AgentCreated event carries the same agent_id (= AION). Keep CAP private: it can withdraw your balance. The AION id is public and goes into every call.
Deposit USDC
Anyone can fund an identity; the account is created on the first deposit (account::deposit). List your USDC coin objects and pick one. 5000000 is 5 USDC; a deposit that would push the balance above 50 USDC aborts (106).
sui client balance --with-coins # USDC section: coinId, balance export COIN=0x… # a USDC coin with enough balance sui client ptb \ --split-coins @$COIN "[5000000]" --assign c \ --move-call $PKG::account::deposit "<$USDC>" @$CONFIG @$AION c.0 \ --gas-budget 50000000
sui client tx-block <digest> --json | jq '.events[] | select(.type|endswith("::account::Deposited")) | .parsedJson'Commit to the request
The ticket commits to the exact request, so the tool cannot be made to run anything else on your escrow. Download daimon_llm_commitment.py (standalone, Python 3 standard library only) and run it:
python3 daimon_llm_commitment.py $AION qwen/qwen3-235b-a22b-2507 --prompt "Say hello in five words." # hex python3 daimon_llm_commitment.py $AION qwen/qwen3-235b-a22b-2507 --messages @messages.json # chat history python3 daimon_llm_commitment.py $AION qwen/qwen3-235b-a22b-2507 --prompt "Say hello in five words." --ptb # [b0,b1,…]
messages.json is [{"role":"system","content":"…"},{"role":"user","content":"…"}] (roles system, user, assistant; at least one user; at most 64 messages; no other keys). The model string and the prompt or messages you commit must be byte-identical to what you give the task later. thread_id is not committed.
Preimage: sha256("daimon.llm.ticket.v1" ‖ 0x00 ‖ agent_id(32) ‖ u32le len(model) ‖ model ‖ u32le len(body) ‖ body), where body is the RFC 8785 JSON of {"messages":[…]}. To check your copy of the script, this reference vector must print the commitment on the second line:
python3 daimon_llm_commitment.py 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f z-ai/glm-5.3-flash --prompt hello # 4398dcef4f5a47fa80aa640f26586f303cbcd5f5124ba4106ec2f316dc7d620b
Mint the ticket
llm::mint_ticket<USDC>(config, aion, owner_cap, max_cost, commitment: Option<vector<u8>>, kind: u8, clock): u64. kind is 0u8. max_cost is the most this call may cost (micro-USDC, at most 1,000,000); it is moved out of your account into escrow now.
export MAX_COST=100000 # 0.10 USDC export BYTES=$(python3 daimon_llm_commitment.py $AION qwen/qwen3-235b-a22b-2507 --prompt "Say hello in five words." --ptb) sui client ptb \ --make-move-vec "<u8>" "$BYTES" --assign cm \ --move-call 0x1::option::some "<vector<u8>>" cm --assign opt \ --move-call $PKG::llm::mint_ticket "<$USDC>" @$CONFIG @$AION @$CAP $MAX_COST opt 0u8 @0x6 \ --gas-budget 50000000
The return value (the sequence number) is not printed by the CLI; read it from the TicketMinted event:
sui client tx-block <digest> --json \
| jq '.events[] | select(.type|endswith("::events::TicketMinted")) | .parsedJson'
# {"agent_id":"0x…","seq":"0","max_cost":"100000","kind":0,"commitment":"<base64>","minted_ms":"…","expires_ms":"…"}
export SEQ=0seqis per identity and never reused. Wait a few seconds for Daimon's indexer to see the mint; the tool answers "ticket not found yet" until then.
Run the task
One-time setup of the Talus Protocol CLI, nexus (macOS arm64 binary shown). The rc.final CLI speaks gRPC, so use a gRPC-capable fullnode URL such as the one below. The manifest is the rc.final mainnet one.
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> nexus gas deposit --amount 100000000 --sui-gas-coin <sui coin id> # moves SUI into the address balance used for task prepay
One run used about 0.033 SUI of Talus Protocol fees (gas_fee_mist in the execution events), so 0.1 SUI of prepay is enough.
Daimon already published a one-vertex DAG. A published DAG is an immutable object, so anyone can schedule tasks on it:
export DAG=0xd688f096124d578039c307b1fcc1d91a6ec51b838eb23596bff77ae85623a840
Or publish your own copy (about 0.016 SUI) from daimon-llm-mainnet.json:
nexus dag validate --path daimon-llm-mainnet.json nexus dag publish --path daimon-llm-mainnet.json --sui-gas-coin <sui coin id> export DAG=0x… # the DAG id it prints
Schedule one occurrence. Input is keyed by vertex name; prompt is the same text you committed:
nexus task schedule --dag-id $DAG --entry-group default \
--input-json "{\"llm\":{\"identity_id\":\"$AION\",\"ticket_seq\":$SEQ,\"model\":\"qwen/qwen3-235b-a22b-2507\",\"prompt\":\"Say hello in five words.\"}}" \
--prepay-amount-mist 100000000 --occurrence-budget-mist 100000000 \
--refund-recipient <your address> --now --sui-gas-coin <sui coin id>For chat history use "messages":[…] instead of "prompt" (give exactly one; the DAG defaults the other port to null). Pass --sui-gas-coin on every write, or the CLI also tries the empty address balance for gas.
Read the answer
nexus execution inspect --task-id <task id> --occurrence-id 0 # "Execution finished successfully" nexus task close --task-id <task id> --sui-gas-coin <sui coin id> # refunds unused prepay
The text view only says which variant ended (ok or err). The output ports are in the --json records, hex-encoded:
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)"; doneThe tool's output is one of:
{"ok":{"completion":"…","cost_micro":4321,"cached_tokens":0,"ticket_seq":0}}
{"err":{"reason":"…"}}cost_micro is what was settled from the ticket (markup included); 0 means the call was refunded in full. Confirm on chain: TicketSettled (escrow, actual) appears in the settle transaction a few seconds after the completion, and your account balance is back up by max_cost - actual. Read the balance from the identity's dynamic fields (sui client ptb --dev-inspect did not print return values in 1.81):
curl -s https://graphql.mainnet.sui.io/graphql -H 'content-type: application/json' -d \
'{"query":"{object(address:\"'$AION'\"){dynamicFields{nodes{name{type{repr}} value{... on MoveValue{json}}}}}}"}' \
| jq '.data.object.dynamicFields.nodes[] | .value.json'
# AccountKey<USDC>: {"balance":"…","spent_today":"…",…} LlmKey: {"escrowed_total":"0","open_tickets":"0",…}The refund of unused prepay arrives as a SUI coin object, not in the address balance, so a second task needs a new nexus gas deposit. Scheduling the identical input again returns the stored answer from the operator (no second upstream call, no second charge, the ticket is not touched), which is safe for retries.
Mainnet proof run (2026-10-13). Ticket seq 0 with max_cost 200000 (0.2 USDC): actual 545 micro-USDC settled, 199,455 returned, about 29 s from task schedule to the finished execution.
Refunds, reclaim, withdraw
- Normal case: settle pays actual to the treasury and returns the rest to your account. Nothing to do.
- Upstream error or refusal before spend: settled at 0, full refund; the ticket is consumed. Mint a new one to retry.
- Transient errors (retry in the message): the ticket stays reserved; retry the identical request with the same ticket.
- Never settled (tool or operator outage): after the TTL (expires_ms from the event) anyone can return the whole escrow to your account. It never goes to the caller.
sui client ptb --move-call $PKG::llm::reclaim_expired "<$USDC>" @$AION $SEQ @0x6 --gas-budget 50000000
Before expiry this aborts 207; for an unknown or already-closed seq it aborts 205. Reclaim works while the Config is paused and on a retired identity.
Withdraw your balance (owner only; never touches escrowed funds):
sui client ptb --move-call $PKG::account::withdraw "<$USDC>" @$AION @$CAP 1000000 @<recipient address> --gas-budget 50000000
Read-only helpers: llm::ticket_info<USDC>(&Aion, seq), llm::has_ticket(&Aion, seq), llm::llm_state(&Aion).
Troubleshooting
Tool err.reason strings
| Reason | Meaning / fix |
|---|---|
| request does not match the ticket commitment | Model, prompt or messages differ from what you committed. Recompute with the script from the exact strings. |
| this ticket is already bound to a different request | The ticket was already reserved for another request. Mint a new ticket. |
| ticket not found yet: the mint is not indexed, retry in a few seconds | Mint not indexed yet, or wrong identity_id/ticket_seq. Retry; check the event. |
| ticket already settled / ticket was reclaimed by its owner / ticket is closed | Ticket is spent. Mint a new one. |
| ticket is expired or about to expire; mint a new one | Less than 6 h of TTL left, or expired. Mint a new one (reclaim the old after expiry). |
| open vouchers are not supported by this tool version; mint a committed ticket | You minted kind 1. Use kind 0u8. |
| model must be a paid OpenRouter slug (letters, digits and / - _ . : ~, no :free) | Fix the model string. |
| proxy 422 model_unbounded_reasoning / model_unbounded_fees / model_unpriced: … (nothing charged) | Escrow does not serve that model (it may reason unasked, searches the web on its own, or has a fee the cap cannot bound). Refunded in full; mint a new ticket for a model on /docs/nexus/models. |
| proxy 400 cap_too_low: … (nothing charged) | max_cost cannot cover even one output token of that model. Refunded in full; mint a new ticket with a higher max_cost. |
| proxy 503 escrow_daily_cap / escrow_ledger_unavailable: … (nothing charged) | Daimon's daily escrow spend limit is reached (until 00:00 UTC), or its spend ledger cannot be read. Refunded in full; mint a new ticket later. |
| give either prompt or messages, not both / give prompt or messages / prompt must not be empty | Input shape. |
| messages must hold 1 to 64 entries / message role must be system, user or assistant / messages needs at least one user message | Input shape. |
| request is larger than 200000 bytes | Shorten the request. |
| identity_id must be 0x followed by 1 to 64 hex characters / identity_id is not valid hex | Pass your Daimon identity id. |
| thread_id must be 1-128 characters of letters, digits and _ : . - | Fix or drop thread_id. |
| request_in_flight: the same request is already being served, retry shortly | Another leader holds the same request. Wait and retry. |
| … (nothing charged) / … (charged N micro-USDC for the spend upstream) | Upstream failed. First: refunded in full. Second: the upstream billed that amount; it is settled and the rest refunded. |
| … the ticket stays reserved, retry in a few minutes / … retry | Transient. Retry the identical request. |
| reserve: temporarily unavailable (settle_backlog / settle_unavailable), retry later / reserve: temporarily unavailable, retry later | Operator side: settles are behind or failing, or this identity already holds its share of unsettled calls (about 5 USDC at a time). Nothing was reserved or charged; retry the identical request later. |
| this ticket was already refunded without an answer / … (this ticket was already settled without an answer) | The call ended without a stored answer. Mint a new ticket. |
| the llm tool is not configured on this host / … misconfigured (issuer rejected the service token) | Operator side. Report it. |
Move aborts
Read as MoveAbort(… module: "llm" or "account" …, code) in the failed transaction or dry-run.
| Code | Module | Meaning |
|---|---|---|
| 100 | account | OwnerCap is not for this identity |
| 101 / 201 | account / llm | Config paused (mint and settle are blocked; reclaim is not) |
| 102 | account | Wrong coin type; only the USDC type above is accepted |
| 103 | account | Zero amount (max_cost or deposit is 0) |
| 104 | account | No account yet; deposit first |
| 105 | account | Account bound to a different Config |
| 106 | account | Deposit would exceed the 50 USDC account cap |
| 107 | account | Insufficient balance for the escrow |
| 108 | account | max_cost above the per-ticket limit (1 USDC, or your own lower max_charge) |
| 109 | account | Day limit (5 USDC, or your own lower limit) reached; escrow counts until settled |
| 115 | account | Identity is retired |
| 202 | llm | Bad kind (use 0) |
| 203 | llm | Commitment must be exactly 32 bytes for kind 0 |
| 204 | llm | Open voucher too large (not relevant for kind 0) |
| 205 | llm | Ticket does not exist (wrong seq, or already settled or reclaimed) |
| 207 | llm | reclaim_expired before expires_ms |
| 208 | llm | Ticket belongs to another Config |
Other
- sui client fails with -32601: your client points at a JSON-RPC URL that was switched off; use https://fullnode.mainnet.sui.io:443 (the 1.81 CLI uses gRPC). grpc-status header missing means the URL only speaks JSON-RPC.
- Dry-run any sui client ptb command first by appending --dry-run.
- Stale Talus Protocol manifest or unknown tool: nexus tool inspect --tool-fqn tech.daimon.llm@1 must show the tool as available.