Daimon

talus protocol · move

Build on Daimon from Move.

Set up a Daimon identity and its OwnerCap on Sui mainnet, fund it, create allowances, see who can charge it, and call an LLM from a Talus Protocol DAG. Every Move snippet here compiles against the live package, and each has the equivalent sui client ptb command.

Pick your path

You want toReadDaimon's approval needed?
Pay for LLM calls from a DAG with a Daimon identity's USDCCall an LLM from a DAGNo, and no Move code of your own
Own and manage an identity from your own Move packageCreate an identityNo
Have your own Move module charge an identityWho can charge an identityYes: the admin must register your witness

A note on names: this guide says Daimon identity. On chain its Move type is agent::Aion, a name fixed by the deployed package, so Aion appears only where code needs the type.

Steps

Constants and dependency

NameValue (Sui mainnet, chain id 35834a8a)
Package, call target (v3 published-at)0x2e9f5bfd91a1691d30c74f96061263d616f1ae4d6000b046343890a31cde33ac
Package, original id (types of Aion and OwnerCap)0x670f3f97acf3e6a9a7e8a090e84e5c76d42d596ecf4e6178483aa1c091da9403
Config (shared)0x2790f87aa70112d1f435e5777d0e5ddab07d4b89361a3d08653f45d5d298ebaa
USDC type0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC
Clock0x6

Call functions at the latest published-at. Types from before an upgrade keep the original id (Aion, OwnerCap); types added by the v3 upgrade (llm_allowance::Allowance, AllowanceCreated) carry the v3 id. Match events and objects by module and name, not by package id, and re-read Published.toml before relying on the call target in code that outlives a Daimon upgrade.

export PKG=0x2e9f5bfd91a1691d30c74f96061263d616f1ae4d6000b046343890a31cde33ac
export CONFIG=0x2790f87aa70112d1f435e5777d0e5ddab07d4b89361a3d08653f45d5d298ebaa
export USDC=0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC
sui client switch --env mainnet && sui client chain-identifier      # must print 35834a8a

Download the source, unzip it next to your package and add it to your Move.toml. Its Published.toml names the live mainnet ids, so your calls resolve to the upgraded package. The zip also holds daimon_integration, a package with every snippet on this page.

[dependencies]
daimon = { local = "../daimon-move/daimon" }

Live limits, read from Config on 2026-11-05: an account holds at most 50 USDC, one charge at most 1 USDC, 5 USDC per UTC day. They are set there, not in the package code, so read them from the chain before relying on them.

What the objects are

  • agent::Aion is the Daimon identity and its account. It is a shared object. Its USDC lives in a dynamic field (account::Account<T>), as do its allowances, grants and approved chargers. Its id is public and goes into every call.
  • agent::OwnerCap is a key + store capability for one identity. Whoever holds it can withdraw, set policy, create and revoke allowances and approve chargers. Keep it private. A wrapper that stores it is your decision; nothing in the package forbids it.
  • account::Config is one shared object for the deployment: treasury address, pinned coin type (USDC), pause flag, ceilings, and the registry of admitted charger witnesses.
  • You will not hold AdminCap (registers chargers, pauses, sets ceilings) or IssuerCap (settles allowances and grants, binds issuer keys). Both are Daimon's.

Create an identity and its OwnerCap

agent::new(ctx: &mut TxContext) shares the new identity and sends the OwnerCap to the sender. It returns nothing, so you cannot get the cap inside the same PTB, only afterwards.

public fun create_identity(ctx: &mut TxContext) { agent::new(ctx); }
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      -> IDENTITY
# 0x670f…9403::agent::OwnerCap  -> CAP
export IDENTITY=0x…  CAP=0x…

Deposit USDC

account::deposit<T>(config, identity, coin, ctx). Anyone can deposit into any identity; the account is created on the first deposit. Aborts: 101 paused, 102 wrong coin type, 103 zero, 106 above the account ceiling. Read the balance with account::account_balance<T>, which returns 0 if there is no account.

public fun fund<T>(config: &Config, identity: &mut Aion, coin: Coin<T>, ctx: &mut TxContext) {
    account::deposit<T>(config, identity, coin, ctx);
}
sui client balance --with-coins                          # pick a USDC coin
sui client ptb \
  --split-coins @<usdc coin> "[5000000]" --assign c \
  --move-call $PKG::account::deposit "<$USDC>" @$CONFIG @$IDENTITY c.0 \
  --gas-budget 50000000

Allowances

llm_allowance::create_allowance<T> moves budget out of the account into a numbered escrow and returns its number. It needs the OwnerCap, and aborts if the identity is retired or Config is paused.

create_allowance<T>(config, identity, cap, budget, max_per_call, expires_ms,
                    spend_pk: vector<u8>, spender: Option<ID>, model_allow_hash: Option<vector<u8>>,
                    clock, ctx): u64

An allowance is paid exactly one way: grant-paid (empty spend_pk, spender = some(id)) or voucher-paid (32-byte spend_pk, spender = none); anything else aborts 333. Limits: 0 < budget <= 50 USDC (304), 0 < max_per_call <= 10 USDC and at most the account's per-charge cap (305, 338), now < expires_ms <= now + 30 days(306). The account's day cap counts the budget once on the creation day (109), and small-order spend keys are refused (302). The event AllowanceCreated carries id (the number) and allowance (the object id); a grant-paid flow names the object id.

Grant-paid

spenderis the identity a registered witness must name when it reserves from this allowance. For Daimon's own tools it is your Talus Protocol Agent object id.

public fun create_grant_allowance<T>(config: &Config, identity: &mut Aion, cap: &OwnerCap, budget: u64,
    max_per_call: u64, expires_ms: u64, spender: ID, clock: &Clock, ctx: &mut TxContext): u64 {
    llm_allowance::create_allowance<T>(config, identity, cap, budget, max_per_call, expires_ms,
        vector[], option::some(spender), option::none(), clock, ctx)
}
sui client ptb \
  --make-move-vec "<u8>" "[]" --assign spend_pk \
  --move-call $PKG::llm_allowance::create_allowance "<$USDC>" @$CONFIG @$IDENTITY @$CAP \
    1000000 200000 <expires_ms> spend_pk "some(@$SPENDER)" none @0x6 \
  --gas-budget 50000000
# events: AllowanceCreated.id -> ALLOWANCE (u64), .allowance -> ALLOWANCE_OBJ (ID)

Voucher-paid

The owner signs a short-lived voucher off chain per call, and Daimon's issuer settles it. The key must be a real ed25519 key. Message format and signing are in the chat tool guide.

public fun create_voucher_allowance<T>(config: &Config, identity: &mut Aion, cap: &OwnerCap, budget: u64,
    max_per_call: u64, expires_ms: u64, spend_pk: vector<u8>, clock: &Clock, ctx: &mut TxContext): u64 {
    llm_allowance::create_allowance<T>(config, identity, cap, budget, max_per_call, expires_ms,
        spend_pk, option::none(), option::none(), clock, ctx)
}
export SPEND_PK="[121,181,86,…]"          # 32 bytes
sui client ptb \
  --make-move-vec "<u8>" "$SPEND_PK" --assign pk \
  --move-call $PKG::llm_allowance::create_allowance "<$USDC>" @$CONFIG @$IDENTITY @$CAP \
    5000000 1000000 <expires_ms> pk none none @0x6 \
  --gas-budget 50000000

Reads: allowance_info<T>, has_allowance, allowance_book, voucher_used, grant_info<T>, has_grant.

Who can charge an identity

Money leaves an account or an allowance only through these paths, and every one pays Config.treasury, never a caller-supplied address. There is no payee argument: an integration is a way for Daimon to bill a call the owner agreed to, not a way to move an identity's USDC to you.

PathNeedsWho can run it
account::charge<T>IssuerCapDaimon's hot key, bounded by per-charge and per-day caps, a monotonic seq and pause
llm_allowance::settle_voucher<T>IssuerCap and the owner's ed25519 signature on the voucherDaimon's hot key
llm_allowance::settle_grant<T>IssuerCap and a reserved grantDaimon's hot key
account::charge_authorized<T, W>Witness W registered by the AdminCap and approved by this identity's owner, and an issuer-bound keyA module that can construct W
llm_allowance::reserve_grant<T, W>Witness W registered by the AdminCap, and an open grant-paid allowance whose spender is the one you passA module that can construct W
  • account::register_charger<W: drop> admits a witness type, keyed by its defining id so it survives compatible upgrades. It is admin-only, and refuses types anyone can construct (abort 122).
  • account::approve_charger<W> is the owner's per-identity consent for charge_authorized. remove_charger<W> withdraws it and works even while paused or retired. Consent is rechecked on every call.
  • reserve_granthas no per-identity approval: the owner's consent is the allowance itself (budget, per-call cap, expiry, named spender). The grant is then settled once by the IssuerCap to the treasury, and unspent escrow stays in the allowance.
  • The witness value only proves that your module decided to charge. The chain checks the type, not your reasons, so keep W {} unconstructible from outside and gate each use.

To get a charger admitted, send Daimon the package id, module and witness type name; the admin reviews the source and registers it from the cold key. The example's Chargershows the shape and does not work on mainnet until then. As of 2026-11-05 the only registered witness is Daimon's own 0xc2f429c80fd2bd6b52ce1dff67cdde9805183690ba7db59a448550a096e1f375::adapter::ChargerV2.

public struct Charger has drop {}      // no public constructor

public fun reserve<T>(config: &Config, identity: &mut Aion, allowance: ID, spender: ID, context: ID,
    nonce: vector<u8>, operation: vector<u8>, max_cost: u64, clock: &Clock, ctx: &mut TxContext) {
    check_spend_is_authorized();       // YOUR gate
    llm_allowance::reserve_grant<T, Charger>(Charger {}, config, identity, allowance, spender, context,
        nonce, operation, vector[], max_cost, vector[], clock, ctx);
}

public fun charge<T>(config: &Config, identity: &mut Aion, amount: u64, clock: &Clock, ctx: &mut TxContext) {
    check_spend_is_authorized();
    let seq = account::next_seq<T>(identity);
    account::charge_authorized<T, Charger>(Charger {}, config, identity, amount, seq, clock, ctx);
}

reserve_grant rules: nonce 1 to 64 bytes, unique per allowance while the grant lives (323); operation 1 to 32 bytes; input_hash empty or 32 bytes; ext at most 256 bytes; 0 < max_cost <= max_per_call. Common aborts: 119 witness not registered, 120 charger not approved by the owner, 329 wrong spender, 334 allowance revoking, 118 no issuer key bound (charge_authorized).

The owner's side, where the witness type is yours:

sui client ptb --move-call $PKG::account::approve_charger "<0x…::example::Charger>" @$CONFIG @$IDENTITY @$CAP --gas-budget 50000000
sui client ptb --move-call $PKG::account::remove_charger  "<0x…::example::Charger>" @$CONFIG @$IDENTITY @$CAP --gas-budget 50000000

Call an LLM from a DAG

This needs no Move code and no registration: Daimon's own charger does the reserving, and your grant-paid allowance names your Talus Protocol Agent as its spender. The grant DAG has three vertices:

  • tech.daimon.chat_hash@1 (off-chain) hashes the chat inputs and passes them on unchanged.
  • tech.daimon.authorize@1 (on-chain) reserves a grant from your allowance for exactly that hash.
  • tech.daimon.chat@1 (off-chain) serves the call against that grant.

The grant settles on chain (GrantSettled): you pay what the call cost, at most max_cost, and the rest stays in the allowance. It takes the nexus CLI 2.1 (brew install talus-network/tap/nexus-cli) configured for mainnet.

export REG=0xee1f602a660903fc3d080ee6c36d77c586d10d7143969c5b56874baba9c095b1
export GRANT_DAG=0x78ad15ccf4f2a2fc0637d2288f02d0ba7bd6d163e424946d59e87cf94af6b4b6

# Once: your Agent and the grant skill. AGENT is the spender of your grant-paid allowance.
nexus tap create-agent --sui-gas-coin <coin> --json                                   # -> AGENT
nexus tap create-skill-artifact --skill-name daimon-chat-hash-grant --dag-id $GRANT_DAG --interface-revision 1 \
  --payment-mode user-funded --fixed-tool $REG=tech.daimon.chat_hash@1 \
  --fixed-tool $REG=tech.daimon.authorize@1 --fixed-tool $REG=tech.daimon.chat@1 --out skill.json
nexus tap register-skill --artifact skill.json --agent-id $AGENT --sui-gas-coin <coin> --json   # -> SKILL
# The task input. Always name the model; max_cost (port 8) at least 5000 micro-USDC; unused chat ports are null.
INPUT=$(jq -nc --arg identity "$IDENTITY" --argjson n "$ALLOWANCE" --arg obj "$ALLOWANCE_OBJ" '{
  hash: {identity_id: $identity, allowance_id: $n, model: "qwen/qwen3-235b-a22b-2507",
         prompt: "Say hello in five words.", messages: null, thread_id: null, options: null},
  authorize: {"0": {one: {kind: "object", id: $identity}}, "2": {one: {kind: "object", id: $obj}}, "8": 200000}}')

