> For the complete documentation index, see [llms.txt](https://docs.talus.network/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.talus.network/guides/tool-development/build-onchain-tool.md).

# Build an On-Chain Tool

{% hint style="info" %}
**Audience:** Nexus adopters and integrators.

**Goal:** Build and test a Move Tool locally, publish it, then prepare its registration inputs; registration remains blocked until a reviewed package-specific decoder proves the nested witness ID.
{% endhint %}

An on-chain Tool is a Sui Move module with a standardized `execute` function that the Nexus workflow calls on Sui. It is where you mutate on-chain state, move assets, or do anything that must be verifiable and atomic. By the end of this guide you will have built and tested a counter package locally, then prepared a published package and every registration input that public tooling can prove. The released 2.1.1 CLI's `nexus tap test` command can exercise the package against published Nexus bytecode before publication; the final registration transaction is still conditional on a network/package operator supplying a tested decoder for the witness nested in the scaffolded state's `Bag`.

{% hint style="warning" %}
**Required before this guide:** Complete [Prepare for On-Chain Development](/guides/getting-started/prepare-onchain-development.md). Do not write, build, publish, or register this Tool until its public Move Registry dependencies, selected-network package IDs, and `Move.lock` have been verified.
{% endhint %}

For a Tool that wraps an external API or runs off-chain computation, build an [off-chain Tool](/guides/tool-development/build-offchain-tool.md) instead. For asset-moving Tools, also read [Authorization and fixed Tools](/concepts/09-authorization-and-fixed-tools.md): a Fixed Tool records an FQN registration dependency, while client/operator DAG and Tool readback plus application authorization still belong in the Move and workflow logic.

### Prerequisites

* A Bash/POSIX-compatible shell plus `jq`, `curl`, and coreutils-compatible `sha256sum`, `stat`, and `mktemp` for the documented checks. Node.js 18 or later is needed only for the hosted Nexus API tutorial or its React example.
* Complete [Developer Setup](/guides/getting-started/setup.md) and [Prepare for On-Chain Development](/guides/getting-started/prepare-onchain-development.md) before using the commands below.
* Familiarity with the Sui Move language and the Sui CLI (`sui client`).
* The Nexus CLI on your `PATH` and a configured environment (`nexus conf`; see the [CLI reference](/reference/cli/conf.md)).
* Complete [Developer Setup's protected-signer gate](/guides/getting-started/setup.md) before publishing or registering. Without provider-approved protected import and `nexus conf set --help` verification, stop after local build/read-only inspection and do not run Nexus mutation commands.
* After that gate is complete, configure a signer SUI address balance for publishing and registration, verified with `nexus gas balance` and funded with `nexus gas deposit`; use an owned SUI coin only for an explicit gas-coin flag.
* An owned `Coin<US>` for Tool collateral before registration. The required amount is network/deployment-specific; obtain it through the supported wallet/operator path and inspect the owned coin plus the Tool registration requirement before signing. The [priority-fee vault guide](/guides/tokenomics/use-priority-fee-vault.md) documents the owned-coin/readback boundary; do not invent an acquisition command or substitute address-balance SUI.
* For actual registration, a reviewed package-specific dynamic-field decoder and fixture that returns the exact nested Tool witness ID. Without it, stop after publication.

#### How an on-chain Tool works

Two ideas make a Move module a Nexus Tool:

* It **satisfies witness requirements.** The `execute` function receives `UIDRequirements` and must satisfy the registered Tool witness stored in its state. The workflow rejects an incomplete requirement set.
* It produces a **durable result.** Instead of aborting on an expected business failure, `execute` builds a transient `TaggedOutput` whose variant (for example `ok` or `err`) and named payloads become the Tool's output variants and ports, then finalizes it with the requirements into a shared `OnchainToolResult`.

The module also declares an `Output` enum, but it is used **only for schema generation** at registration time — the runtime consumes the `OnchainToolResult`, not the enum.

How on-chain tools compare to off-chain tools:

| Aspect             | On-chain Tool                                          | Off-chain Tool                         |
| ------------------ | ------------------------------------------------------ | -------------------------------------- |
| Runtime            | Sui Move module                                        | HTTP service (Rust)                    |
| Execution          | Runs on Sui inside the transaction                     | Runtime invokes over HTTPS             |
| Best for           | On-chain state changes, asset moves                    | External APIs, LLMs, arbitrary compute |
| Proof of execution | Satisfied witness requirements and `OnchainToolResult` | Verified HTTP result submission        |

#### Step 1 — Scaffold the project

The `nexus tool new --template move` scaffold uses `version = "1.0.0"`, edition `2024.alpha`, and MVR dependencies without an `[addresses]` table. It defaults to Standard execution mode. Install CLI 2.1.1 using [Developer Setup](/guides/getting-started/setup.md) for this scaffold. For a TAP package, use [Scaffold the Talus Agent Package](/guides/tap-development/scaffold-and-package.md), which supplies the TAP-specific modules and environment configuration.

Create a new Move tool project with the CLI:

```bash
nexus tool new --name counter_tool --description "Increment and report a shared counter" --template move --target ./
cd counter_tool
```

This generates a buildable Move package with a fully worked `execute` function that you adapt to your logic. The scaffold declares the Nexus primitives/interface dependencies through MVR. For a network-facing consumer, use the public [nexus-primitives Move Registry package](https://www.moveregistry.com/package/@talus/nexus-primitives) and [nexus-interface Move Registry package](https://www.moveregistry.com/package/@talus/nexus-interface).

```toml
[package]
name = "counter_tool"
version = "1.0.0"
edition = "2024.alpha"

[dependencies]
nexus_primitives = { r.mvr = "@talus/nexus-primitives" }
nexus_interface = { r.mvr = "@talus/nexus-interface" }

```

The generated Tool uses both packages directly: `nexus_primitives` supplies data, requirements, and tagged outputs; `nexus_interface` supplies `OnchainToolResult` and the optional Agent authorization type. `r.mvr` resolves the package by its public name for the selected network; commit `Move.lock` and keep its resolved addresses matched to the deployment. For a disposable offline compile or source inspection, copy the direct dependencies from the public [Nexus Move Packages](https://github.com/Talus-Network/nexus-move-packages/tree/main/packages) repository and retain any required transitive closure, including `packages/kernel`, in the same `deps/` tree. That copied closure is not an installation authority, and kernel is support-only rather than a direct application dependency. This standalone counter uses the default Standard mode so a DAG Task can call it without authorization bindings. For a Tool that requires an Agent vertex authorization proof, scaffold with `--mode workflow-authorization` and implement the recipient/state/input-commitment checks before mutation.

{% hint style="warning" %}
Point the dependency sources and addresses at your target network — a selected public package revision for local inspection, or deployment-matched published packages and revisions for the network you publish to. Published addresses differ between localnet, testnet, and mainnet, so never hard-code one network's addresses for another.
{% endhint %}

Before changing dependencies, complete [Developer Setup](/guides/getting-started/setup.md) to verify the selected CLI and network. MVR resolves dependency addresses; do not reintroduce a legacy own-package `[addresses]` table into the generated manifest. Keep `Move.lock` and deployment package metadata together, and stop rather than infer IDs when a matching public registry record is unavailable.

#### Step 2 — Module structure

The module starts with its imports, a one-time witness, a Tool witness (the requirement identity), and your Tool's state:

```move
module counter_tool::counter_tool;

use nexus_primitives::data;
use nexus_interface::onchain_tool_result::{Self as onchain_tool_result, OnchainToolResult};
use nexus_primitives::proof_of_uid::UIDRequirements;
use nexus_primitives::tagged_output;
use sui::bag::{Self, Bag};
use sui::clock::Clock;
use sui::transfer::share_object;
use std::ascii::String as AsciiString;

/// One-time witness for package initialization.
public struct COUNTER_TOOL has drop {}

/// Witness object used to satisfy this Tool's execution requirement.
public struct CounterToolWitness has key, store {
    id: UID,
}

/// Your tool's state object.
public struct CounterToolState has key {
    id: UID,
    /// Stores the witness object that identifies this Tool requirement.
    witness: Bag,
    /// Application-specific state: the running count.
    count: u64,
}
```

The `init` function creates the state, stores the witness inside its `Bag`, and shares the state object so the workflow can pass it into `execute`:

```move
fun init(_otw: COUNTER_TOOL, ctx: &mut TxContext) {
    let state = CounterToolState {
        id: object::new(ctx),
        witness: {
            let mut bag = bag::new(ctx);
            bag.add(b"witness", CounterToolWitness { id: object::new(ctx) });
            bag
        },
        count: 0,
    };
    share_object(state);
}
```

**Owned vs shared objects**

Any object argument `execute` needs must be reachable when the workflow builds the transaction:

* **Shared objects** (like `CounterToolState`, created with `share_object`) are the usual choice for tool state: any execution can reference them by id, and Sui serializes concurrent mutations for you.
* **Owned objects** can be passed too, but they must be owned by the address submitting the execution, which couples the tool to a specific caller. Prefer shared state for tools reused across agents.

Primitive inputs (like `increase_with: u64`) are supplied per invocation as the tool's input ports; object arguments are supplied by the workflow when it assembles the call.

#### Step 3 — Define the output variants

Declare an `Output` enum describing every variant your tool can emit. The CLI reads this enum to generate the output schema during registration:

```move
/// Tool execution output variants.
/// Used only for automatic schema generation during registration — the runtime
/// consumes the OnchainToolResult, not this enum.
public enum Output {
    Ok {
        old_count: u64,
        new_count: u64,
        increment: u64,
    },
    Err {
        reason: AsciiString,
    },
    LargeIncrement {
        old_count: u64,
        new_count: u64,
        increment: u64,
        warning: AsciiString,
    },
}
```

#### Step 4 — Implement `execute`

`execute` is the core of the Tool. The standard signature starts with two framework-supplied owned values, continues with every user-facing input port, and ends with `TxContext`. Add `Clock` explicitly when your Tool needs time; it is a normal shared-object input port.

Keep the application-owned state transition in a private helper. Both the published `execute` function and the test-only module extension in Step 7 call this helper, so a local unit test covers the same counter decision without calling Nexus interface stubs:

```move
fun apply_increment(
    state: &mut CounterToolState,
    increase_with: u64,
): (vector<u8>, u64, u64, u64) {
    let old_count = state.count;
    if (increase_with == 0) {
        (b"err", old_count, old_count, 0)
    } else {
        state.count = state.count + increase_with;
        if (increase_with > 100) {
            (b"large_increment", old_count, state.count, increase_with)
        } else {
            (b"ok", old_count, state.count, increase_with)
        }
    }
}

public fun execute(
    requirements: UIDRequirements,
    result: OnchainToolResult,
    state: &mut CounterToolState,
    increase_with: u64,
    clock: &Clock,
    ctx: &mut TxContext,
) {
    let mut requirements = requirements;
    requirements.satisfy(&state.witness().id);
    let (variant, old_count, new_count, increment) =
        apply_increment(state, increase_with);

    let output = if (variant == b"err") {
        tagged_output::new(b"err")
            .with_named_payload(b"reason", data::inline_data_value(b"\"Cannot increment by zero\""))
    } else if (variant == b"large_increment") {
        tagged_output::new(b"large_increment")
            .with_named_payload(b"old_count", data::inline_data_value(old_count.to_string().into_bytes()))
            .with_named_payload(b"new_count", data::inline_data_value(new_count.to_string().into_bytes()))
            .with_named_payload(b"increment", data::inline_data_value(increment.to_string().into_bytes()))
            .with_named_payload(b"warning", data::inline_data_value(b"\"Large increment, consider smaller steps\""))
    } else {
        tagged_output::new(b"ok")
            .with_named_payload(b"old_count", data::inline_data_value(old_count.to_string().into_bytes()))
            .with_named_payload(b"new_count", data::inline_data_value(new_count.to_string().into_bytes()))
            .with_named_payload(b"increment", data::inline_data_value(increment.to_string().into_bytes()))
    };

    let _ = clock.timestamp_ms();
    onchain_tool_result::finalize_and_share(result, requirements, output, ctx);
}
```

Notes on the signature:

* `requirements` and `result` are framework-supplied. The CLI detects this prefix and selects standard registration automatically; there is no manual registration-mode flag.
* Place user-facing ports (`state`, `increase_with`, and `clock`) after the fixed prefix and before `ctx`. The generated schema numbers them `"0"`, `"1"`, and `"2"` in that order.
* Satisfying the state witness is mandatory. An unsatisfied `UIDRequirements` value cannot produce an accepted result.
* End with `onchain_tool_result::finalize_and_share(result, requirements, output, ctx)`. The function is `public`, returns no Move value, and exposes data through the durable result.

**Field value types**

`with_named_payload` stores canonical inline JSON bytes. Numbers and booleans are unquoted; strings and addresses include their JSON quotes:

```move
// Numeric value
.with_named_payload(b"count", data::inline_data_value(value.to_string().into_bytes()))

// String value (quotes are part of the encoded JSON)
.with_named_payload(b"message", data::inline_data_value(b"\"Hello world\""))

// Boolean value
.with_named_payload(b"success", data::inline_data_value(b"true"))

// Address value: build quoted 0x-prefixed JSON bytes first
.with_named_payload(b"sender", data::inline_data_value(encoded_address))

// Object/array/null JSON
.with_named_payload(b"metadata", data::inline_data_value(b"{\"key\":\"value\"}"))
```

**Avoiding hot-potato leaks**

`TaggedOutput` has no `drop` ability, so the Move compiler forces every code path to produce one. The `if`/`else` above binds it to `output` on every branch, then hands it to `finalize_and_share`, which consumes it. A branch that builds a `TaggedOutput` and forgets it fails to compile with `UnusedValueWithoutDrop`. The same applies to any other non-`drop` value you create inside `execute` — consume or return it.

**Validation boundaries**

Treat local validation as layered evidence. `sui move build` and `sui move summary` prove Move compilation and normalized ABI shape, while the public SDK CLI's DAG/TAP commands prove artifact structure and configuration syntax; none of these checks executes native framework functions, proves witness acceptance at runtime, or proves that a FQN is registered to the generated module. Keep a build record containing the selected package revision, compiled module SHA-256, module identity, expected FQN/module/function/schema intent, and the deployment readback used for comparison. These structural checks do not replace native execution on a local/deployed runtime or live Tool/DAG/skill registry readback.

#### Step 5 — Authorization mode and fixed Tools

The counter above uses standard mode, so `execute` begins with `UIDRequirements` and `OnchainToolResult`. If an Agent skill needs workflow-authorization mode, prepend owned `ProvenValue<AgentVertexAuthorization>`; the CLI derives the mode from that published signature and chooses the matching registry entry point. Asset-sensitive Tools still need application checks around their state and inputs.

**Cap-gated workflow authorization**

Use the cap-gated signature when the Tool mutates state or releases an asset for one protected DAG vertex. The runtime proof must be consumed against the exact worksheet recipient and the result's input commitment before the Tool satisfies its witness or changes application state:

```move
use nexus_interface::authorization::{Self as interface_authorization, AgentVertexAuthorization};
use nexus_primitives::authorization::ProvenValue;

const EAuthorizationMismatch: u64 = 0;
const EBusinessStateInvariant: u64 = 1;

public fun execute(
    authorization: ProvenValue<AgentVertexAuthorization>,
    requirements: UIDRequirements,
    result: OnchainToolResult,
    state: &mut CounterToolState,
    ctx: &mut TxContext,
) {
    let mut requirements = requirements;
    let input_commitment = onchain_tool_result::input_commitment(&result);
    assert!(
        interface_authorization::consume_verified_for_worksheet_as_recipient(
            authorization,
            requirements.proof(),
            &state.id,
            input_commitment,
        ),
        EAuthorizationMismatch,
    );

    // Keep application-specific custody and state checks after proof consumption.
    assert!(state.count < 1_000_000, EBusinessStateInvariant);
    requirements.satisfy(&state.witness().id);
    state.count = state.count + 1;

    let output = tagged_output::new(b"ok")
        .with_named_payload(b"new_count", data::inline_data_value(state.count.to_string().into_bytes()));
    onchain_tool_result::finalize_and_share(result, requirements, output, ctx);
}
```

This example keeps the Standard-mode counter above deliberately separate from the cap-gated path. The presence of `ProvenValue`, a Fixed Tool entry, or a DAG identity is not proof that the submitted worksheet recipient, vertex, Task, and input commitment authorize this mutation; the runtime consumption check is the admission boundary.

`fixed_tools` in a TAP skill records required Tool FQNs, and Skill registration checks that each FQN is registered at transaction time. The pinned code does not validate the stored registry ID or DAG membership, and `update_dag` does not rerun the check; independently read back the DAG vertex/FQN, Tool ID, registry ID, immutable schema, authorization mode, and application-specific policy before asset-sensitive use. Stronger enforcement remains unresolved, and these checks do not replace package-specific sender, capability, recipient, amount, or state checks. Keep those authorization rules in the Move package whose assets they protect.

#### Step 6 — Emitting events

Emitting Sui events makes executions observable off-chain (indexers, dashboards, the SDK's event poller). Declare an event struct with `copy, drop` and emit it from `execute`:

```move
use sui::event;

/// Emitted whenever the counter is incremented.
public struct CounterIncremented has copy, drop {
    old_count: u64,
    new_count: u64,
    increment: u64,
}
```

Events are independent of the result: the `OnchainToolResult` drives DAG data flow, while events are for observability. Emitting the event on the success branches, before building the tagged output, keeps them in sync.

#### Step 7 — Helpers and tests

Add the witness accessor and a public getter for the tool witness id, which is useful when registering:

```move
/// Borrow the witness object stored in the state bag.
fun witness(self: &CounterToolState): &CounterToolWitness {
    self.witness.borrow(b"witness")
}

/// Get the tool witness id, used during on-chain registration.
public fun tool_witness_id(self: &CounterToolState): ID {
    object::uid_to_inner(&self.witness().id)
}

/// Read the current count.
public fun count(self: &CounterToolState): u64 {
    self.count
}
```

**Test the execute core with a module extension**

Keep test-only construction, invocation, inspection, and cleanup out of the production module by adding `tests/counter_tool_extension.move`:

```move
#[test_only]
extend module counter_tool::counter_tool;

public fun state_for_testing(count: u64, ctx: &mut TxContext): CounterToolState {
    let mut witness = sui::bag::new(ctx);
    sui::bag::add(
        &mut witness,
        b"witness",
        CounterToolWitness { id: sui::object::new(ctx) },
    );
    CounterToolState {
        id: sui::object::new(ctx),
        witness,
        count,
    }
}

public fun execute_logic_for_testing(
    state: &mut CounterToolState,
    increase_with: u64,
): (vector<u8>, u64, u64, u64) {
    apply_increment(state, increase_with)
}

public fun destroy_state_for_testing(state: CounterToolState) {
    let CounterToolState { id, mut witness, count: _ } = state;
    let CounterToolWitness { id: witness_id } =
        sui::bag::remove(&mut witness, b"witness");
    witness_id.delete();
    sui::bag::destroy_empty(witness);
    id.delete();
}
```

The extension shares the Tool module's private scope, so it can construct `CounterToolState`, call the private `apply_increment` function used by `execute`, and clean up the key and `Bag` values. It is excluded from production bytecode and cannot replace an existing function. Use `_for_testing` names and add only the constructors, invocations, observations, and cleanup required by the test.

Add `tests/counter_tool_tests.move` to exercise the normal branch:

```move
#[test_only]
module counter_tool::counter_tool_tests;

use counter_tool::counter_tool;
use std::unit_test::assert_eq;

#[test]
fun execute_logic_applies_normal_increment() {
    let ctx = &mut sui::tx_context::dummy();
    let mut state = counter_tool::state_for_testing(4, ctx);
    let (variant, old_count, new_count, increment) =
        counter_tool::execute_logic_for_testing(&mut state, 3);

    assert!(variant == b"ok");
    assert_eq!(old_count, 4);
    assert_eq!(new_count, 7);
    assert_eq!(increment, 3);
    assert_eq!(state.count(), 7);

    counter_tool::destroy_state_for_testing(state);
}
```

Repeat the test with `0` and a value greater than `100` to cover the `err` and `large_increment` decisions. Run the suite with `sui move test --build-env testnet`. Module extensions require `edition = "2024.alpha"`; follow [Local Move tests with module extensions](/guides/getting-started/prepare-onchain-development.md#local-move-tests-with-module-extensions) for the manifest and evidence boundaries.

This extension tests the application-owned state transition that `execute` delegates to; it does not locally complete the public `execute` function. Do not call `data::inline_data_value`, `tagged_output::new`, or `onchain_tool_result::finalize_and_share` as if the interface dependency were a mock: existing Nexus interface functions deliberately abort in the local Move VM. Assertions about witness acceptance, result finalization, authorization, events, shared protocol objects, or Nexus state transitions require a real Testnet transaction.

#### Optional published-bytecode test for a Nexus-calling fixture

The `counter_tool_tests` example above calls only the private `execute_logic_for_testing` helper through a test-only extension. Its `sui move test` run proves the application-owned increment decisions; it does not invoke the public `execute`, `UIDRequirements`, `OnchainToolResult`, witness, authorization, or finalization paths. Do not treat that test as proof of those protocol calls.

For a fixture that actually calls published Nexus functions, set `MOVE_PACKAGES_ROOT` to the root of a disposable public [Nexus Move Packages](https://github.com/Talus-Network/nexus-move-packages) checkout, then use the CLI described in [Prepare for On-Chain Development](/guides/getting-started/prepare-onchain-development.md#local-published-bytecode-tap-tests) with its [embedded Tool/TAP example](https://github.com/Talus-Network/nexus-move-packages/tree/main/examples/local_testing):

```bash
nexus tap test --path "$MOVE_PACKAGES_ROOT/examples/local_testing" --list --build-env "${SUI_BUILD_ENV:-testnet}"
nexus tap test --path "$MOVE_PACKAGES_ROOT/examples/local_testing" execute_accepts_content_and_finalizes_the_nexus_result --threads 1 --build-env "${SUI_BUILD_ENV:-testnet}"
```

That fixture arranges a real published `execute` call and tests authorization, input commitment, state changes, and result finalization in the local VM. Its result is still local/read-only evidence: it does not prove package publication, Tool registration, DAG or skill binding, or live Task execution. Use the package-owned `sui move test` extension for application logic and reserve Testnet transactions for the live boundary.

#### Step 8 — Publish to Sui

Publish the package to your target network:

```bash
export SUI_BUILD_ENV="testnet"
sui move build --build-env "$SUI_BUILD_ENV"
sui move test --build-env "$SUI_BUILD_ENV"
```

The build proves that the unpublished package compiles against the selected dependency graph. `sui move test` proves application-owned logic and expected stub boundaries. The explicit released fixture command above exercises selected published Nexus bytecode in a local VM when its tests call public functions. Neither local check proves package publication, Tool registration, or live workflow execution. Complete the live `execute` path after Testnet publication and registration with a real transaction and check its effects, events, and object state.

```bash
sui client publish . --build-env "$SUI_BUILD_ENV" --json > publish.json

# Save the package id from the output.
export PACKAGE_ID="0x..."
export COUNTER_STATE_ID="0x..."
```

From the publish output, note two things: the **package id** (the address of your published package) and the **shared state object id** (the `CounterToolState` created by `init`).

You then need the **Tool witness ID**: the UID of the `CounterToolWitness` stored inside the state's `witness` bag. It is the requirement identity used during registration — it is *not* the shared state object ID. A parent-object read is useful context, but it is not proof of the nested UID:

```bash
sui client object "$COUNTER_STATE_ID" --json > counter-state.json
```

The public Nexus CLI has no generic decoder for this package-specific nested witness. Before registration, obtain the named operator artifact `operator-supplied/decode-counter-tool-witness`, record its reviewed source URL or commit and SHA-256, and require it to emit this proof JSON contract: `state_id` equal to `COUNTER_STATE_ID`, `field_key` equal to `witness`, `witness_type` equal to `${PACKAGE_ID}::counter_tool::CounterToolWitness`, `witness_uid_field` equal to `id`, `witness_id` equal to the nested UID, and `proof` equal to `Bag dynamic field witness`. The artifact must read the published object/Bag and prove the UID, not copy an address from an input or package manifest.

Verify the artifact before accepting its output:

```bash
export WITNESS_DECODER="./operator-supplied/decode-counter-tool-witness"
export WITNESS_DECODER_SOURCE="<reviewed source URL or commit>"
export WITNESS_DECODER_SHA256="<64-hex SHA-256 for the reviewed artifact>"
test -x "$WITNESS_DECODER"
test -n "$WITNESS_DECODER_SOURCE"
test "${#WITNESS_DECODER_SHA256}" = 64
printf '%s  %s\n' "$WITNESS_DECODER_SHA256" "$WITNESS_DECODER" | sha256sum -c -
"$WITNESS_DECODER" "$COUNTER_STATE_ID" > witness-proof.json
jq -e --arg state "$COUNTER_STATE_ID" --arg type "${PACKAGE_ID}::counter_tool::CounterToolWitness" '
  .state_id == $state
  and .field_key == "witness"
  and .witness_type == $type
  and .witness_uid_field == "id"
  and (.witness_id | type == "string" and test("^0x[0-9a-fA-F]+$"))
  and .proof == "Bag dynamic field witness"
' witness-proof.json
export TOOL_WITNESS_ID="$(jq -er '.witness_id' witness-proof.json)"
```

The Move getter documents the relationship but is not itself an off-chain read procedure. Stop if the named artifact, source/checksum verification, exact type/UID proof, or output fields are unavailable; substituting the counter ID creates a mismatched Tool record.

#### Step 9 — Register the tool

This step is conditional. Proceed only after the package-specific decoder has proved `TOOL_WITNESS_ID` from the published state; the parent-object read above is not sufficient evidence. The CLI analyzes your `execute` function and `Output` enum to generate the input and output schemas, then writes the record into the on-chain ToolRegistry:

```bash
nexus tool register onchain \
  --package "$PACKAGE_ID" \
  --module counter_tool \
  --tool-fqn "xyz.taluslabs.counter-tool@1" \
  --description "Increments an on-chain counter" \
  --tool-witness-id "$TOOL_WITNESS_ID" \
  --timeout 5s
```

After registration, validate the live Tool record by FQN:

```bash
nexus tool validate onchain --ident "xyz.taluslabs.counter-tool@1" --json
```

Success returns JSON with `valid: true`, `fqn`, and `tool`. This proves that the registered Tool's published `execute`/`Output` schema and Standard or WorkflowAuthorization mode agree with the live registration; it does not prove package lineage, UpgradeCap custody, dependency linkage, witness ownership, or business invariants.

What each flag does:

* `--package` / `--module` (`-m`) — the published package address and the Move module name that contains `execute`.
* `--tool-fqn` — the fully qualified name (`domain.name@version`).
* `--description` — a human-readable description.
* `--tool-witness-id` — the `CounterToolWitness` object id from the previous step.
* `--timeout` — the execution timeout (defaults to `5s`; must be between `1s` and `2m`).

Two more flags are available when you need them:

* `--collateral-coin <OBJECT_ID>` — the `Coin<US>` to use as collateral; when omitted, the CLI selects an available matching coin.
* `--invocation-cost <MIST>` — set the Tool's per-invocation price; it defaults to `0` for development.

> **Tool and cashier custody:** Registration returns a `Tool` object plus separate `CloneableOwnerCap<OverTool>` and `CloneableOwnerCap<OverToolCashier>` capabilities for your address. The Tool owner capability authorizes Tool lifecycle/package-pointer work; the cashier capability authorizes accepted-policy and collection work. Registration locks an owned `Coin<US>`; after unregistering and waiting for the configured lock, `nexus tool claim-collateral --tool-fqn <FQN> --owner-cap <OVER_TOOL_CAP_ID>` reclaims that collateral through `OverTool`. It is not SUI gas or Tool earnings. `collect-invocations` collects finalized Invocation receipts and `collect-deposits` collects generic `CashierDeposit` objects. Verify selected-build help, bindings, capabilities, package IDs, tests, and transaction effects; do not substitute `claim-collateral` for either collection path.

Unless `--no-save` is supplied, the CLI records those separate capability IDs in local Tool configuration. Confirm the live state:

```bash
nexus tool inspect --tool-fqn "xyz.taluslabs.counter-tool@1" --json
```

Use the exact FQN from the registration receipt and compare the returned identity, registration state, and network with that receipt. If the FQN is unknown, recover it from the deployment record or DAG. Exit status 0 alone does not prove registration: if the Tool cannot be read or its state remains unknown, retain the error as missing evidence. Do not register a duplicate Tool to repair discovery.

To change the Tool price, use the command and separate cashier-admin capability, then inspect the Tool, `ExecutionPayment`, exact Invocation, effects, and events:

```bash
nexus tool set-invocation-cost \
  --tool-fqn "xyz.taluslabs.counter-tool@1" \
  --cashier-admin <CASHIER_ADMIN_ID> \
  --cost <MIST>
```

Use `nexus tool cashier --help` to inspect the FixedPrice, FiniteCredits, TimePass, inbox, and collection families. A successful price update does not prove an Invocation settlement or revenue collection; retain the exact Invocation ID, settlement/refund receipt, cashier inbox, collection effects, and resulting balance separately.

#### Step 10 — How the workflow executes an on-chain Tool

When a workflow reaches an `on_chain` vertex, the leader builds a programmable transaction block (PTB) that calls your module's `execute`, supplies its framework prefix plus the DAG inputs, and submits the durable `OnchainToolResult`. If the Tool call aborts, that transaction rolls back; an expected business failure should use an `err` result variant when downstream routing must continue.

Immediate transaction gas, Tool price, and scheduled custody are separate meters:

* **Immediate Sui transaction gas** is paid upfront by the signer or leader from its address balance or an explicitly owned gas coin; it is not drawn from the Task reserve or Occurrence budget.
* **The Tool’s invocation price**, `invocation_costs_mist` set at registration or with `nexus tool set-invocation-cost --tool-fqn <FQN> --cashier-admin <OBJECT_ID> --cost <MIST>`, is snapshotted for the exact Invocation and its `ExecutionPayment` lock. Verify the Tool record, Invocation, payment lock, settlement/refund receipt, effects, and events.
* **Eligible leader reimbursement** is only the recorded commit/settlement gas evidence that the protocol accepts for reimbursement through `ExecutionPayment`; priority-fee charges use their separate reserve and deposit path.
* **TaskPaymentReserve** funds future occurrence budgets and is restored after occurrence settlement; it does not pay immediate Sui transaction gas.

Set the invocation cost to `0` while developing so sample DAGs run without charging callers.

#### Step 11 — Use the Tool in a workflow

Once registered, reference the tool from a DAG by its FQN with the `on_chain` variant:

```json
{
  "vertices": [
    {
      "kind": {
        "variant": "on_chain",
        "tool_fqn": "xyz.taluslabs.counter-tool@1"
      },
      "name": "counter_increment",
      "entry_ports": [
        { "name": "0" },
        { "name": "1" },
        { "name": "2" }
      ]
    }
  ],
  "edges": []
}
```

The three entry ports match `state`, `increase_with`, and `clock` after the hidden framework prefix. Publish the DAG only after the Tool is registered, then schedule it through the Task model:

```bash
nexus tap default-agent show --json
# If this reports AgentRegistry missing default agent or EDefaultDagExecutorMissing,
# stop and hand the deployment to its operator before using --dag-id.
nexus dag validate --path counter-dag.json
nexus dag publish --path counter-dag.json --json > counter-dag-publish.json
DAG_ID=$(jq -r '.dag_id' counter-dag-publish.json)

nexus task schedule \
  --dag-id "$DAG_ID" \
  --input-json "{\"counter_increment\":{\"0\":\"$COUNTER_STATE_ID\",\"1\":10,\"2\":\"0x6\"}}" \
  --prepay-amount-mist 50000000 \
  --occurrence-budget-mist 50000000 \
  --now \
  --json > counter-task.json
```

Read the Task and Occurrence IDs from the receipt, follow them with `nexus task occurrence inspect --follow`, and inspect the runtime with `nexus execution inspect`. The `ok` variant exposes `old_count`, `new_count`, and `increment` for downstream edges and durable inspection. Capture the execution output and transaction evidence immediately: Testnet history can be pruned after only a few days. To settle and close the one-shot Task, follow [Execute and settle an Agent](/guides/agent-usage/execute-and-settle-agent.md).

#### Verification checklist

You have built a complete on-chain Tool when you can:

* run application-owned Move unit tests green without treating Nexus interface stubs as local mocks;
* publish the package (`sui client publish`);
* register it (`nexus tool register onchain …`) and see it via `nexus tool inspect --tool-fqn …`;
* publish a three-input `on_chain` DAG, schedule a Task, and observe the expected durable output;
* observe the failure path (the `err` variant) when you pass an invalid input.

#### Common failure modes

| Symptom                                  | Likely cause and fix                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UnusedValueWithoutDrop`                 | A branch of `execute` built a `TaggedOutput` (or another non-`drop` value) without passing it to `finalize_and_share`. Ensure every branch binds one `output` and finalizes it.                                                                                                                                                                                                                                                                                                              |
| Missing capability / authorization error | The Tool signature selected workflow-authorization mode or the application state rejected the supplied proof/inputs. Inspect the registered mode, Agent skill, and package-specific checks.                                                                                                                                                                                                                                                                                                  |
| Wrong object ownership                   | `execute` expects an owned object the submitter does not own. Prefer shared state (`share_object`) for reusable tools.                                                                                                                                                                                                                                                                                                                                                                       |
| Shared-object version errors             | A stale reference to the shared state object. Re-fetch it; do not cache versions across executions.                                                                                                                                                                                                                                                                                                                                                                                          |
| `FunctionNotFound`                       | `--module`/`--package` do not match the published module that contains `execute`, or the function is not an `entry`/`public` function. Verify with `sui client object`.                                                                                                                                                                                                                                                                                                                      |
| Move abort code                          | `execute` (or a callee) hit an `abort`/`assert!`. Prefer returning an `err` variant over aborting; decode the code from the module that raised it.                                                                                                                                                                                                                                                                                                                                           |
| Transaction or payment failure           | After the [protected-signer gate](/guides/getting-started/setup.md) is approved, fund the signer's or leader's SUI address balance with `nexus gas balance`/`nexus gas deposit` or the explicit owned gas coin required for the transaction; otherwise stop at query-only inspection and do not improvise a signer import. Ensure the Task reserve and ExecutionPayment cover their separate budgets and charges. Lower `set-invocation-cost` only when changing the Tool price is intended. |

#### Next guides

* [Build an off-chain tool](/guides/tool-development/build-offchain-tool.md) — wrap an external API in Rust.
* [Authorization and fixed Tools](/concepts/09-authorization-and-fixed-tools.md) — separate identity preservation from application asset authorization.
* [Execute and settle an Agent](/guides/agent-usage/execute-and-settle-agent.md) — run this Tool through Task, Occurrence, and Execution.
* [Upgrade an on-chain Tool](/guides/tool-development/build-onchain-tool-vertex-and-upgrade.md) — preserve the Tool identity while moving its package pointer to a validated compatible package.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.talus.network/guides/tool-development/build-onchain-tool.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `build a script that syncs our docs to a CMS` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
