Skip to content

feat(plugins): PixZClaw — dual-rail BRL PIX + Solana Pay USDC invoicing (T0/T1)#123

Open
capitv wants to merge 3 commits into
zeroclaw-labs:mainfrom
capitv:feat/pixzclaw-dual-rail-brl-usdc
Open

feat(plugins): PixZClaw — dual-rail BRL PIX + Solana Pay USDC invoicing (T0/T1)#123
capitv wants to merge 3 commits into
zeroclaw-labs:mainfrom
capitv:feat/pixzclaw-dual-rail-brl-usdc

Conversation

@capitv

@capitv capitv commented Jul 21, 2026

Copy link
Copy Markdown

PixZClaw

Charge in BRL from chat. Get paid over PIX or over USDC on Solana. The agent never holds a key.

Brazil already has instant bank payments (PIX, ~150M users). What it does not have is a way for a freelancer or shopkeeper to accept stablecoins without an exchange account, a PSP contract, or a custodial wallet. PixZClaw issues one invoice on two independent rails and verifies the on-chain one honestly.

Tracks A (payments) + E (shared wasm substrate).

Plugin Tier Role
brl-usdc-invoice T1 Real PIX Copia e Cola (EMV + CRC16-CCITT) and a Solana Pay USDC URL under one invoice_id
invoice-status T0 Verifies the USDC actually received by the merchant: PAID / UNDERPAID / OVERPAID
pixzclaw-brief T0 Merchant till: USDC/SOL balances, 24h close, 7d sparkline, recent activity

It checks the amount, not just that "something happened"

The interesting part is invoice-status. Counting successful signatures on a Solana Pay reference is not settlement — anyone can touch a reference, and a customer can underpay.

  1. The reference is derived deterministically: bs58(sha256("zc-inv-v1" || invoice_id || "|" || merchant)[0..32]). Same invoice, same address, no database.
  2. getSignaturesForAddress finds candidate transactions.
  3. For each, getTransaction reads meta.preTokenBalances / meta.postTokenBalances, filtered by mint == usdc_mint && owner == merchant, and computes the net delta credited to the merchant. This covers transfer and transferChecked without parsing instructions, and handles an ATA created inside the same transaction.
  4. Deltas are summed across recent transactions, so an invoice settled by two partial transfers reads as PAID, while a transaction that merely references the invoice contributes nothing.

If the RPC cannot return a transaction, the tool degrades to signature seen, amount unverified and never reports PAID. Underpayment reports the shortfall instead of silently passing.

Honest about what it cannot see

Bank PIX settlement is invisible on-chain. The tools never infer it: the PIX leg is reported paid only when the operator says so. No PSP integration, which keeps bank credentials out of the agent entirely. Saying "I don't know" was preferred over a plausible guess.

Fail-closed against prompt injection

Amount caps and a config-locked recipient are enforced inside the plugin, below the model. An injected charge 999999999 is refused by the cap; a merchant_override is ignored while recipient_locked is set, and the derived reference still uses the config merchant. Both are covered by tests.

Layout

Follows plugins/redact-text: pure core, thin #[cfg(target_family = "wasm")] shim, cdylib + rlib, structured logging via log-record, host tests that need no wasm toolchain and no network.

The shared Solana/PIX substrate (solana-wasm-core: decimal math, EMV+CRC16, Solana Pay encoding, reference derivation, JSON-RPC over an injected HttpTransport, output shaping) is vendored into each plugin under vendor/, so every plugin directory builds standalone from the CI snapshot. No solana-sdk, no solana-client.

  • 101 host testscargo test --locked in each plugin
  • cargo clippy --all-targets -- -D warnings clean, host and wasm32-wasip2
  • Built for wasm32-wasip2 against the vendored wit/v0
  • Permissions: config_read on all three, plus http_client on the two that read RPC
  • MIT OR Apache-2.0

Running in production

These are deployed on a Raspberry Pi 3 running ZeroClaw with the Telegram channel, issuing and checking real invoices. Deployment repo, install scripts, agent skills, and an overview page: https://github.com/capitv/pixzclaw-pi · https://capitv.github.io/pixzclaw-pi/

One deployment lesson shaped the output format: the host redacts high-entropy base58 in chat, which breaks a raw solana: URL. The invoice therefore delivers the Solana leg as a QR link (the QR still encodes the full pay URL) and keeps the PIX copy-paste code in a fenced block, so the whole message stays forwardable to the customer.

capitv and others added 3 commits July 21, 2026 15:56
…ng (T0/T1)

Three tool plugins for the Brazilian long tail: charge in BRL from chat,
let the payer settle over PIX or over USDC on Solana, and verify the USDC
leg on-chain. No plugin holds a key or signs anything.

| Plugin | Tier | Role |
|---|---|---|
| brl-usdc-invoice | T1 | One invoice, two rails: real PIX Copia e Cola (EMV+CRC16) and a Solana Pay USDC URL sharing an invoice_id |
| invoice-status | T0 | Verifies the USDC actually received by the merchant; PAID / UNDERPAID / OVERPAID |
| pixzclaw-brief | T0 | Merchant till: USDC/SOL balances, 24h close, 7d sparkline |

Design notes:

