fix(spend): unblock SpendRecord persist + clean tool result (solders typing tighten)#21
Merged
Conversation
added 2 commits
May 10, 2026 02:53
…statuses
`solana-py >= 0.34` / `solders` strictly types `get_signature_statuses(...)` —
it now rejects a `List[str]` with `TypeError: argument signatures: str object
cannot be converted to Signature`. The on-chain spend itself succeeds
(send_raw_transaction returns a finalized tx), but the post-send confirm
loop blew up trying to convert the signature, so the SpendRecord never
got persisted in Postgres and the MCP tool returned an error to the
caller despite the chain having moved USDC.
Verified live against the production devnet endpoint (vault
9d36b255-..., tx
2gPpot9Gcx7wPQ6u7uiqyLEjCrEmtLd7HqPooE4qsarmseTuuxMm35AfYovi5cZ8qFGGjgBvtH1E2WWLo1Yyf6aE):
"TypeError(\"argument signatures: str object cannot be converted to Signature\")"
Wrap on entry to `_confirm` with `Signature.from_string(signature)`. Caller
contract unchanged (still passes a `str`).
`solders.transaction_status.TransactionConfirmationStatus` is not hashable,
so `s.confirmation_status in {"confirmed","finalized"}` raises TypeError.
The on-chain spend already landed by the time we hit this; the bug only
prevents the SpendRecord from being persisted and a successful response
from being returned to the caller.
Verified live with the second devnet spend, vault balance 3.99 -> 3.98:
the chain side is fine, only the post-confirm path was busted.
Coerce via `str(s.confirmation_status).lower().rsplit(".", 1)[-1]` so it
works against both enum values like
TransactionConfirmationStatus.Finalized and plain strings (older solders).
Germey
approved these changes
May 10, 2026
acedatacloud-dev
added a commit
that referenced
this pull request
May 10, 2026
…pay_for_api caveat (#22) Adds a "Live on devnet" badge + a quoted callout near the top with the real 2026-05-10 verification result (3 spends, vault 4.00 -> 3.97 USDC, finalized tx 249u8Pion...3y3D on Solscan). The customer who reported "MCP could not be loaded" can now skim the top of the README, click the Solscan link to confirm the on-chain side is live, and run the curl / demo recipe to confirm their own MCP URL is healthy without any Claude / Cursor / SDK plumbing. Concrete changes: - "60-second verification" section near the top: 3 steps, all `curl` + `python scripts/demo.py`. End-state explicitly: "If steps 1-2 work, any `MCP could not be loaded` you see in Claude Desktop is a client-side problem". - Spelled out the `aceguard_spend` request/response shape with a real finalized tx as the canonical example. Added the `recipient ATA must exist on devnet` pre-req inline (Anchor 3012), with the one-line `spl-token create-account` command to satisfy it. - Pivoted Step 5 of the walkthrough from `pay_for_api` to `aceguard_spend`. Reason: api.acedata.cloud issues mainnet x402 quotes (`EPjFWdd5...` mint, `5iVXFr...` payTo); the production x402guard deploy is on devnet, so the recipient ATA the on-chain program expects does not exist on this cluster. This is *expected* per .plans/X402GUARD.md and called out clearly so customers do not burn an afternoon trying to make that path work pre-mainnet flip. - Updated Step 6 (boundary-in-action prompts) to use `aceguard_spend` invocations that map to actual Anchor errors today, instead of the pre-existing `pay_for_api` examples that no longer fire. Pairs with #18 / #19 / #20 / #21. The mainnet flip stays the V2 step .plans/X402GUARD.md already calls out (#11 / "Why devnet, not mainnet"). Co-authored-by: acedata-bot <bot@acedata.cloud>
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.
Summary
Two bugs in
_confirm()(the post-send_raw_transactionconfirmationloop in
api/core/spend_executor.py) caused every successful on-chainspend to fall over before persisting the
SpendRecordand returning aresult to the MCP caller. The chain side was actually fine — both bugs
hit after
send_raw_transactionreturned a tx signature, so USDCmoved on-chain but the DB never saw it, the agent saw a
tool exceptionerror, and the next spend attempt would fail with
NonceReplaybecausethe in-DB nonce counter never advanced past the on-chain
last_nonce.Two bugs, both modern-
solderstyping tighteningBug 1 —
Signaturenot strsolana-py >= 0.34/soldersno longer accepts the base58 string here,needs an actual
Signatureobject.Fix: wrap on entry:
Signature.from_string(signature).Bug 2 —
TransactionConfirmationStatusenum not hashableThe enum value can't be looked up in a
setof strings, even when itsstring repr matches. (Older solders returned a plain string here, which
silently worked.)
Fix: coerce to lowercase short-name string before membership check:
str(s.confirmation_status).lower().rsplit(".", 1)[-1]— works againstboth
TransactionConfirmationStatus.Finalized(enum) and"finalized"(plain string from older versions).
Live verification
Three real on-chain spends from devnet vault
9d36b255-0efd-4b12-9d74-304cdc0c0e0d(allow onlyapi.acedata.cloud,1 USDC daily, 0.1 USDC per-call):
2gPpot9G...Yyf6aE249u8Pion...3y3DisError: false, returns tx + nonce + Solscan, USDC 3.98 → 3.97After the fix,
aceguard_historyshows the spend,aceguard_balancereturns
balance_usdc=3.97 spent_today_usdc=0.02 daily_remaining_usdc=0.98,and the on-chain vault ATA balance matches the DB exactly.
The first two spends are real on-chain transfers that recorded
nonce=1and
nonce=2in the program'sPolicyaccount but didn't persist aSpendRecord — that's an operational issue, not a code bug, and is out
of scope here. Future work could add a
_confirmouter try/except thatinserts a SpendRecord even if the post-confirm path explodes, so the
chain and DB never diverge again.
Verification
uvx ruff check api/core/spend_executor.py— cleanhttps://x402guard.acedata.cloud/mcp/<TOKEN>—third spend returned cleanly with finalized tx + Solscan URL
aceguard_historynow lists the spend with full metadataaceguard_balancedaily counter ticked up correctlyPairs with #18 (
scheme:"exact") and #19 (CLI demo + README rewrite)to make the full chain
MCP -> aceguard_spend -> on-chain Anchor program -> SpendRecord persisted -> tool result returnedwork end-to-end onthe live
x402guard.acedata.clouddevnet instance.