A self-hosted ZeroClaw agent that takes payments over chat: "charge table 4, 2.5 USDC, invoice 502" → scannable QR in the thread → customer approves in their own wallet → "✅ Invoice 502 paid" posted automatically, confirmed from the chain by deterministic code — never by the model. Running today, on real mainnet payments.
▶ The showcase write-up (what/who/threat model/evidence): SHOWCASE.md
The whole path, in order. Budget an evening. You need: a Linux box or Windows-with-WSL, a phone with Telegram and a Solana wallet (Phantom works), and ~$5 of SOL for network fees.
1. Toolchain (~10 min)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # install Rust
rustup target add wasm32-wasip22. Build the ZeroClaw host (~20 min of compiling — the released binary does not include the plugin host, so build from source):
git clone https://github.com/zeroclaw-labs/zeroclaw && cd zeroclaw
cargo build --release --features plugins-wasm,plugins-wasm-cranelift
export PATH="$PWD/target/release:$PATH" # or copy the binary onto your PATH3. Create your Telegram bot (2 min): message @BotFather in Telegram →
/newbot → pick a name → save the token it gives you.
4. Set up the shop wallet (5 min): create a fresh wallet in Phantom — its address goes in the config; its key never touches this system. Three gotchas that bite everyone (details in SHOWCASE.md → operator notes):
- send the shop wallet a dust amount of each token you'll accept (e.g. $0.10 USDC) — the Solana Pay spec forbids customer wallets from creating its token account for it;
- any wallet that pays needs ~0.01 SOL for fees;
- "transaction simulation failed" usually means a wallet is at the ~0.0009 SOL rent floor, not a bug.
5. Build and install the plugins (5 min, from this repo's root):
WALLET=<your-shop-wallet> RPC_URL=<your-rpc-url> ./quickstart.sh6. Runtime config (5 min):
cp deploy/config.example.toml ~/.zeroclaw/config.toml # fill the <PLACEHOLDERS>:
# bot token, model API key,
# shop wallet, RPC key
mkdir -p ~/.zeroclaw/agents/venice/workspace
cp deploy/IDENTITY.md deploy/payment-watch.py ~/.zeroclaw/agents/venice/workspace/
# edit the CLAWPAY_* constants at the top of payment-watch.py (or set them as env)Any OpenAI-compatible model endpoint works (we run Venice); paste its key in
the config. Validate with zeroclaw config list — errors print loudly.
7. Launch and pair (2 min):
zeroclaw daemon # prints a one-time bind codeSend /bind <code> to your bot in Telegram. Pairing persists.
8. Your first sale. In the bot chat:
charge table 4, 1 USDC, invoice 1
→ QR appears → customer scans it with their wallet's main scanner (the QR icon on the home screen — not the Send screen, which drops the amount) → they approve → within a minute the bot posts ✅ Invoice 1 paid — solscan.io/tx/…, confirmed from the chain by the watcher, never by the model.
Stuck? USING.md has the deep walkthrough and a troubleshooting table; deploy/README.md documents each runtime file.
Two wasm32-wasip2 tool plugins and the shared core crate
they're built on, giving any ZeroClaw agent safe hands on Solana. All custody
tiers are T0/T1: no private key exists anywhere in this repository, and
nothing here can submit a transaction.
| component | tier | permissions | what it does |
|---|---|---|---|
crates/solana-wasi-core |
— | — | The substrate (Track E): base58/PDAs, v0 tx encoding, JSON-RPC over a pluggable transport, SPL instruction builders, Token-2022 mint parsing, durable nonces, Solana Pay URLs. Pure Rust, solana-sdk-free, host-testable. |
plugins/token-risk-check |
T0 | http_client, config_read |
RED/AMBER/GREEN verdict on any mint: authorities, Token-2022 extensions (permanent delegate, hooks, fees, frozen-by-default), holder concentration. |
plugins/solana-pay-request |
T1 | config_read only — no network |
"Charge table 4 for 25 USDC" → QR-ready Solana Pay URL. Operator-pinned recipient, mint allowlist, amount caps. |
The tools compose: risk-check vets a mint, pay-request invoices in it — and every guardrail is compiled Rust inside the sandbox, not prompt text. The terminal is strictly request-and-verify: it cannot construct or send an outbound transaction in any form, so "refund to my wallet"-style prompt injection has nothing to grab.
crates/solana-wasi-core is the canonical Track E crate, with its own
49-test suite. Upstream CI (tools/ci/validate_components.sh) builds each
plugin from a snapshot of only plugins/<name>/ plus wit/v0 — it does
not copy crates/ — so a repo-relative path dependency would fail CI and no
upstream plugin uses one. Each plugin is therefore self-contained: the
core is vendored verbatim into plugins/<name>/solana-wasi-core/ and used via
a local path dependency, so the plugin directory builds in isolation exactly
as CI runs it. dev/sync-core.sh (and .ps1) refresh the
copies from the canonical crate; when solana-wasi-core is published to
crates.io, the vendored dirs are deleted and each plugin's dependency becomes
solana-wasi-core = "0.1" — a one-line change. The copies are byte-identical
to crates/solana-wasi-core/src, so there is one source of truth.
- Pure core, thin shim. Every plugin's logic is a plain Rust module;
the
#[cfg(target_family = "wasm")]component shim only parses args and forwards.cargo testruns everything on the host with mocked RPC — no wasm toolchain, no network. - Fail closed. Policy violations (redirected recipients, unlisted mints, over-cap amounts, off-curve "wallets", authority mismatches) return refusals, never partial results. Each plugin README carries a prompt-injection transcript, and the refusals in it are pinned as tests.
- Config is the control plane. Operators pin recipients, allowlists,
caps, and RPC URLs in the plugin's jailed config section; none of these
are model-reachable (schemas are
deny_unknown_fields). - Shaped output. Tool results are ~150–200 tokens of prose. Raw RPC never leaves a plugin; tests pin the budget.
- Live-verified wire formats. ATA derivation matched 8/8 real PYUSD holders and 3/3 USDC accounts on mainnet; mint fixtures are live snapshots (PYUSD's full Token-2022 TLV table); shortvec vectors match the runtime's. See dev/verify_vectors.md.
One-command path: edit the config values at the top of
quickstart.sh (your wallet, RPC URL, caps), then run it from
the repo root — it builds, installs, and configures both plugins on a
plugin-enabled zeroclaw host. The manual steps are below.
rustup target add wasm32-wasip2
# Each plugin is self-contained (vendored core) and builds in isolation,
# exactly as upstream CI runs it.
for p in plugins/*/; do
(cd "$p" && cargo test --locked && cargo build --locked --target wasm32-wasip2 --release)
done
# The canonical Track E crate, with its own test suite:
(cd crates/solana-wasi-core && cargo test --locked)Install a plugin by copying its target/wasm32-wasip2/release/*.wasm next to
its manifest.toml under your ZeroClaw plugins directory, then configure:
# in ~/.zeroclaw/config.toml — settings live in the NESTED `config` map
# (flat keys on the entry are silently ignored); quickstart.sh writes this.
[[plugins.entries]]
name = "solana-pay-request"
config = { recipient = "<your wallet>", allowed_mints = "SOL,EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", max_amount = "100" }(Requires a host built with --features plugins-wasm,plugins-wasm-cranelift.)
The bounty warned that solana-sdk/solana-client don't fit inside a WIT
component, and that's correct — so solana-wasi-core reimplements the thin
slice that matters. What we learned:
- The crypto stack is fine.
bs58,sha2,base64,serde_json, andcurve25519-dalek(serial backend, default features off) all compile forwasm32-wasip2without patching. The off-curve check for PDA derivation is non-negotiable: ~half of all 32-byte strings are valid curve points, and skipping the check derives a wrong ATA about half the time. wakiworks as advertised for blocking HTTP insideexecute, with the version-skew caveats the ZeroClaw docs describe (vendored wit-bindgen 0.34 next to our 0.46;wasi:http@0.2.4imports linking against a 0.2.6 host).- Keep wasi:http out of components that don't need it. The waki transport
is a cargo feature (
http) on a wasm-only target dependency, sosolana-pay-requestships with no network imports at all — the manifest permission (config_readonly) and the component's import list agree. - Transactions are just bytes. Compact-u16, the v0 message layout, SPL instruction tags, ATA derivation, and nonce-account layouts are small, stable formats; hand-encoding them with byte-exact tests is less code than fighting a 300-crate dependency tree, and the resulting components are ~200–400 KB.
- Statelessness is a feature. ZeroClaw tool plugins get a fresh store per call, so per-call caps live in the plugin and velocity/daily limits belong to the host approval gate. The READMEs say this explicitly instead of pretending an in-memory counter would survive.
MIT.