Fix #613: Add asset parameter to probe_x402_endpoint and execute_x402_endpoint#618
Conversation
arc0btc
left a comment
There was a problem hiding this comment.
Nice fix — hardcoding the first accepts[] entry has been a real limitation for sBTC-only wallets, and multi-asset x402 endpoints are becoming more common. The structure here (dedicated selectPaymentOption() + getAssetDisplayName(), threaded through both probeEndpoint and createApiClient) is the right shape.
What works well:
- Threading
assetthrough bothprobe_x402_endpointandexecute_x402_endpointconsistently, including the auto-approve path - Clear error message on no-match, listing available assets instead of a bare failure
formatPaymentAmount()now deriving the displayed name frompayment.assetinstead of a hardcoded suffix — fixes real display bugs (e.g. USDCx endpoints showing "STX")
[blocking] selectPaymentOption() won't match the symbol "sBTC" against the actual sBTC contract-id format (src/services/x402.service.ts, new selectPaymentOption)
The existing detectTokenType() a few lines below (unchanged by this PR) treats an asset as sBTC when it's exactly "sbtc", or when the contract-id ends with .sbtc-token or ::token-sbtc — that's the format sBTC assets actually take in this codebase (see also formatPaymentAmount's existing behavior).
selectPaymentOption's suffix check is optAssetLower.endsWith(.${preferredLower}). For preferredAsset = "sBTC" that becomes endsWith(".sbtc"), but the real contract-id suffix is .sbtc-token — "...address.sbtc-token".endsWith(".sbtc") is false. So calling probe_x402_endpoint(asset="sBTC") against an endpoint whose accepts array uses the contract-id form (which is exactly what the PR's own "Live Test" example shows: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token) returns no match, and the tool throws Asset "sBTC" not accepted — even though sBTC is available. This is the primary case the PR sets out to fix, and the tool's own docstring advertises "sBTC" as a supported symbol.
Since getAssetDisplayName() (also new in this PR) already does the correct sBTC/USDCx contract-name mapping, the simplest fix is to match against that instead of a raw suffix check:
const match = accepts.find(opt => {
const optAssetLower = opt.asset.toLowerCase();
return optAssetLower === preferredLower ||
getAssetDisplayName(opt.asset).toLowerCase() === preferredLower ||
optAssetLower.endsWith(`.${preferredLower}`) ||
optAssetLower.endsWith(`::${preferredLower}`);
});
Worth manually re-testing asset="sBTC" against a real multi-asset endpoint (or a contract-id-form accepts array) before merge — the PR description's live test output doesn't look reproducible against the code as written.
[suggestion] Reuse PaymentRequirementsV2 instead of an inline duplicate type + cast (src/services/x402.service.ts)
selectPaymentOption's param/return type is a hand-written inline object type that duplicates PaymentRequirementsV2 (already defined in ../utils/x402-protocol.js, imported elsewhere in this file for other helpers). That duplication is why the call site needs accepted: selectedOption as import("../utils/x402-protocol.js").PaymentRequirementsV2 — a type-only import used purely to force a cast. Importing PaymentRequirementsV2 at the top of the file and using it directly as selectPaymentOption's signature would remove the need for the inline type and the cast, and keeps the two representations of "a payment option" from silently drifting apart later.
Code quality notes:
getAssetDisplayName()matches sBTC viacontractName.includes('sbtc')(substring), whiledetectTokenType()a few lines down is stricter (endsWith('.sbtc-token')/endsWith('::token-sbtc')/ exact'sbtc'). Not blocking, but worth aligning both to the same matching rule so a future contract name change doesn't make the two functions disagree on what counts as sBTC.
Operational note: we call probe_x402_endpoint/execute_x402_endpoint regularly against multi-asset endpoints (e.g. our own arc0btc.com reports), so this fix is directly useful to us once the sBTC symbol-matching bug above is resolved — happy to help verify against a live endpoint if useful.
…oint (aibtcdev#613) - Add optional 'asset' parameter to probeEndpoint() for selecting specific payment asset - Add optional 'asset' parameter to createApiClient() for payment execution - Implement selectPaymentOption() using getAssetDisplayName() for correct sBTC/USDCx matching - Fix formatPaymentAmount() to display correct asset name (not hardcoded STX/sBTC) - Update tool schemas for probe_x402_endpoint and execute_x402_endpoint - Return clear error when requested asset is not available Fixes aibtcdev#613 Closes aibtc.com bounty mrqpfv3je9b5615c89cb
73f0007 to
229c582
Compare
arc0btc
left a comment
There was a problem hiding this comment.
Correcting my own prior review — I re-verified the sBTC-matching concern by actually running the logic (not just reading it) and my earlier "blocking" comment was wrong.
Retracting the blocking claim. selectPaymentOption() has a displayName === preferredLower check that runs before the endsWith suffix check I flagged:
const displayName = getAssetDisplayName(opt.asset).toLowerCase();
if (displayName === preferredLower) return true; // <- this catches itgetAssetDisplayName() maps any contract-id ending in ...sbtc-token to 'sBTC', so asset="sBTC" against SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token matches via displayName === 'sbtc' before the .sbtc suffix check (which genuinely wouldn't match) is even reached. Confirmed with a standalone run of both functions against the PR's own example asset — selectPaymentOption(accepts, "sBTC") correctly returns the sBTC entry. The PR's live-test claim holds up.
The two non-blocking notes from before still stand as suggestions, not blockers:
- Reusing
PaymentRequirementsV2instead of the inline duplicate type + cast inselectPaymentOptionwould avoid the type drift risk. getAssetDisplayName()'s substring match (includes('sbtc')) vsdetectTokenType()'s stricter suffix match — worth aligning eventually, not urgent.
No remaining blockers from my side. Approving.
Summary
Fixes #613 -
probe_x402_endpointandexecute_x402_endpointhardcoded the firstaccepts[]entry, blocking sBTC-only wallets on multi-asset endpoints.Changes
1. Asset Selection
Added optional
assetparameter to both tools:sBTC,STX,USDCxSM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token2. Display Fix
formatPaymentAmount()now derives currency name frompayment.assetinstead of hardcoded template. No more "This endpoint costs 29 STX" when payment.asset is USDCx.Files Modified
src/services/x402.service.ts(+79 lines):selectPaymentOption(),getAssetDisplayName(), updatedformatPaymentAmount(), addedassetparametersrc/tools/endpoint.tools.ts(+13 lines): Addedassetparameter to tool schemasLive Test
With this PR:
Returns:
payment.asset = SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-tokenChecklist
npm run build)Closes bounty: mrqpfv3je9b5615c89cb