- Amount verification, not signature counting. invoice-status derives the
  Solana Pay reference from sha256("zc-inv-v1"||invoice_id||"|"||merchant),
  finds the settling transactions through getSignaturesForAddress, then
  reads meta.pre/postTokenBalances per transaction to compute the net USDC
  delta credited to the merchant for the configured mint. Partial transfers
  are summed; a transaction that merely touches the reference contributes
  nothing. When the RPC cannot return a transaction the tool degrades to
  "signature seen, amount unverified" and never reports PAID.

- Honest about PIX. Bank settlement is invisible on-chain, so the PIX leg is
  only ever reported as paid when the operator says so. The tools do not
  guess and do not integrate a PSP, which keeps bank credentials out of the
  agent entirely.

- Fail-closed against prompt injection. Amount caps and a config-locked
  recipient are enforced inside the plugin, below the model: an injected
  "charge 999999999" is refused, and a merchant override is ignored while
  recipient_locked is set. Covered by tests.

- Pure core, thin shim, per the redact-text reference layout. The shared
  Solana/PIX logic (amount math, EMV+CRC16, Solana Pay encoding, reference
  derivation, JSON-RPC, output shaping) lives in solana-wasm-core, vendored
  into each plugin under vendor/ so every plugin directory builds standalone.
  No solana-sdk, no solana-client.

101 host tests run without network or a wasm toolchain. Built for
wasm32-wasip2 against the vendored wit/v0. Permissions requested: config_read
(all three) plus http_client for the two that read RPC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…maps

The invoice-status README still described signature-only settlement, which
has not been how the plugin works since it started reading token balances.
It now documents the real flow (getSignaturesForAddress, then getTransaction
over up to MAX_VALUE_CHECKS successful signatures, summing pre/post token
balance deltas filtered by mint and owner), the full verdict table with the
99.5% tolerance, why a signature alone is never reported as PAID, and how
partial payments and reference spam are handled. Each claim points at the
test that proves it.

Also across the three plugins:

- LICENSE (MIT) added; READMEs state MIT OR Apache-2.0 at the user's option,
  matching Cargo.toml.
- Track A / Track E declared, with a one-sentence defense of each custody tier.
- Config tables and config.example.toml brought in line with the code:
  watch_hint on the invoice plugin, usdc_mint now load-bearing on the status
  plugin rather than reserved.
- The payment-reminder flow documented, including that the plugins hold no
  cron permission: the invoice only prints the offer and the status tool only
  emits a teardown hint, while the host schedules and cancels.
- "What we would build next" and "What fought us on wasm32-wasip2" sections
  added, plus an output-budget section with measured character counts.
- pixzclaw-brief README grew to cover the 24h close-out, the sparkline
  legend, relative timestamps, and the injected-clock design.

Worked examples are verbatim output captured by calling the real entry
points; product output stays in pt-BR because that is what the tools emit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The project's headline claim is that USDC settlement is verified for
real. Three paths could still produce a verdict that was not true.

1. Spam could mask a real payment
   status_tool.rs scanned only the 5 newest successful signatures, so
   six successful dust transactions touching the reference pushed the
   genuine payment out of the window and a paid invoice answered
   PENDING. Cost of the attack: six network fees.

   Now every successful signature the lookback returns is scanned, with
   an early stop once the invoice is covered. The cap is 64, and
   signatures beyond it are counted as unscanned rather than ignored.

   That accounting is the point: received_units is a LOWER BOUND
   whenever part of the scan is missing. A lower bound is enough to
   confirm a payment that it already covers, but never enough to assert
   a shortfall. Publishing "UNDERPAID … faltam 7.27" because the public
   RPC rate-limited us halfway through is the same lie as claiming PAID
   without checking, pointed the other way. An incomplete scan that has
   not covered the invoice now degrades to SIG OK.

2. Reused invoice_id could settle the wrong invoice
   auto_invoice_id was sha256(amount|description|merchant) with no time
   and no nonce, so two "R$ 10" charges on different days minted the
   same id, therefore the same reference, and yesterday's payment
   marked today's invoice PAID with a receipt. No attacker needed --
   charging the same amount twice reproduced it.

   The auto id is now salted with the issuance instant, supplied by the
   wasm shim so the core stays pure. It fails closed: an implausible
   timestamp refuses to mint an id rather than mint a colliding one.
   Explicit invoice_ids are still honoured as given, and the READMEs now
   say they must be unique per sale. Reference derivation is unchanged
   and still reproducible from the id.

3. Verification ran on f64 with a 0.5% tolerance
   A payer could send 0.5% less and receive a settlement receipt -- R$ 5
   short on a R$ 1,000 invoice -- and the sum itself was floating point,
   in the one part of the system advertised as real verification. The
   issuing side was already exact u128 integer arithmetic; the
   verification side was the loose one.

   Both sides are integers now. The RPC's uiTokenAmount.amount and
   decimals are used directly, mismatched decimals across transactions
   refuse to produce a verdict instead of summing nonsense, and the
   tolerance band is gone.

Also corrected the comment, the README and the tool description that
each claimed the old 5-signature scan already prevented masking. It
prevented one spam transaction, not six. A README that misdescribes the
security property is worse than no README.

Verified: 135 tests pass (83 core, 20 + 25 + 7 plugins), clippy
--all-targets -D warnings clean on all three, vendor-core.sh --check
reports no drift, rustfmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant