> 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/talus-docs-v2.1.0/guides/tool-development/verify-offchain-tool-result.md).

# Verify an Off-Chain Tool Result

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

**Goal:** Configure and prove the selected per-vertex verifier mode so an off-chain Tool result advances only with the intended evidence.
{% endhint %}

An off-chain Tool returns a result through the leader. This guide configures the complete `RegisteredKey` path: active Tool and Leader keys, Toolkit JSON, ToolRegistry verifier support, the per-vertex DAG field, inspection, and negative evidence. It ends with the separate External verifier boundary.

### Prerequisites

* A registered off-chain Tool that you can run and redeploy; see [Build an off-chain Tool](/talus-docs-v2.1.0/guides/tool-development/build-offchain-tool.md).
* The Tool's saved `OwnerCap<OverTool>` record, or its object ID for `--owner-cap`.
* A configured Nexus CLI, signer SUI address balance, and target-network object bundle.
* A public Leader capability identifier and active-key readback from the selected integration environment; this guide does not publish Leader process setup.
* `jq`, `curl`, and a non-production environment for negative verification tests.

#### The verifier modes

Each off-chain DAG vertex chooses exactly one `ToolVerifierMode`:

* **`None`** — ordinary submission without verifier proof.
* **`RegisteredKey`** — active Leader and Tool Ed25519 keys authenticate the schema-ordered canonical input commitment and canonical response BCS through signed HTTP v3.
* **`External`** — workflow calls the Tool's registered on-chain verifier and consumes its `Accept` or `Reject` verdict.

`RegisteredKey` proves provenance and integrity of the canonical evidence, not the semantic truth of a correctly signed answer. `External` proves that the configured verifier ran, not that its governance or logic is universally trustworthy.

#### RegisteredKey data flow

```mermaid
sequenceDiagram
  participant Leader as Leader
  participant Toolkit as Toolkit Tool
  participant Workflow as Workflow
  participant Auth as NetworkAuth
  Leader->>Leader: Hash schema-ordered canonical input and derive invocation nonce
  Leader->>Toolkit: POST /invoke with v3 Leader signature headers
  Toolkit->>Auth: Resolve Leader ID and key ID from local allowlist
  Toolkit->>Toolkit: Verify Leader signature and recompute canonical input hash
  Toolkit->>Toolkit: Invoke Tool and encode canonical response BCS
  Toolkit-->>Leader: Canonical BCS plus v3 Tool signature
  Leader->>Leader: Parse required Tool signature — do not make a local production verdict
  Leader->>Workflow: Submit result, input hash, nonce, and both signatures
  Workflow->>Auth: Resolve active Leader and Tool keys
  Workflow->>Workflow: Recompute commitments/nonce and verify both signatures
  Workflow-->>Leader: Accept or reject result
```

The Leader signature covers only the 32-byte schema-ordered input hash. The Tool signs `"nexus.direct.v3.tool-response" || leader_signature || nonce || SHA-256("nexus.direct.v3.raw-output" || canonical_response_bcs)`. HTTPS protects unsigned headers and plaintext request/response transport. V3 has no method/path/query/raw-body signature, no `iat`/`exp`, no signed status, and no Tool KID header.

#### Step 1 — Generate and register the Tool key

Set the exact FQN and generate a keypair file. Capture the runtime secret directory before any later `cd`, protect the file before it contains any private material, avoid putting the key in command arguments or history, and delete or migrate temporary copies through a controlled backup lifecycle:

```bash
export TOOL_FQN="xyz.taluslabs.exchanges.coinbase.spot-price@1"
umask 077
SECRET_DIR="$(mktemp -d)"
chmod 700 "$SECRET_DIR"
SECRET_FILES=(
  "$SECRET_DIR/tool-key.json"
  "$SECRET_DIR/tool-signing-key.hex"
  "$SECRET_DIR/tool-signing-key.hex.bak"
  "$SECRET_DIR/toolkit-config.json"
)
cleanup_secrets() {
  status=$?
  trap - EXIT
  unset TOOL_SIGNING_KEY NEXUS_TOOLKIT_CONFIG_PATH
  for secret_file in "${SECRET_FILES[@]}"; do
    rm -f -- "$secret_file"
  done
  for secret_file in "${SECRET_FILES[@]}"; do
    if test -e "$secret_file"; then
      printf 'secret cleanup failed: %s remains\n' "$secret_file" >&2
      status=1
    fi
  done
  if test -d "$SECRET_DIR" && ! rmdir -- "$SECRET_DIR"; then
    printf 'secret cleanup failed: %s is not empty\n' "$SECRET_DIR" >&2
    status=1
  fi
  if test -e "$SECRET_DIR"; then
    printf 'secret cleanup failed: %s remains\n' "$SECRET_DIR" >&2
    status=1
  fi
  exit "$status"
}
trap cleanup_secrets EXIT
nexus tool auth keygen --out "$SECRET_DIR/tool-key.json"
chmod 600 "$SECRET_DIR/tool-key.json"
test "$(stat -c '%a' "$SECRET_DIR/tool-key.json")" = 600
jq -er '.private_key_hex' "$SECRET_DIR/tool-key.json" > "$SECRET_DIR/tool-signing-key.hex"
chmod 600 "$SECRET_DIR/tool-signing-key.hex"
test "$(stat -c '%a' "$SECRET_DIR/tool-signing-key.hex")" = 600
```

`keygen --out` writes JSON, while `register-key --signing-key` accepts a raw encoded key or a file containing only that encoded key. Extract the `private_key_hex` value rather than passing the JSON file itself.

Register the key and read it back:

```bash
nexus tool auth register-key \
  --tool-fqn "$TOOL_FQN" \
  --signing-key "$SECRET_DIR/tool-signing-key.hex" \
  --skip-if-active

nexus tool auth list-keys --tool-fqn "$TOOL_FQN"
```

The CLI resolves `OwnerCap<OverTool>` from the saved Tool record. If that record is unavailable, add `--owner-cap <OWNER_CAP_OBJECT_ID>`. Retain the registration transaction digest, Tool ID, binding object ID, active Tool key binding `key_id`, and the corresponding public-key evidence. Keep the private key in a secret manager; the shell variable is a local tutorial convenience.

#### Step 2 — Configure RegisteredKey support on the Tool

Registering a message key does not configure Tool verifier support. The Tool owner must perform the separate ToolRegistry mutation:

```bash
nexus tool configure-verifier registered-key \
  --tool-fqn "$TOOL_FQN"
```

This command also resolves the owner capability from saved CLI config, with `--owner-cap <OWNER_CAP_OBJECT_ID>` as the explicit override. Read the canonical Tool record back:

```bash
nexus tool inspect --tool-fqn "$TOOL_FQN" --json > tool-inspect.json
jq -e '.exists == true and .verifier_support.kind == "registered_key"' tool-inspect.json
```

The configuration result and inspection output are different evidence: retain the mutation digest and the later Tool state projection.

#### Step 3 — Export or synchronize allowed Leaders

Capture one absolute allowlist path, then export every Leader identity with an active Ed25519 key:

```bash
leaders_path="$PWD/allowed-leaders.json"
nexus tool auth export-allowed-leaders \
  --all \
  --out "$leaders_path"
sha256sum "$leaders_path"
```

For a restricted Tool, replace `--all` with one or more `--leader <LEADER_CAP_ID>` arguments. For a long-running refresh process:

```bash
nexus tool auth sync-allowed-leaders \
  --out "$leaders_path" \
  --interval 30s
```

Add `--once` to refresh atomically and exit. The exported file's schema version is `1`, but it is the active `AllowedLeadersFileV1` schema under the signed-HTTP v3 wire module. The Toolkit endpoint authenticates against the local file contents without an RPC lookup; refresh or restart that local file after NetworkAuth key rotation rather than assuming the endpoint discovers the new binding on its own.

