Skip to content

Solana Pay · durable nonce · ZeroClaw

An agent’s payment dies in ninety seconds. The person approving it is serving a table.

That is why agent payment designs quietly drop the human. A Solana blockhash expires while the approval sits unread, so the transaction is rubbish by the time anyone answers. Aval anchors it to a durable nonce instead, and the request waits as long as the shop does.

the owner’s channel · replay of a recorded run

The gate is not the problem. The clock behind it is.

Ordinary blockhash

Signed at midday, queued for approval, answered four and a half hours later. By then the hash was long dead.

$ solana confirm -v 4kR8...control
  Result: Error
  Hash has expired

Durable nonce

Same wait, same moment of signing. The nonce keeps its value in an account, so the transaction is still good whenever the answer comes.

$ solana confirm -v 5U2c5RV3...QZnj7
  Result: Finalized
  AdvanceNonceAccount (instruction 0)

Devnet, checkable by anyone

The receiving wallet was never funded by an airdrop.

It was generated empty and left alone. Its balance came from that one transfer and from nothing else, which is the difference between a proof and a screenshot.

4h 29m

between signing and landing

0.01 SOL

the whole balance, from that transfer

9.9 h

daemon uptime, unattended, at last check

signature   5U2c5RV3TEurNYFeC7r17jK5CWkRgMzwEsxA978M9DLL
            Yhjx38JmUkK1yVG5qiXVqo9z2XPRbesXfgNeuBbQZnj7

nonce advanced after use — the same transaction cannot land twice
  7tfvBRBMvnitJJ9tCN9BZLADhaPuGgJ4m4KwguTCYr9E  →  A8zkZeDL...

The limits live in Rust. The model never sees a field it could raise.

A ceiling written into a prompt is a suggestion. This one is read from the operator’s own config inside the sandbox, and it is not among the arguments the model can pass, so no message arriving in the shop can move it.

T0

Reading

Balances, signatures, whether a reference key has been used. The worst case is a wrong answer, and the operator holds an RPC key at most.

settlement-poll runs here.

T1

Building

An unsigned transaction anchored to a durable nonce. It can sit in a queue for hours without going stale. A person signs it, or nobody does.

Refunds run here.

T2

Signing

Holding a key and submitting. Aval does this on no path at all, so there is no key to steal and no session wallet to drain.

Deliberately unused.

# the component's entire permission grant
permissions = ["config_read"]     # no http_client

# and what came back when a charge went over the ceiling
amount 5000 is over the shop's per-charge ceiling of 500

Published because it is the useful part

Four times, the model reported work it had not done.

None of it was fixed by better wording. Every fix was the same move: take the decision off the model and put it in code that cannot be talked round.

  1. 01

    Invented an endpoint to mint a key

    Asked for a reference key with no tool that could make one, it reached for HTTP and produced example.com/generate-reference-key.

    Key generation moved into a Rust component that has no network permission at all.

  2. 02

    Invented an endpoint to check a token

    Same shape, different sentence: api.example.com/shop/token/verify. Neither address exists.

    The allowlist is compared inside the component, against the operator’s own config.

  3. 03

    “Table 4 has been charged”

    Confident, formatted, and behind it native_tool_calls: 0. No charge existed.

    The till reads the tool result, never the sentence. No URL means no charge.

  4. 04

    Wrote a fake tool result under attack

    An injected message asked for a refund elsewhere. The model composed a <tool_result> block as prose to make it look finished.

    Nothing ran. The counter read zero, and a refund destination is read from the paying transaction.

Six settings that start the daemon cleanly and do nothing at all.

Each of these cost an evening. They are written down so the next operator loses an hour instead.

schema_version missing

The whole provider block is discarded. No warning anywhere.

[channels.telegram]

Must be [channels.telegram.default] under v3. Binds nothing otherwise.

allowed_users

A v2 field. It takes the sender allowlist with it when it goes.

execution_mode

The key is default_execution_mode. The wrong one is accepted in silence.

deterministic mode

Runs headless with no agent turn, so ordinary steps fail closed.

wit/v0 drift

One enum variant apart. Compiles, installs, lists, then refuses to instantiate.

Set it up in an evening, on your own machine.

Plugins are not in the release binary, so the host is built from source. After that it is three procedures, one skill and one config file.

cargo build --release --features plugins-wasm-cranelift
zeroclaw --config-dir ./runtime sop validate
zeroclaw --config-dir ./runtime daemon

curl http://127.0.0.1:42617/health     # the daemon answers for itself

The terminal in the hero replays a recorded run rather than holding a live connection. The daemon runs on the operator’s own machine, which is the point of the thing, and a public page cannot reach it.