nexus task schedule --agent-id $AGENT --skill-id $SKILL --authorization-binding authorize=$IDENTITY \
  --entry-group default --input-json "$INPUT" \
  --prepay-amount-mist 300000000 --occurrence-budget-mist 300000000 --refund-recipient <your address> \
  --now --sui-gas-coin <coin> --json

nexus execution inspect --task-id <task> --occurrence-id 0 --timeout-secs 300 --json
# hash -> ok, authorize -> authorized, chat -> ok, receipt {"kind":"grant","allowance_id":N,"nonce":"…"}
nexus task close --task-id <task> --sui-gas-coin <coin> --json     # refunds the unused prepay
  • It must be an Agent-skill task (--agent-id --skill-id); a plain --dag-id task is refused for a workflow-authorization tool. --entry-group default is required.
  • Give hash the same identity and allowance as authorize (ports 0 and 2), or chat@1 refuses the call before money moves.
  • In your own DAG the prompt can come from an earlier vertex: wire it into the prompt port of hash.
  • Everything you send and get back is public on chain. Do not send secrets.

Voucher-paid calls and the error tables are in the chat tool guide.

Revoke, withdraw, close

Revoke an allowance. revoke_allowance stops new reserves and serves at once and works while Config is paused. Vouchers already served and grants already reserved settle during a 24 h grace. After min(revoke time, expires_ms) + 24 h anyone may run finish_revoke: the whole remainder returns to the identity's own account, never to the caller, and the allowance is deleted. An expired allowance needs no revoke. Abort 319 if called before the release time.

public fun revoke<T>(identity: &mut Aion, cap: &OwnerCap, allowance_id: u64, clock: &Clock, ctx: &mut TxContext) {
    llm_allowance::revoke_allowance<T>(identity, cap, allowance_id, clock, ctx);
}
public fun finish<T>(identity: &mut Aion, allowance_id: u64, clock: &Clock, ctx: &mut TxContext) {
    llm_allowance::finish_revoke<T>(identity, allowance_id, clock, ctx);
}
sui client ptb --move-call $PKG::llm_allowance::revoke_allowance "<$USDC>" @$IDENTITY @$CAP $ALLOWANCE @0x6 --gas-budget 50000000
# after the release time:
sui client ptb --move-call $PKG::llm_allowance::finish_revoke "<$USDC>" @$IDENTITY $ALLOWANCE @0x6 --gas-budget 50000000

Withdraw. account::withdraw sends an amount from the account to a recipient. It needs the OwnerCap, and works while paused and for any coin type.

public fun withdraw_all<T>(identity: &mut Aion, cap: &OwnerCap, recipient: address, ctx: &mut TxContext) {
    let amount = account::account_balance<T>(identity);
    account::withdraw<T>(identity, cap, amount, recipient, ctx);
}
sui client ptb --move-call $PKG::account::withdraw "<$USDC>" @$IDENTITY @$CAP <micro-USDC> @<recipient> --gas-budget 50000000

Close. Withdraw everything, including allowance remainders after finish_revoke, then run account::retire_v2. It requires the account to be empty (114), keeps the identity and OwnerCap, and blocks later deposits, charges and reserves.

sui client ptb --move-call $PKG::account::retire_v2 "<$USDC>" @$IDENTITY @$CAP --gas-budget 50000000

Per-account policy. account::set_policy<T>(config, identity, cap, max_charge_micro, max_day_micro)sets the account's own limits, up to the Config ceiling.

What was verified

  • The Move snippets compile: sui move build in daimon_integration against the package in the zip (sui 1.81.0).
  • Simulated on mainnet on 2026-11-05 with --dry-run against a real identity, nothing sent. Succeeded: agent::new, deposit, create_allowance (both forms), approve_charger, remove_charger, withdraw, finish_revoke. These two reached the contract and aborted as that identity's state requires: revoke_allowance 318 (already revoking) and retire_v2 114 (account not empty).
  • The grant DAG ran on mainnet on 2026-10-27: hash, authorize and chat all passed, and the grant settled 5000 micro-USDC.
  • Not simulated: the example's reserve and charge, which abort until the admin registers Charger.
  • Append --dry-run to any sui client ptb to see its effects without signing.