Skip to content

Repository files navigation

Clawpay — a Solana payment terminal in your shop's Telegram

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

From zero to your first payment

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-wasip2

2. 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 PATH

3. 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.sh

6. 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 code

Send /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.

What's underneath

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.

Repository layout & the shared core

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.

Design rules (uniform across the repo)

  • 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 test runs 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.

Build everything

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.)

What fought us on wasm32-wasip2 (Track E notes)

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, and curve25519-dalek (serial backend, default features off) all compile for wasm32-wasip2 without 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.
  • waki works as advertised for blocking HTTP inside execute, with the version-skew caveats the ZeroClaw docs describe (vendored wit-bindgen 0.34 next to our 0.46; wasi:http@0.2.4 imports 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, so solana-pay-request ships with no network imports at all — the manifest permission (config_read only) 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.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages