feat(plugins): PixZClaw — dual-rail BRL PIX + Solana Pay USDC invoicing (T0/T1)#123
Open
capitv wants to merge 3 commits into
Open
feat(plugins): PixZClaw — dual-rail BRL PIX + Solana Pay USDC invoicing (T0/T1)#123capitv wants to merge 3 commits into
capitv wants to merge 3 commits into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).
brl-usdc-invoiceinvoice_idinvoice-statuspixzclaw-briefIt 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.bs58(sha256("zc-inv-v1" || invoice_id || "|" || merchant)[0..32]). Same invoice, same address, no database.getSignaturesForAddressfinds candidate transactions.getTransactionreadsmeta.preTokenBalances/meta.postTokenBalances, filtered bymint == usdc_mint && owner == merchant, and computes the net delta credited to the merchant. This coverstransferandtransferCheckedwithout parsing instructions, and handles an ATA created inside the same transaction.If the RPC cannot return a transaction, the tool degrades to
signature seen, amount unverifiedand 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 999999999is refused by the cap; amerchant_overrideis ignored whilerecipient_lockedis 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 vialog-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 injectedHttpTransport, output shaping) is vendored into each plugin undervendor/, so every plugin directory builds standalone from the CI snapshot. Nosolana-sdk, nosolana-client.cargo test --lockedin each plugincargo clippy --all-targets -- -D warningsclean, host andwasm32-wasip2wasm32-wasip2against the vendoredwit/v0config_readon all three, plushttp_clienton the two that read RPCRunning 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.