#### Step 4 — Create and load the Toolkit config

These examples use the exact SDK revision prepared in Step 7. Toolkit JSON rejects an obsolete top-level `version` field and uses optional per-Tool `response_signing_key`. This RegisteredKey path requires that key for the exact served FQN so successful canonical result BCS carries a Tool signature.

Build the temporary `toolkit-config.json` with the exact FQN and private Tool key inside the runtime secret directory:

```bash
jq -n \
  --arg fqn "$TOOL_FQN" \
  --arg leaders_path "$leaders_path" \
  --rawfile key "$SECRET_DIR/tool-signing-key.hex" \
  '{
    invoke_max_body_bytes: 10485760,
    signed_http: {
      mode: "required",
      allowed_leaders_path: $leaders_path,
      tools: {
        ($fqn): {
          response_signing_key: ($key | rtrimstr("\n")),
          replay_cache_ttl_ms: 300000
        }
      }
    }
  }' > "$SECRET_DIR/toolkit-config.json"

jq -e '
  (has("version") | not) and
  .signed_http.mode == "required" and
  (.signed_http.tools[env.TOOL_FQN].response_signing_key | length) > 0
' "$SECRET_DIR/toolkit-config.json"
```

The equivalent literal schema is:

```json
{
  "invoke_max_body_bytes": 10485760,
  "signed_http": {
    "mode": "required",
    "allowed_leaders_path": "<runtime-config-dir>/allowed-leaders.json",
    "tools": {
      "xyz.taluslabs.exchanges.coinbase.spot-price@1": {
        "response_signing_key": "0000000000000000000000000000000000000000000000000000000000000000",
        "replay_cache_ttl_ms": 300000
      }
    }
  }
}
```

The zero key is test material only. In this demo, `toolkit-config.json` is temporary secret-bearing state: `umask 077` creates it mode `0600`, and the trap from Step 1 removes it with the key files when the shell exits or the Toolkit stops. If a persistent protected service config is required, move it to the operator’s mode-`0600` secret store, remove the demo copy, and rotate/delete it during service stop or key rotation; do not leave both copies. Never pass a private key on a command line, commit it, print it, or assume shell environment variables are absent from same-user process inspection. `invoke_max_body_bytes` defaults to 10 MiB. `signed_http.mode` is `disabled` or `required`; omission defaults that section to `required`, while omitting the section disables signed HTTP. Use either `allowed_leaders_path` or an inline `allowed_leaders` object with `{ "version": 1, "leaders": [...] }`. `tools` must be nonempty in required mode; a Tool entry may omit a response key for request-verification-only service, but this RegisteredKey result path requires a valid `response_signing_key`. `replay_cache_ttl_ms` is optional, must be positive, and controls completed entries.

There is no Tool KID field and no external replay-store field. Replay state is an internal process-local memory cache. In-flight nonce reuse returns `409`; an exact completed retry returns cached canonical bytes; the completed entry expires after the configured TTL.

Launch or restart the Tool with the config:

```bash
export NEXUS_TOOLKIT_CONFIG_PATH="$SECRET_DIR/toolkit-config.json"
cargo run
```

If the Toolkit terminates TLS directly, set both `NEXUS_TOOL_TLS_CERT_PATH` and `NEXUS_TOOL_TLS_KEY_PATH`. Otherwise, the gateway must forward the request headers `X-Nexus-Sig-V`, `X-Nexus-Leader-Id`, `X-Nexus-Leader-Key-Id`, `X-Nexus-Input-Hash`, `X-Nexus-Leader-Signature`, and `X-Nexus-Nonce`, plus response headers `X-Nexus-Sig-V` and `X-Nexus-Tool-Signature`. It must provide authenticated HTTPS to the Tool; signed HTTP does not make a plaintext hop safe.

#### Step 5 — Resolve the public Leader boundary

Public Nexus Docs do not publish Leader process configuration, bootstrap, private-key entry, or readiness commands. A RegisteredKey integration needs a public Leader capability identifier and active-key readback supplied by the selected hosted or test environment; retain those public identifiers with the Tool allowlist and verification evidence.

The selected build must prove that the Leader key binding is active before a RegisteredKey vertex can run. If the build does not expose the combined runtime and key-binding read surface, stop at the public Tool, Task, Occurrence, Execution, and transaction-effect evidence and record the capability as unavailable. Do not infer a working signature path from a successful local request or a screenshot.

#### Step 6 — Select RegisteredKey on the DAG vertex

Set `verifier` beside the off-chain vertex's `name` and `kind`:

```json
{
  "name": "price",
  "kind": {
    "variant": "off_chain",
    "tool_fqn": "xyz.taluslabs.exchanges.coinbase.spot-price@1"
  },
  "verifier": "registered_key",
  "entry_ports": [
    {
      "name": "currency_pair"
    }
  ]
}
```

Accepted spellings are `none`, `registered_key`, and `external`. The parser rejects a verifier field on an on-chain vertex. Validate the complete DAG before publishing:

```bash
nexus dag validate --path ./dag.json
```

Tool support and DAG mode are separate: the CLI/parser can accept the JSON while publication still fails if the registered Tool does not support the selected mode.

#### Step 7 — Prove unsigned and tampered evidence fails

With Toolkit `Required` running locally, an unsigned request must fail before Tool input decoding:

```bash
status="$(curl -sS \
  -o unsigned-response.json \
  -w '%{http_code}' \
  -H 'content-type: application/json' \
  --data '{"ports":[]}' \
  http://127.0.0.1:8080/invoke)"
test "$status" = 401
jq -e '.error == "auth_failed"' unsigned-response.json
```

Run the pinned v3 tamper test to prove missing, malformed, and wrong Tool signatures do not verify against altered evidence:

```bash
set -Eeuo pipefail
NEXUS_SDK_DIR="${NEXUS_SDK_DIR:-sdk-source}"
git clone https://github.com/Talus-Network/nexus-sdk.git "$NEXUS_SDK_DIR"
git -C "$NEXUS_SDK_DIR" checkout 45d397aafcfbeeeaf5032d5fb9fa5d99b3f36205
cd "$NEXUS_SDK_DIR"
cargo +stable test -p nexus-sdk --all-features \
  signed_http::v3::tests::response_verification_rejects_missing_malformed_and_wrong_signatures \
  -- --exact
```

This SDK test is reproducible cryptographic evidence, not a live on-chain verdict. Before production, perform a staging Execution whose Tool uses an intentionally unregistered signing key, retain the `Reject` verification event/result, restore the registered key, rerun the same canonical input, and retain the accepted workflow result. Never conduct the negative key test against production traffic.

#### Step 8 — Inspect the accepted path

Retain Tool, key, Task, Occurrence, and Execution readbacks:

```bash
nexus tool inspect --tool-fqn "$TOOL_FQN" --json
nexus tool auth list-keys --tool-fqn "$TOOL_FQN" --json
nexus task occurrence inspect --task-id <TASK_ID> --occurrence-id <OCCURRENCE_ID> --json
nexus execution inspect --task-id <TASK_ID> --occurrence-id <OCCURRENCE_ID> --json
```

For an accepted RegisteredKey result, record the on-chain verification event/state with the Tool/Leader active bindings, Tool FQN/ID, DAG hash, input hash, Execution ID, walk index, runtime vertex/iteration, transaction digest, and result. The production leader parses and carries the Tool signature; do not claim that its local logs are the verifier decision.

#### Step 9 — Use an External verifier when required

`External` is a separate Tool support configuration. The Tool owner registers one public verifier method, immutable witness, and shared-object list. Workflow then calls that method with the worksheet, `TaggedOutput`, auxiliary bytes, and registered objects, and consumes `VerificationVerdict` atomically.

```move
public fun verify(
  worksheet: &mut ProofOfUID,
  result: TaggedOutput,
  auxiliary: vector<u8>,
  witness: &DemoVerifier,
): VerificationVerdict {
  // Return Accept or Reject and stamp the supplied worksheet.
}
```

This fragment shows the call shape, not a deployment recipe. Configure the exact package/module/function and witness objects through the Tool verifier command, inspect the Tool’s External support record, and use `"verifier":"external"` on the off-chain vertex.

#### Verification checklist

* Tool key registration returns and readback agree on Tool ID, binding object ID, active key ID, and public key.
* `nexus tool inspect --json` reports `.verifier_support.kind == "registered_key"`.
* The allowlist contains the intended Leader capability ID and active key ID and has a recorded SHA-256.
* The Toolkit config parses, has no top-level version, uses Toolkit `required`, maps the exact FQN to `response_signing_key`, and contains no Tool KID or external replay-store field.
* The Tool launches through `NEXUS_TOOLKIT_CONFIG_PATH`, and the proxy/TLS path preserves v3 headers.
* The complete DAG validates with `"verifier":"registered_key"` on an off-chain vertex.
* Unsigned input, tampered signature/evidence, and unregistered Tool key all produce retained negative evidence.
* The restored key and identical canonical input produce an accepted on-chain workflow result.

#### Common failure modes

| Symptom                               | Likely cause and safe response                                                                                                                                                                                                                                                                                             |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `No OwnerCap<OverTool>`               | Restore the saved Tool record or pass the exact `--owner-cap` object ID.                                                                                                                                                                                                                                                   |
| Toolkit config fails at startup       | Remove any obsolete top-level version or unknown field; check a positive body/replay limit, one allowlist source, a nonempty Tools map, the exact FQN, and a valid 32-byte `response_signing_key` when Tool signatures are required.                                                                                       |
| Unsigned request is accepted          | The config path was not loaded, the `signed_http` section is absent, or mode is `disabled`. Stop before publishing the DAG.                                                                                                                                                                                                |
| Leader fails before invocation        | A RegisteredKey vertex selected a Leader whose capability/key-ID binding is missing, inactive, or absent from the local allowlist.                                                                                                                                                                                         |
| Tool returns `401 auth_failed`        | The Leader capability/key ID is absent from the allowlist, the active key rotated, a v3 header is malformed, or the canonical input hash does not verify.                                                                                                                                                                  |
| Tool returns `409 request_in_flight`  | The same deterministic nonce is still executing; let the leader retry after the in-flight request completes.                                                                                                                                                                                                               |
| Leader reports missing Tool signature | Toolkit is disabled, the FQN has no signing key, a gateway stripped response headers, or the response was a local JSON error rather than canonical result BCS. The missing/malformed stamp aborts before a verification result; repair the transport/signing path and resubmit rather than searching for a Reject verdict. |
| On-chain RegisteredKey rejects        | A valid stamped verdict carried an invalid active-key signature, input commitment, nonce context, Tool signature, or canonical response BCS. Inspect durable `VerifierDecision::Reject`/failure evidence; do not trust a local parse.                                                                                      |
| Accepted result remains pending       | Verification passed, but payment/ToolCashier/Occurrence settlement is a separate lifecycle. Inspect locks and settlement.                                                                                                                                                                                                  |

#### Next guides

* [Build an off-chain Tool](/talus-docs-v2.1.0/guides/tool-development/build-offchain-tool.md) — implement and deploy the Tool runtime.
* [Execute and settle an Agent](/talus-docs-v2.1.0/guides/agent-usage/execute-and-settle-agent.md) — run and settle the verified vertex.
* [DAG execution diagnosis](/talus-docs-v2.1.0/concepts/05-workflow-dag-execution.md#diagnosing-a-stuck-or-failed-run) — diagnose leader readiness and transport failures.


---

# 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/talus-docs-v2.1.0/guides/tool-development/verify-offchain-tool-result.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.
