feat(sdk): x402 v2 conformance for TS/Python/Rust SDKs#188
Conversation
- send X-Tinyplace-SDK identification header on every request - parse the 402 challenge from the standard accepts[] array (promoting the from/nonce/expiresAt binding fields out of extra), with a legacy payment fallback - add buildX402PaymentEnvelope/encodeX402PaymentHeader to produce a standard base64 X-PAYMENT PaymentPayload envelope
- send X-Tinyplace-SDK identification header on every request - parse the 402 challenge from the standard accepts[] array (promoting the from/nonce/expiresAt binding fields out of extra), with a legacy payment fallback - add build_x402_payment_envelope/encode_x402_payment_header for the standard base64 X-PAYMENT PaymentPayload envelope
- send X-Tinyplace-SDK identification header on every request (rust/<version> via CARGO_PKG_VERSION) - parse the 402 challenge from the standard accepts[] array (promoting the from/nonce/expiresAt binding fields out of extra), with a legacy payment fallback - add build_x402_payment_envelope/encode_x402_payment_header for the standard base64 X-PAYMENT PaymentPayload envelope
|
@senamakel is attempting to deploy a commit to the Vezures Team on Vercel. A member of the Team first needs to authorize it. |
|
Warning Review limit reached
More reviews will be available in 21 minutes and 9 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughImplements the x402 v2 "delegated payment" flow across TypeScript, Python, and Rust SDKs. Adds a Changesx402 v2 Delegated Payment Header Flow
Sequence Diagram(s)sequenceDiagram
rect rgba(173, 216, 230, 0.5)
Note over SDK Client,Solana RPC: Delegated Payment Header Construction
SDK Client->>Backend: POST /bounties or /registry/names (no payment)
Backend-->>SDK Client: 402 + accepts[0] {network, asset, amount, payTo, extra.feePayer}
SDK Client->>Solana RPC: getTokenAccountsByOwner (source ATA lookup)
SDK Client->>Solana RPC: getLatestBlockhash
SDK Client->>SDK Client: serialize two-signer message (feePayer, authority, compute-budget, TransferChecked)
SDK Client->>SDK Client: sign(message, authoritySecretKey) → authoritySignature
SDK Client->>SDK Client: zero fee-payer signature slot → base64 wire tx
SDK Client->>SDK Client: build x402 v2 exact-scheme envelope → base64 JSON → PAYMENT-SIGNATURE header
end
rect rgba(144, 238, 144, 0.5)
Note over SDK Client,Backend: Header-only Settlement
SDK Client->>Backend: POST /bounties or /registry/names + PAYMENT-SIGNATURE header (no payment body)
Backend-->>SDK Client: 201 {bounty/identity, paymentHeader}
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0696d2a22a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const fromAccepts = challengeFromAccepts(value); | ||
| if (fromAccepts) { | ||
| return { ...errorField, payment: fromAccepts }; |
There was a problem hiding this comment.
Read x402 v2 challenges from the standard header
This adds parsing for v2 accepts[], but the HTTP path that feeds this function still only looks at the legacy X-Payment-Required/x-payment-required header. Standard x402 v2 HTTP transports put the base64 PaymentRequired object in PAYMENT-REQUIRED, often with an empty body, so a compliant paid endpoint will still produce a TinyPlaceError without paymentRequired and the auto-payment/retry helpers cannot pay. The same header-name gap exists in the Python and Rust clients, so the migration remains incompatible with standard v2 header-only challenges.
Useful? React with 👍 / 👎.
- TS: generate src/version.ts from package.json via scripts/gen-version.mjs
(prebuild/pretest hook), so the version is never hand-maintained
- Python: derive from importlib.metadata.version("tinyplace") with a 0.0.0
fallback for uninstalled checkouts
- Rust already uses CARGO_PKG_VERSION (unchanged)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6921d2ee22
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| amount: authorization.amount, | ||
| asset: authorization.asset, | ||
| payTo: authorization.to, | ||
| maxTimeoutSeconds: 60, |
There was a problem hiding this comment.
Preserve challenge timeout in v2 envelopes
When a v2 challenge advertises an accepts[] requirement with maxTimeoutSeconds other than 60, the retry payload generated here echoes a different accepted requirement than the server quoted. Standard x402 verification matches the submitted payload against the original payment requirements, so those endpoints can reject otherwise valid payments; the new parsers also drop the challenge's timeout, leaving callers no way to supply the correct value. Please carry maxTimeoutSeconds from the selected accepts[] entry instead of hard-coding it, and mirror the fix in the Python/Rust helpers.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 095597e788
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| export function buildX402PaymentEnvelope( | ||
| authorization: X402Authorization, | ||
| ): X402PaymentEnvelope { | ||
| return { |
There was a problem hiding this comment.
Preserve v2 resource in payment envelopes
For standard x402 v2 challenges that include a resource object, this builder drops it and returns only accepted, payload, and extensions. The v2 payment payload schema carries resource inside the submitted paymentPayload (see Coinbase's v2 facilitator schema: https://docs.cdp.coinbase.com/api-reference/v2/rest-api/x402-facilitator/settle-payment), so header-based paid endpoints can reject these X-PAYMENT values as an invalid payload even though the SDK parsed the original challenge. Please preserve the challenge resource through the parser/API and include it in the encoded envelope; the Python and Rust mirror helpers have the same gap.
Useful? React with 👍 / 👎.
Export an X402_PAYMENT_HEADER constant (= "PAYMENT-SIGNATURE", the canonical x402 v2 submission header) from all three SDKs and correct the X402 envelope docs that referenced the legacy X-PAYMENT name. The PaymentPayload envelopes and accepts[] challenge parsers were already v2; this fixes the documented submission surface. Adds a regression test per SDK.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4915bc4774
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const extra = | ||
| typeof accept["extra"] === "object" && accept["extra"] !== null | ||
| ? (accept["extra"] as Record<string, unknown>) | ||
| : {}; |
There was a problem hiding this comment.
Preserve accepts metadata when parsing challenges
When a paid endpoint returns the documented accepts[] challenge shape with signed metadata under metadata (gitbooks/commerce/payments.md:82-96), this parser only looks at accepts[].extra, so paymentRequired.payment.metadata is empty. Auto-payment then signs a payload that no longer matches the quoted requirement (or drops quote-specific fields beyond the default domain) and verify/settle can reject it; the Python and Rust parsers mirror the same extra-only behavior.
Useful? React with 👍 / 👎.
Port buildPayerSignedDelegatedTx/buildDelegatedX402PaymentMap from the TS SDK: build a payer-signed [SetComputeUnitLimit, SetComputeUnitPrice, TransferChecked] Solana tx with the facilitator as fee payer (signature slot zeroed for the backend to co-sign) and carry it as metadata.delegatedTx. registry.register and bounties.create now read metadata.feePayer from the 402 challenge and settle SPL (USDC/CASH) gasless via this path; native SOL keeps the direct path. Adds pure unit tests asserting the wire shape + payment map.
Add src/solana.rs: hand-rolled legacy Solana wire (shortvec/header/account-keys/ compiled instructions) building a payer-signed [SetComputeUnitLimit, SetComputeUnitPrice, TransferChecked] tx with the facilitator as fee payer (signature slot zeroed) carried as metadata.delegatedTx. Reuses existing crates (ed25519-dalek, bs58, sha2, curve25519-dalek) — no new deps. RegistryApi.register and BountiesApi.create gain *_with_solana_payment methods that read the 402 challenge's feePayer and settle gasless. Adds a unit test decoding the wire.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: da7b211a2d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .network | ||
| .clone() | ||
| .unwrap_or_else(|| SOLANA_MAINNET_NETWORK.to_string()); | ||
| let mint = options.mint.clone().unwrap_or_else(|| asset.clone()); |
There was a problem hiding this comment.
Resolve symbol assets before deriving token accounts
When a delegated 402 challenge carries a symbol-valued asset such as USDC (which existing tiny.place legacy challenges/tests still do) and the caller does not pass options.mint, this uses the literal string as the SPL mint. The next step derives ATAs by base58-decoding that value, so register_with_solana_payment/create_with_solana_payment fail with an invalid pubkey before they can sign the delegated transaction. Please run the challenge asset through the existing Solana asset resolver and only fall back to the raw value when it is already a mint address.
Useful? React with 👍 / 👎.
| signer=self._signer, | ||
| rpc_url=rpc_url, | ||
| fee_payer=str(fee_payer), | ||
| mint=mint or SOLANA_USDC_MINT, |
There was a problem hiding this comment.
Use the challenged asset mint for delegated bounties
In the new fee-payer delegated path, any bounty challenge for a non-native asset other than mainnet USDC (for example a CASH/devnet/custom mint, which this branch's comment says should be supported) signs the payment map with challenge_asset but builds the Solana transfer with SOLANA_USDC_MINT unless the caller manually overrides mint. That produces a delegated transaction whose transfer mint does not match the quoted x402 asset, so the facilitator will reject it or settle the wrong token; resolve challenge_asset the same way the direct payment helper does before defaulting to USDC.
Useful? React with 👍 / 👎.
…egated path - Store the minted SIWS proof in the CLI config (~/.tinyplace/config.json) and reuse it across invocations until expiry instead of re-minting per request (LocalSigner adopts a persisted token after validating address/sig/expiry; context.ts loads + writes it back, 0600). Mirrors the Python signer's cache. - registry.registerWithSolanaPayment now settles SPL (USDC) via the gasless delegated facilitator path (metadata.delegatedTx, fee payer from the challenge) instead of the direct client-fee-paid path that needed SOL; native SOL keeps the direct fallback. Fixes a latent bug where the bounty flow didn't thread ctx.fetch into buildDelegatedX402PaymentMap.
mint_siws now stores the rotated proof on the signer (matching the Python signer) instead of returning a fresh token without updating the cache; the proof is minted once at construction and reused on every request. Adds a test asserting the siws: token is well-formed (tiny.place domain line, decodable envelope) and stable across reads.
…Tx + native SOL)
All three SDKs (TypeScript, Python, Rust) now settle sponsored register +
bounty payments with the canonical x402 v2 wire: build the partially-signed SPL
transfer, wrap it in {x402Version:2, accepted:{...,extra:{feePayer}},
payload:{transaction}}, base64-encode it, and submit via the PAYMENT-SIGNATURE
header with no body payment. The 402 challenge is parsed from accepts[0]
(network/amount/asset/payTo/extra.feePayer).
Removed: the proprietary metadata.delegatedTx body-map transport, the native-SOL
payment path, and the Ed25519 authorization-as-payment builder for these flows.
SIWS / signed-write request auth is unchanged. Marketplace/games untouched.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3dc332f245
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| secretKey: options.secretKey, | ||
| rpcUrl: options.rpcUrl, | ||
| feePayer, | ||
| mint: options.mint ?? SOLANA_USDC_MINT, |
There was a problem hiding this comment.
Use the challenged mint for delegated registration
When the 402 registration challenge advertises a devnet/custom SPL mint (or CASH) and the caller does not manually pass options.mint, this falls back to the baked-in mainnet USDC mint even though payment.asset still comes from the challenge. buildDelegatedX402PaymentHeader then builds the transfer and accepted.asset around USDC, so the facilitator sees a payment for a different asset than the quoted requirement and the registration payment is rejected or settles the wrong token. Resolve the mint from the challenged asset before defaulting to USDC.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
sdk/typescript/src/api/registry.ts (1)
167-190:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDrop body
paymentwhen a delegated registration header is supplied.
RegisterRequeststill allowspayment, soregister(request, paymentHeader)can send both the legacy body map andPAYMENT-SIGNATURE. Strippaymentbefore signing/submitting the header-based request.Proposed fix
// Standard x402 v2: the sponsored SPL transfer rides in the // PAYMENT-SIGNATURE header; the request body carries NO `payment` field. if (paymentHeader) { + delete request.payment; headers[X402_PAYMENT_HEADER] = paymentHeader; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/typescript/src/api/registry.ts` around lines 167 - 190, When paymentHeader is supplied, the request body should not contain a payment field to avoid sending both the legacy body payment and the header-based PAYMENT-SIGNATURE. Before calling this.http.postPublic() in the registry.ts file, add a check: if paymentHeader exists, delete the payment field from the request object to ensure only the header-based payment mechanism is used for the registration.
🧹 Nitpick comments (7)
sdk/typescript/tests/solana-delegated.test.ts (1)
125-127: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winAdd explicit return types to the new test callbacks.
Line 125 and Line 126 add callback functions without explicit return types.
Proposed fix
-describe("buildDelegatedX402PaymentHeader", () => { - it("encodes the agent-signed wire transaction into the standard x402 v2 SVM PAYMENT-SIGNATURE envelope", async () => { +describe("buildDelegatedX402PaymentHeader", (): void => { + it("encodes the agent-signed wire transaction into the standard x402 v2 SVM PAYMENT-SIGNATURE envelope", async (): Promise<void> => {As per coding guidelines,
**/*.{ts,tsx}: ESLint requires explicit return types on functions (@typescript-eslint/explicit-function-return-type).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/typescript/tests/solana-delegated.test.ts` around lines 125 - 127, The callback functions in the describe and it test declarations are missing explicit return type annotations as required by the ESLint rule `@typescript-eslint/explicit-function-return-type`. Add explicit return type annotations to both callback functions passed to describe and it functions in the test file. Since the describe callback contains async operations and the it callback is async, both should be annotated with explicit return types (likely Promise<void> or void depending on their async nature).Source: Coding guidelines
sdk/typescript/tests/registry.test.ts (1)
60-90: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winAdd explicit return types to newly added callbacks.
Line 60 and Line 68 introduce new functions without explicit return types, which breaks the TS lint rule used in this repo.
Proposed fix
- it("sends the X-Tinyplace-SDK identification header on every request", async () => { + it("sends the X-Tinyplace-SDK identification header on every request", async (): Promise<void> => { @@ - fetch: async (input, init) => { + fetch: async ( + input: RequestInfo | URL, + init?: RequestInit, + ): Promise<Response> => {As per coding guidelines,
**/*.{ts,tsx}: ESLint requires explicit return types on functions (@typescript-eslint/explicit-function-return-type).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/typescript/tests/registry.test.ts` around lines 60 - 90, Add explicit return type annotations to the callback functions in the test. The test function (the "it" callback starting at line 60) and the async fetch callback function (starting at line 68) both lack explicit return type declarations, which violates the ESLint rule requiring explicit function return types. Add the appropriate return type annotations to both the outer test callback and the inner fetch callback to comply with the TypeScript linting requirements.Source: Coding guidelines
sdk/typescript/tests/x402.test.ts (1)
89-125: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winAdd explicit return types to the new tests.
Line 89 and Line 122 add untyped function callbacks in a
.tsfile.Proposed fix
- it("encodes a standard x402 v2 X-PAYMENT envelope", () => { + it("encodes a standard x402 v2 X-PAYMENT envelope", (): void => { @@ - it("exposes the canonical x402 v2 submission header", () => { + it("exposes the canonical x402 v2 submission header", (): void => {As per coding guidelines,
**/*.{ts,tsx}: ESLint requires explicit return types on functions (@typescript-eslint/explicit-function-return-type).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/typescript/tests/x402.test.ts` around lines 89 - 125, The two test callback functions passed to the `it()` calls - one for the test "encodes a standard x402 v2 X-PAYMENT envelope" and another for "exposes the canonical x402 v2 submission header" - are missing explicit return type annotations. Add explicit return type annotations of `: void` to both anonymous callback functions passed to `it()` to comply with the TypeScript ESLint rule requiring explicit function return types.Source: Coding guidelines
sdk/typescript/tests/tinyplace-error.test.ts (1)
28-78: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winType the return value of the newly added
itcallbacks.The new test callbacks at Line 28 and Line 70 should declare explicit return types.
Proposed fix
- it("parses the challenge from the standard x402 v2 accepts[] array", () => { + it("parses the challenge from the standard x402 v2 accepts[] array", (): void => { @@ - it("falls back to the legacy payment field when accepts[] is absent", () => { + it("falls back to the legacy payment field when accepts[] is absent", (): void => {As per coding guidelines,
**/*.{ts,tsx}: ESLint requires explicit return types on functions (@typescript-eslint/explicit-function-return-type).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/typescript/tests/tinyplace-error.test.ts` around lines 28 - 78, The test callbacks for the `it()` functions at "parses the challenge from the standard x402 v2 accepts[] array" and "falls back to the legacy payment field when accepts[] is absent" are missing explicit return type annotations, which violates the ESLint `@typescript-eslint/explicit-function-return-type` rule. Add explicit return type annotations (void) to both arrow function callbacks passed to the it() functions.Source: Coding guidelines
sdk/typescript/tests/flows.test.ts (1)
177-283: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winSplit
delegatedRegistrationFetchinto smaller helpers to meet the function-length rule.This function now handles registry challenge flow, Solana metadata, and RPC stubs in one block, which exceeds the configured max-length guideline and makes edge-case updates harder to maintain.
As per coding guidelines, "Limit functions to a maximum of 20 lines of code; extract longer logic into separate helper functions."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/typescript/tests/flows.test.ts` around lines 177 - 283, The delegatedRegistrationFetch function exceeds the 20-line maximum code guideline. Extract the logic for handling each path into separate helper functions: create one helper for the /registry/names endpoint (the registryCalls logic), another for the /solana endpoint, and a third for the /solana/rpc endpoint (the RPC methods handling). Then refactor the fetch method inside delegatedRegistrationFetch to call these helpers within the appropriate if statements, reducing the main function to meet the length requirement and improving maintainability.Source: Coding guidelines
sdk/typescript/scripts/gen-version.mjs (1)
10-16: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winRename the abbreviated
pkgbinding.
pkgviolates the abbreviation rule; use a descriptive name likepackageJson.As per coding guidelines, "Avoid abbreviations (unicorn/prevent-abbreviations) — exceptions:
db,arg,args,env,fn,prop,props,ref,refs."♻️ Proposed rename
-const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8")); +const packageJson = JSON.parse( + readFileSync(join(here, "..", "package.json"), "utf8"), +); @@ -export const SDK_VERSION = ${JSON.stringify(pkg.version)}; +export const SDK_VERSION = ${JSON.stringify(packageJson.version)};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/typescript/scripts/gen-version.mjs` around lines 10 - 16, The variable binding `pkg` violates the project's abbreviation guidelines and should be renamed to a more descriptive name. Replace the abbreviated `pkg` binding with `packageJson` in both its declaration (where it reads from package.json using JSON.parse) and in its usage where it accesses pkg.version for the SDK_VERSION export. This ensures the code follows the coding standards which disallow abbreviations except for specific exceptions like db, arg, args, env, fn, prop, props, ref, and refs.Source: Coding guidelines
sdk/typescript/src/cli/context.ts (1)
16-18: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winAdd JSDoc for the exported context factory.
makeContextis exported; add a public JSDoc block before the function. As per coding guidelines, "Always include JSDoc comments for public functions and exported modules."📝 Proposed documentation
+/** + * Builds the CLI runtime context, including endpoint resolution, managed + * identity setup, signer/encryption wiring, and client construction. + */ export async function makeContext( options: TinyPlaceCliOptions, ): Promise<CliContext> {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/typescript/src/cli/context.ts` around lines 16 - 18, The exported makeContext function is missing required JSDoc documentation. Add a JSDoc comment block before the makeContext function that describes what the function does, documents the options parameter of type TinyPlaceCliOptions, and documents the Promise return type of CliContext, following the coding guidelines for public and exported functions.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@sdk/python/src/tinyplace/api/bounties.py`:
- Around line 50-53: The create method needs to sanitize the request before
delegating to the HTTP call when payment information is present. Before passing
the request to post_directory_auth_as, check if the request contains a "payment"
field. If it does, create a clone of the request dictionary and remove the
"payment" key from the clone, then use this sanitized clone for the delegated
HTTP call instead of the original request. This prevents the delegated path from
carrying both body payment and the PAYMENT-SIGNATURE header. Apply this same
sanitization pattern to all other similar methods in the range 82-115 that use
delegation.
In `@sdk/python/src/tinyplace/http.py`:
- Around line 456-472: The function currently returns an empty or partial
payment dict when required fields are missing, which prevents the caller
_challenge_from_object() from falling back to legacy payment handling. After
building the payment dict from the entry and extra parameters, add a guard
clause that checks if the payment dict contains the required fields
(specifically ensure both "amount" and "to" keys are present). If either
required field is missing, return None instead of the incomplete dict to
preserve Phase 1 compatibility by allowing the legacy payment fallback to
execute.
In `@sdk/rust/src/api/bounties.rs`:
- Around line 123-130: The current implementation of the bounty creation with
payment header does not retry if the backend returns retryable errors like 402
or 5xx status codes while the transaction confirmation is pending. This causes
the generated payment signature header to be lost. Add bounded retry logic
around the post_directory_auth_as_with_headers call that specifically catches
retryable status codes (402 and 5xx errors) and resubmits the same request using
the identical header_name and header_value to preserve the PAYMENT-SIGNATURE
across retries. Ensure the retry mechanism is bounded with a reasonable attempt
limit to prevent infinite loops while still allowing recovery from transient
confirmation delays.
- Around line 102-126: The initial probe call to self.create(request) on line
103 sends the original request which may already contain a payment field, but
the payment field should only be cleared later for the second request. Create a
clone of the request with the payment field set to None before passing it to the
initial self.create() call in the match statement (where payment_challenge is
called), ensuring the first probe is truly paymentless and receives the correct
402 challenge before the delegated payment header is built.
In `@sdk/rust/src/api/registry.rs`:
- Around line 170-177: The post_public_with_headers method call does not handle
transient failures (402/5xx status codes) gracefully when the delegated
registration header is involved. When such failures occur, the generated proof
header is lost and cannot be safely reused. Implement bounded retry logic that
preserves the same headers built from header_name and header_value across retry
attempts for transient errors, or alternatively modify the error response to
include the header information so callers can safely resubmit requests with the
same PAYMENT-SIGNATURE proof without needing to regenerate it.
- Around line 138-166: The RegisterRequest is clearing only the signature field
before making the initial registration probe call, but the payment field is not
being cleared. If the request already contains a body payment map, it will be
sent in the first self.register() call and could cause the request to succeed
through the legacy body payment path instead of using the delegated header flow.
Clear the payment field from the request object in addition to clearing the
signature field, by adding request.payment = None; right after the line where
request.signature is set to None.
In `@sdk/rust/src/solana.rs`:
- Line 327: The `is_none_or` method call on the `minimum` option requires Rust
1.82.0+, but the project doesn't specify an explicit MSRV in Cargo.toml,
defaulting to 1.56.0. Either add `rust-version = "1.82"` to the Cargo.toml file
to establish the required MSRV, or replace the `is_none_or` method call with a
compatible alternative using pattern matching like `matches!(minimum, None) ||
minimum.map_or(false, |min| amount >= min)` to maintain compatibility with
earlier Rust versions.
In `@sdk/typescript/src/api/bounties.ts`:
- Around line 42-48: The create method is currently sending the entire
BountyCreateRequest object as the body, which may include a payment field
alongside the payment header. When paymentHeader is supplied, exclude the
payment field from the request body to ensure the delegated wire format uses
header-only payment information. Modify the request object passed to
postDirectoryAuthAs to omit the payment property when paymentHeader is provided,
ensuring only the header-based payment mechanism is used rather than both header
and body payment maps.
In `@sdk/typescript/src/api/registry.ts`:
- Around line 116-120: The `registrationPayment` property in the
`SolanaRegistrationFailure` interface is missing the `X402PaymentMap` type in
its union type definition. Update the type annotation for `registrationPayment`
to include `X402PaymentMap` alongside the existing `SolanaX402PaymentExecution`
and `DelegatedRegistrationPayment` types, so that the actual type attached by
`registerWithExistingSolanaPayment` is accurately represented and catch handlers
can properly branch on the correct type without needing to cast it away.
- Around line 222-256: For non-native USDC/SPL asset registrations, the feePayer
field from metadata is required for the delegated facilitator-sponsored flow.
Add a validation check after the isNative and feePayer variable assignments that
throws an error if isNative is false but feePayer is missing or falsy. This
ensures that only native SOL registrations can fall back to the direct payment
path when feePayer is absent, while non-native assets properly fail with a clear
error message rather than silently falling through to the else branch that calls
executeSolanaX402Payment.
In `@sdk/typescript/src/cli/context.ts`:
- Around line 39-56: The code currently persists both the SIWS token and the
secretKey to the config file whenever the token changes, but this leaks
environment-provided secrets to disk. The issue is at the persistConfig call
within the block starting at line 49 where `managed && signer` is true: the
condition checks if `managed` is true, but `managed` only indicates that
options.env is absent, not that the seed came from the config file rather than
an environment variable. Remove the `...(seed ? { secretKey: seed } : {})`
spread from the persistConfig call so that only the updated SIWS token is
persisted to the config file, never writing environment-provided secrets to
disk.
In `@sdk/typescript/src/http.ts`:
- Around line 954-963: The function `challengeFromAccepts()` always returns an
object once `accepts[0]` is object-like, even if the resulting object lacks
essential fields like `amount` or `to`, which prevents the Phase 1 `payment`
fallback at Line 890 from being used and can result in an empty payment
challenge. Add a guard condition after constructing the return object to check
if the final object contains at least the required usable fields (specifically
checking for `amount` and `to`), and return null or undefined if these critical
fields are missing, allowing the legacy fallback mechanism to handle such cases
properly.
In `@sdk/typescript/src/local-signer.ts`:
- Around line 254-270: The adoptSiws method does not handle exceptions thrown by
ed25519.verify() when the signature has an invalid length, which prevents
graceful fallback to re-minting a fresh proof. Add validation to check that the
signature from the parsed token has exactly 64 bytes before calling
ed25519.verify(), and wrap the verify() call in a try-catch block to catch any
RangeError or other exceptions, returning false in both cases to ensure all
malformed tokens are treated as non-adoptable.
---
Outside diff comments:
In `@sdk/typescript/src/api/registry.ts`:
- Around line 167-190: When paymentHeader is supplied, the request body should
not contain a payment field to avoid sending both the legacy body payment and
the header-based PAYMENT-SIGNATURE. Before calling this.http.postPublic() in the
registry.ts file, add a check: if paymentHeader exists, delete the payment field
from the request object to ensure only the header-based payment mechanism is
used for the registration.
---
Nitpick comments:
In `@sdk/typescript/scripts/gen-version.mjs`:
- Around line 10-16: The variable binding `pkg` violates the project's
abbreviation guidelines and should be renamed to a more descriptive name.
Replace the abbreviated `pkg` binding with `packageJson` in both its declaration
(where it reads from package.json using JSON.parse) and in its usage where it
accesses pkg.version for the SDK_VERSION export. This ensures the code follows
the coding standards which disallow abbreviations except for specific exceptions
like db, arg, args, env, fn, prop, props, ref, and refs.
In `@sdk/typescript/src/cli/context.ts`:
- Around line 16-18: The exported makeContext function is missing required JSDoc
documentation. Add a JSDoc comment block before the makeContext function that
describes what the function does, documents the options parameter of type
TinyPlaceCliOptions, and documents the Promise return type of CliContext,
following the coding guidelines for public and exported functions.
In `@sdk/typescript/tests/flows.test.ts`:
- Around line 177-283: The delegatedRegistrationFetch function exceeds the
20-line maximum code guideline. Extract the logic for handling each path into
separate helper functions: create one helper for the /registry/names endpoint
(the registryCalls logic), another for the /solana endpoint, and a third for the
/solana/rpc endpoint (the RPC methods handling). Then refactor the fetch method
inside delegatedRegistrationFetch to call these helpers within the appropriate
if statements, reducing the main function to meet the length requirement and
improving maintainability.
In `@sdk/typescript/tests/registry.test.ts`:
- Around line 60-90: Add explicit return type annotations to the callback
functions in the test. The test function (the "it" callback starting at line 60)
and the async fetch callback function (starting at line 68) both lack explicit
return type declarations, which violates the ESLint rule requiring explicit
function return types. Add the appropriate return type annotations to both the
outer test callback and the inner fetch callback to comply with the TypeScript
linting requirements.
In `@sdk/typescript/tests/solana-delegated.test.ts`:
- Around line 125-127: The callback functions in the describe and it test
declarations are missing explicit return type annotations as required by the
ESLint rule `@typescript-eslint/explicit-function-return-type`. Add explicit
return type annotations to both callback functions passed to describe and it
functions in the test file. Since the describe callback contains async
operations and the it callback is async, both should be annotated with explicit
return types (likely Promise<void> or void depending on their async nature).
In `@sdk/typescript/tests/tinyplace-error.test.ts`:
- Around line 28-78: The test callbacks for the `it()` functions at "parses the
challenge from the standard x402 v2 accepts[] array" and "falls back to the
legacy payment field when accepts[] is absent" are missing explicit return type
annotations, which violates the ESLint
`@typescript-eslint/explicit-function-return-type` rule. Add explicit return type
annotations (void) to both arrow function callbacks passed to the it()
functions.
In `@sdk/typescript/tests/x402.test.ts`:
- Around line 89-125: The two test callback functions passed to the `it()` calls
- one for the test "encodes a standard x402 v2 X-PAYMENT envelope" and another
for "exposes the canonical x402 v2 submission header" - are missing explicit
return type annotations. Add explicit return type annotations of `: void` to
both anonymous callback functions passed to `it()` to comply with the TypeScript
ESLint rule requiring explicit function return types.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 229b5a9b-80fe-4ba8-add2-c20282ad302e
📒 Files selected for processing (39)
sdk/python/src/tinyplace/__init__.pysdk/python/src/tinyplace/api/bounties.pysdk/python/src/tinyplace/api/registry.pysdk/python/src/tinyplace/http.pysdk/python/src/tinyplace/solana.pysdk/python/src/tinyplace/x402.pysdk/python/tests/test_bounties.pysdk/python/tests/test_registry.pysdk/python/tests/test_x402_delegated.pysdk/python/tests/test_x402_standard.pysdk/rust/src/api/bounties.rssdk/rust/src/api/registry.rssdk/rust/src/http.rssdk/rust/src/lib.rssdk/rust/src/signer.rssdk/rust/src/solana.rssdk/rust/src/x402.rssdk/rust/tests/core.rssdk/rust/tests/x402_standard.rssdk/typescript/package.jsonsdk/typescript/scripts/gen-version.mjssdk/typescript/src/api/bounties.tssdk/typescript/src/api/registry.tssdk/typescript/src/cli/context.tssdk/typescript/src/cli/flows.tssdk/typescript/src/cli/types.tssdk/typescript/src/http.tssdk/typescript/src/index.tssdk/typescript/src/local-signer.tssdk/typescript/src/solana.tssdk/typescript/src/version.tssdk/typescript/src/x402.tssdk/typescript/tests/cli.test.tssdk/typescript/tests/flows.test.tssdk/typescript/tests/local-signer-siws.test.tssdk/typescript/tests/registry.test.tssdk/typescript/tests/solana-delegated.test.tssdk/typescript/tests/tinyplace-error.test.tssdk/typescript/tests/x402.test.ts
| async def create(self, request: JsonDict, *, payment_header: str | None = None) -> Json: | ||
| headers = {X402_PAYMENT_HEADER: payment_header} if payment_header else None | ||
| return await self._http.post_directory_auth_as( | ||
| "/bounties", str(request.get("creator") or ""), request | ||
| "/bounties", str(request.get("creator") or ""), request, headers=headers |
There was a problem hiding this comment.
Sanitize delegated bounty requests before probing and retrying.
If request already contains "payment", the probe is not paymentless and the funded create can carry both body payment and PAYMENT-SIGNATURE. Clone and strip "payment" for the delegated path.
Proposed fix
async def create(self, request: JsonDict, *, payment_header: str | None = None) -> Json:
+ body = dict(request)
+ if payment_header:
+ body.pop("payment", None)
headers = {X402_PAYMENT_HEADER: payment_header} if payment_header else None
return await self._http.post_directory_auth_as(
- "/bounties", str(request.get("creator") or ""), request, headers=headers
+ "/bounties", str(request.get("creator") or ""), body, headers=headers
)
@@
- challenge = await self._create_challenge(request)
+ paymentless_request = dict(request)
+ paymentless_request.pop("payment", None)
+ challenge = await self._create_challenge(paymentless_request)
@@
- bounty = await self._create_retrying(request, payment_header, attempts, interval_ms)
+ bounty = await self._create_retrying(
+ paymentless_request, payment_header, attempts, interval_ms
+ )Also applies to: 82-115
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/python/src/tinyplace/api/bounties.py` around lines 50 - 53, The create
method needs to sanitize the request before delegating to the HTTP call when
payment information is present. Before passing the request to
post_directory_auth_as, check if the request contains a "payment" field. If it
does, create a clone of the request dictionary and remove the "payment" key from
the clone, then use this sanitized clone for the delegated HTTP call instead of
the original request. This prevents the delegated path from carrying both body
payment and the PAYMENT-SIGNATURE header. Apply this same sanitization pattern
to all other similar methods in the range 82-115 that use delegation.
| payment: dict[str, Any] = {} | ||
| for key in ("scheme", "network", "asset", "amount"): | ||
| if isinstance(entry.get(key), str): | ||
| payment[key] = entry[key] | ||
| if isinstance(entry.get("payTo"), str): | ||
| payment["to"] = entry["payTo"] | ||
| for key in binding_keys: | ||
| if isinstance(extra.get(key), str): | ||
| payment[key] = extra[key] | ||
| metadata = { | ||
| key: value_ | ||
| for key, value_ in extra.items() | ||
| if key not in binding_keys and isinstance(value_, str) | ||
| } | ||
| if metadata: | ||
| payment["metadata"] = metadata | ||
| return payment |
There was a problem hiding this comment.
Return None for unusable accepts[0] challenges.
This currently returns {} or a partial dict when accepts[0] lacks amount/payTo, causing _challenge_from_object() to skip the legacy payment fallback. Guard before returning so Phase 1 compatibility still works.
🐛 Proposed guard
metadata = {
key: value_
for key, value_ in extra.items()
if key not in binding_keys and isinstance(value_, str)
}
if metadata:
payment["metadata"] = metadata
- return payment
+ return payment if payment.get("amount") and payment.get("to") else None🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/python/src/tinyplace/http.py` around lines 456 - 472, The function
currently returns an empty or partial payment dict when required fields are
missing, which prevents the caller _challenge_from_object() from falling back to
legacy payment handling. After building the payment dict from the entry and
extra parameters, add a guard clause that checks if the payment dict contains
the required fields (specifically ensure both "amount" and "to" keys are
present). If either required field is missing, return None instead of the
incomplete dict to preserve Phase 1 compatibility by allowing the legacy payment
fallback to execute.
| // First call without payment to receive the 402 challenge. | ||
| let challenge = match self.create(request).await { | ||
| Ok(bounty) => return Ok(bounty), | ||
| Err(error) => payment_challenge(error)?, | ||
| }; | ||
|
|
||
| // Build the standard PAYMENT-SIGNATURE header from the challenge. | ||
| let (header_name, header_value) = build_delegated_payment_header_from_challenge( | ||
| &challenge, | ||
| ChallengeDelegatedPaymentOptions { | ||
| secret_key: options.secret_key, | ||
| decimals: options.decimals, | ||
| rpc: options.rpc, | ||
| rpc_url: options.rpc_url, | ||
| mint: options.mint, | ||
| source_token_account: options.source_token_account, | ||
| destination_token_account: options.destination_token_account, | ||
| }, | ||
| ) | ||
| .await?; | ||
|
|
||
| // Re-post the bounty with the payment in the header and NO body | ||
| // `payment` map. | ||
| let mut funded = request.clone(); | ||
| funded.payment = None; |
There was a problem hiding this comment.
Probe with a paymentless request in the delegated flow.
Line 103 still sends the caller’s original request; payment is only cleared later on Line 126. If a reused request already contains a legacy body payment, the “no payment” probe can settle or fail through the wrong wire format before the delegated header is built.
Proposed fix
- // First call without payment to receive the 402 challenge.
- let challenge = match self.create(request).await {
+ // First call without payment to receive the 402 challenge.
+ let mut paymentless = request.clone();
+ paymentless.payment = None;
+ let challenge = match self.create(&paymentless).await {
Ok(bounty) => return Ok(bounty),
Err(error) => payment_challenge(error)?,
};
@@
- let mut funded = request.clone();
- funded.payment = None;
+ let funded = paymentless;
let headers: crate::auth::Headers = vec![(header_name, header_value)];🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/rust/src/api/bounties.rs` around lines 102 - 126, The initial probe call
to self.create(request) on line 103 sends the original request which may already
contain a payment field, but the payment field should only be cleared later for
the second request. Create a clone of the request with the payment field set to
None before passing it to the initial self.create() call in the match statement
(where payment_challenge is called), ensuring the first probe is truly
paymentless and receives the correct 402 challenge before the delegated payment
header is built.
| // Re-post the bounty with the payment in the header and NO body | ||
| // `payment` map. | ||
| let mut funded = request.clone(); | ||
| funded.payment = None; | ||
| let headers: crate::auth::Headers = vec![(header_name, header_value)]; | ||
| self.http | ||
| .post_directory_auth_as_with_headers("/bounties", creator, Some(&funded), &headers) | ||
| .await |
There was a problem hiding this comment.
Retry the same delegated header through confirmation lag.
The funded create is attempted once. If the backend returns a retryable post-payment 402/5xx while the transaction is not yet confirmed, this method drops the only copy of the generated header; re-running the whole flow can build a fresh transfer and risk duplicate settlement. Add bounded retry options that resubmit the same PAYMENT-SIGNATURE, or return/attach the header so callers can safely retry.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/rust/src/api/bounties.rs` around lines 123 - 130, The current
implementation of the bounty creation with payment header does not retry if the
backend returns retryable errors like 402 or 5xx status codes while the
transaction confirmation is pending. This causes the generated payment signature
header to be lost. Add bounded retry logic around the
post_directory_auth_as_with_headers call that specifically catches retryable
status codes (402 and 5xx errors) and resubmits the same request using the
identical header_name and header_value to preserve the PAYMENT-SIGNATURE across
retries. Ensure the retry mechanism is bounded with a reasonable attempt limit
to prevent infinite loops while still allowing recovery from transient
confirmation delays.
| // The registration payload is re-signed on each call, so the signature | ||
| // must not be carried across the challenge fetch. | ||
| request.signature = None; | ||
|
|
||
| // First call without payment to receive the 402 challenge. | ||
| let challenge = match self.register(request.clone()).await { | ||
| Ok(identity) => return Ok(identity), | ||
| Err(error) => payment_challenge(error)?, | ||
| }; | ||
|
|
||
| // Build the standard PAYMENT-SIGNATURE header from the challenge. | ||
| let (header_name, header_value) = build_delegated_payment_header_from_challenge( | ||
| &challenge, | ||
| ChallengeDelegatedPaymentOptions { | ||
| secret_key: options.secret_key, | ||
| decimals: options.decimals, | ||
| rpc: options.rpc, | ||
| rpc_url: options.rpc_url, | ||
| mint: options.mint, | ||
| source_token_account: options.source_token_account, | ||
| destination_token_account: options.destination_token_account, | ||
| }, | ||
| ) | ||
| .await?; | ||
|
|
||
| // Re-post the signed registration with the payment in the header and NO | ||
| // body `payment` map. | ||
| let mut funded = request; | ||
| funded.payment = None; |
There was a problem hiding this comment.
Clear body payment before the registration challenge probe.
Line 140 clears only signature; a RegisterRequest with an existing body payment will send that map on the “no payment” probe at Line 143. That can register through the legacy/body path and bypass the delegated header flow.
Proposed fix
// The registration payload is re-signed on each call, so the signature
// must not be carried across the challenge fetch.
+ request.payment = None;
request.signature = None;
@@
// Re-post the signed registration with the payment in the header and NO
// body `payment` map.
let mut funded = request;
- funded.payment = None;
let payload = registration_signature_payload(&funded);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // The registration payload is re-signed on each call, so the signature | |
| // must not be carried across the challenge fetch. | |
| request.signature = None; | |
| // First call without payment to receive the 402 challenge. | |
| let challenge = match self.register(request.clone()).await { | |
| Ok(identity) => return Ok(identity), | |
| Err(error) => payment_challenge(error)?, | |
| }; | |
| // Build the standard PAYMENT-SIGNATURE header from the challenge. | |
| let (header_name, header_value) = build_delegated_payment_header_from_challenge( | |
| &challenge, | |
| ChallengeDelegatedPaymentOptions { | |
| secret_key: options.secret_key, | |
| decimals: options.decimals, | |
| rpc: options.rpc, | |
| rpc_url: options.rpc_url, | |
| mint: options.mint, | |
| source_token_account: options.source_token_account, | |
| destination_token_account: options.destination_token_account, | |
| }, | |
| ) | |
| .await?; | |
| // Re-post the signed registration with the payment in the header and NO | |
| // body `payment` map. | |
| let mut funded = request; | |
| funded.payment = None; | |
| // The registration payload is re-signed on each call, so the signature | |
| // must not be carried across the challenge fetch. | |
| request.payment = None; | |
| request.signature = None; | |
| // First call without payment to receive the 402 challenge. | |
| let challenge = match self.register(request.clone()).await { | |
| Ok(identity) => return Ok(identity), | |
| Err(error) => payment_challenge(error)?, | |
| }; | |
| // Build the standard PAYMENT-SIGNATURE header from the challenge. | |
| let (header_name, header_value) = build_delegated_payment_header_from_challenge( | |
| &challenge, | |
| ChallengeDelegatedPaymentOptions { | |
| secret_key: options.secret_key, | |
| decimals: options.decimals, | |
| rpc: options.rpc, | |
| rpc_url: options.rpc_url, | |
| mint: options.mint, | |
| source_token_account: options.source_token_account, | |
| destination_token_account: options.destination_token_account, | |
| }, | |
| ) | |
| .await?; | |
| // Re-post the signed registration with the payment in the header and NO | |
| // body `payment` map. | |
| let mut funded = request; | |
| let payload = registration_signature_payload(&funded); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/rust/src/api/registry.rs` around lines 138 - 166, The RegisterRequest is
clearing only the signature field before making the initial registration probe
call, but the payment field is not being cleared. If the request already
contains a body payment map, it will be sent in the first self.register() call
and could cause the request to succeed through the legacy body payment path
instead of using the delegated header flow. Clear the payment field from the
request object in addition to clearing the signature field, by adding
request.payment = None; right after the line where request.signature is set to
None.
| export interface SolanaRegistrationFailure extends Error { | ||
| registrationPayment?: SolanaX402PaymentExecution | X402PaymentMap; | ||
| registrationPayment?: | ||
| | SolanaX402PaymentExecution | ||
| | DelegatedRegistrationPayment; | ||
| onChainTx?: string; |
There was a problem hiding this comment.
Keep registrationPayment typed to the actual attached value.
registerWithExistingSolanaPayment attaches an X402PaymentMap, but SolanaRegistrationFailure.registrationPayment excludes that shape and Line 688 casts it away. Include X402PaymentMap so catch handlers can branch accurately.
Proposed fix
export interface SolanaRegistrationFailure extends Error {
registrationPayment?:
| SolanaX402PaymentExecution
- | DelegatedRegistrationPayment;
+ | DelegatedRegistrationPayment
+ | X402PaymentMap;
onChainTx?: string;
}
@@
- failure.registrationPayment = payment as
- | SolanaX402PaymentExecution
- | DelegatedRegistrationPayment;
+ failure.registrationPayment = payment;Also applies to: 682-690
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/typescript/src/api/registry.ts` around lines 116 - 120, The
`registrationPayment` property in the `SolanaRegistrationFailure` interface is
missing the `X402PaymentMap` type in its union type definition. Update the type
annotation for `registrationPayment` to include `X402PaymentMap` alongside the
existing `SolanaX402PaymentExecution` and `DelegatedRegistrationPayment` types,
so that the actual type attached by `registerWithExistingSolanaPayment` is
accurately represented and catch handlers can properly branch on the correct
type without needing to cast it away.
| const isNative = asset.trim().toUpperCase() === SOLANA_NATIVE_ASSET; | ||
| const feePayer = metadata["feePayer"]; | ||
|
|
||
| // The USDC registration fee settles gaslessly through the facilitator: a | ||
| // payer-signed delegated tx whose fee payer is the facilitator (from the | ||
| // 402 challenge's metadata.feePayer), so the wallet needs no SOL for gas. | ||
| // This mirrors the bounty-funding flow and the Python/Rust SDKs. Native SOL | ||
| // — which the facilitator cannot settle — falls back to the direct path, | ||
| // where the wallet pays its own gas and broadcasts the transfer itself. | ||
| let payment: SolanaX402PaymentExecution | DelegatedRegistrationPayment; | ||
| let paymentMap: X402PaymentMap | undefined; | ||
| let paymentHeader: string | undefined; | ||
| let onChainTx: string | undefined; | ||
| if (!isNative && feePayer) { | ||
| // Standard x402 v2 SVM "exact": the agent-signed SPL transfer is encoded | ||
| // into the PAYMENT-SIGNATURE header (payload.transaction); the register | ||
| // body carries NO `payment` field. The facilitator co-signs as fee payer | ||
| // and broadcasts, so the wallet needs no SOL for gas. | ||
| paymentHeader = await buildDelegatedX402PaymentHeader({ | ||
| secretKey: options.secretKey, | ||
| rpcUrl: options.rpcUrl, | ||
| feePayer, | ||
| mint: options.mint ?? SOLANA_USDC_MINT, | ||
| decimals: options.decimals ?? 6, | ||
| ...(options.sourceTokenAccount | ||
| ? { sourceTokenAccount: options.sourceTokenAccount } | ||
| : {}), | ||
| ...(options.destinationTokenAccount | ||
| ? { destinationTokenAccount: options.destinationTokenAccount } | ||
| : {}), | ||
| ...(options.fetch ? { fetch: options.fetch } : {}), | ||
| payment: { network, asset, amount, to, metadata }, | ||
| }); | ||
| payment = { delegated: true, paymentHeader }; | ||
| } else { |
There was a problem hiding this comment.
Require feePayer for non-native delegated registration.
For USDC/SPL assets, missing metadata.feePayer currently falls into executeSolanaX402Payment, producing a body-based/direct path even though this flow is documented as facilitator-sponsored. Throw for non-native challenges without feePayer; reserve the direct fallback for native SOL.
Proposed fix
const isNative = asset.trim().toUpperCase() === SOLANA_NATIVE_ASSET;
const feePayer = metadata["feePayer"];
+ if (!isNative && !feePayer) {
+ throw new Error(
+ "registration payment challenge is missing the facilitator fee payer",
+ );
+ }
@@
- if (!isNative && feePayer) {
+ if (!isNative) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/typescript/src/api/registry.ts` around lines 222 - 256, For non-native
USDC/SPL asset registrations, the feePayer field from metadata is required for
the delegated facilitator-sponsored flow. Add a validation check after the
isNative and feePayer variable assignments that throws an error if isNative is
false but feePayer is missing or falsy. This ensures that only native SOL
registrations can fall back to the direct payment path when feePayer is absent,
while non-native assets properly fail with a clear error message rather than
silently falling through to the else branch that calls executeSolanaX402Payment.
| // Adopt the persisted SIWS proof when one is stored; LocalSigner ignores it if | ||
| // it is stale or belongs to a different key and mints a fresh one instead. | ||
| const signer = seed | ||
| ? await LocalSigner.fromSeed(hexToBytes(seed), { | ||
| ...(config.siwsToken ? { siwsToken: config.siwsToken } : {}), | ||
| }) | ||
| : undefined; | ||
| // Persist the SIWS proof the signer settled on (a freshly minted or rotated | ||
| // token) so the next managed-CLI run reuses it instead of re-minting. Skipped | ||
| // for embedder/test env (the CLI never writes a key/token on their behalf). | ||
| if (managed && signer) { | ||
| const token = signer.persistableSiwsToken(); | ||
| if (token && token !== config.siwsToken) { | ||
| await persistConfig(env, { | ||
| ...config, | ||
| ...(seed ? { secretKey: seed } : {}), | ||
| siwsToken: token, | ||
| }); |
There was a problem hiding this comment.
Do not persist env-provided identity secrets.
managed only means options.env is absent, so a real CLI run with TINYPLACE_SECRET_KEY still reaches Lines 49-56. If the SIWS token changes, this writes the env seed into the config file, leaking a one-off secret to disk and potentially overwriting the managed identity.
🔒 Proposed fix
- let seed = env.TINYPLACE_SECRET_KEY ?? config.secretKey;
+ const envSeed = env.TINYPLACE_SECRET_KEY;
+ let seed = envSeed ?? config.secretKey;
let generated = false;
if (!seed && managed) {
seed = bytesToHex(randomSeed());
generated = true;
await persistSecretKey(env, config, seed);
@@
const signer = seed
? await LocalSigner.fromSeed(hexToBytes(seed), {
- ...(config.siwsToken ? { siwsToken: config.siwsToken } : {}),
+ ...(!envSeed && config.siwsToken
+ ? { siwsToken: config.siwsToken }
+ : {}),
})
: undefined;
@@
- if (managed && signer) {
+ if (managed && signer && !envSeed) {
const token = signer.persistableSiwsToken();
if (token && token !== config.siwsToken) {
await persistConfig(env, {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/typescript/src/cli/context.ts` around lines 39 - 56, The code currently
persists both the SIWS token and the secretKey to the config file whenever the
token changes, but this leaks environment-provided secrets to disk. The issue is
at the persistConfig call within the block starting at line 49 where `managed &&
signer` is true: the condition checks if `managed` is true, but `managed` only
indicates that options.env is absent, not that the seed came from the config
file rather than an environment variable. Remove the `...(seed ? { secretKey:
seed } : {})` spread from the persistConfig call so that only the updated SIWS
token is persisted to the config file, never writing environment-provided
secrets to disk.
| return { | ||
| ...stringField(accept, "scheme"), | ||
| ...stringField(accept, "network"), | ||
| ...stringField(accept, "asset"), | ||
| ...stringField(accept, "amount"), | ||
| // accepts[] names the recipient `payTo`; the SDK challenge calls it `to`. | ||
| ...(typeof accept["payTo"] === "string" ? { to: accept["payTo"] } : {}), | ||
| ...binding, | ||
| ...(Object.keys(metadata).length > 0 ? { metadata } : {}), | ||
| }; |
There was a problem hiding this comment.
Do not let an unusable accepts[0] suppress the legacy fallback.
challengeFromAccepts() always returns an object once accepts[0] is object-like, even if no usable amount/payTo was parsed. That makes Line 890 skip the Phase 1 payment fallback and can surface an empty payment challenge.
🐛 Proposed guard
- return {
+ const payment: PaymentChallenge = {
...stringField(accept, "scheme"),
...stringField(accept, "network"),
...stringField(accept, "asset"),
...stringField(accept, "amount"),
// accepts[] names the recipient `payTo`; the SDK challenge calls it `to`.
...(typeof accept["payTo"] === "string" ? { to: accept["payTo"] } : {}),
...binding,
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
};
+ return payment.amount && payment.to ? payment : undefined;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return { | |
| ...stringField(accept, "scheme"), | |
| ...stringField(accept, "network"), | |
| ...stringField(accept, "asset"), | |
| ...stringField(accept, "amount"), | |
| // accepts[] names the recipient `payTo`; the SDK challenge calls it `to`. | |
| ...(typeof accept["payTo"] === "string" ? { to: accept["payTo"] } : {}), | |
| ...binding, | |
| ...(Object.keys(metadata).length > 0 ? { metadata } : {}), | |
| }; | |
| const payment: PaymentChallenge = { | |
| ...stringField(accept, "scheme"), | |
| ...stringField(accept, "network"), | |
| ...stringField(accept, "asset"), | |
| ...stringField(accept, "amount"), | |
| // accepts[] names the recipient `payTo`; the SDK challenge calls it `to`. | |
| ...(typeof accept["payTo"] === "string" ? { to: accept["payTo"] } : {}), | |
| ...binding, | |
| ...(Object.keys(metadata).length > 0 ? { metadata } : {}), | |
| }; | |
| return payment.amount && payment.to ? payment : undefined; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/typescript/src/http.ts` around lines 954 - 963, The function
`challengeFromAccepts()` always returns an object once `accepts[0]` is
object-like, even if the resulting object lacks essential fields like `amount`
or `to`, which prevents the Phase 1 `payment` fallback at Line 890 from being
used and can result in an empty payment challenge. Add a guard condition after
constructing the return object to check if the final object contains at least
the required usable fields (specifically checking for `amount` and `to`), and
return null or undefined if these critical fields are missing, allowing the
legacy fallback mechanism to handle such cases properly.
| private adoptSiws(token: string): boolean { | ||
| const parsed = parseSiwsToken(token); | ||
| if (!parsed) { | ||
| return false; | ||
| } | ||
| const { message, signature } = parsed; | ||
| const address = message.split("\n")[1] ?? ""; | ||
| if (address !== this.agentId) { | ||
| return false; | ||
| } | ||
| const expiration = siwsExpiration(message); | ||
| if (expiration === undefined || expiration <= Date.now()) { | ||
| return false; | ||
| } | ||
| const messageBytes = new TextEncoder().encode(message); | ||
| if (!ed25519.verify(signature, messageBytes, this.publicKey)) { | ||
| return false; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does @noble/curves version 2.2.0 ed25519.verify throw for invalid Ed25519 signature byte lengths, or does it return false?
💡 Result:
In @noble/curves version 2.2.0, the ed25519.verify function throws a RangeError if the provided signature does not have the correct length [1][2]. The library documentation and technical specifications for the ed25519 verify function explicitly define the signature parameter as a 64-byte value, and the implementation enforces these constraints by throwing errors for invalid argument ranges [1][2]. Therefore, passing a signature with an incorrect byte length will cause the function to throw rather than returning false [1][2].
Citations:
- 1: https://github.com/paulmillr/noble-ed25519/blob/main/index.ts
- 2: https://jsr.io/@noble/ed25519/doc/~/verify
🏁 Script executed:
# Find and inspect the local-signer.ts file
fd -t f "local-signer.ts"Repository: tinyhumansai/tiny.place
Length of output: 102
🏁 Script executed:
# Get the parseSiwsToken function to see if it validates signatureType
rg -A 15 "function parseSiwsToken|const parseSiwsToken" --type tsRepository: tinyhumansai/tiny.place
Length of output: 1067
🏁 Script executed:
# Check the adoptSiws function around lines 254-270 for error handling
rg -B 5 -A 20 "private adoptSiws" --type tsRepository: tinyhumansai/tiny.place
Length of output: 1916
🏁 Script executed:
# Get the complete parseSiwsToken function implementation
rg -A 30 "function parseSiwsToken" --type tsRepository: tinyhumansai/tiny.place
Length of output: 2009
🏁 Script executed:
# Check for other usages of adoptSiws and parseSiwsToken
rg "adoptSiws|parseSiwsToken" --type tsRepository: tinyhumansai/tiny.place
Length of output: 396
🏁 Script executed:
# Look at the full file structure around those functions to understand context
sed -n '230,310p' sdk/typescript/src/local-signer.ts | cat -nRepository: tinyhumansai/tiny.place
Length of output: 3229
🏁 Script executed:
# Check lines 288-301 to see what "Also applies to" means
sed -n '288,301p' sdk/typescript/src/local-signer.ts | cat -nRepository: tinyhumansai/tiny.place
Length of output: 559
Uncaught exceptions during SIWS signature verification bypass graceful fallback.
adoptSiws calls ed25519.verify() without error handling. Per @noble/curves v2.2.0 behavior, this function throws a RangeError when the signature does not have exactly 64 bytes. Since parseSiwsToken does not validate the signatureType field or enforce signature byte length, a persisted token with a malformed signature will throw an uncaught exception, breaking the intended fallback to re-minting a fresh proof.
Add signature length validation before the verify call and wrap it in a try-catch to ensure all malformed tokens are treated as non-adoptable:
🛡️ Proposed hardening
const parsed = JSON.parse(json) as {
signedMessage?: unknown;
signature?: unknown;
+ signatureType?: unknown;
};
if (
typeof parsed.signedMessage !== "string" ||
- typeof parsed.signature !== "string"
+ typeof parsed.signature !== "string" ||
+ parsed.signatureType !== "ed25519"
) {
return undefined;
} const messageBytes = new TextEncoder().encode(message);
+ if (signature.length !== 64) {
+ return false;
+ }
+ try {
- if (!ed25519.verify(signature, messageBytes, this.publicKey)) {
+ if (!ed25519.verify(signature, messageBytes, this.publicKey)) {
return false;
}
+ } catch {
+ return false;
+ }
this.siwsToken = token;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/typescript/src/local-signer.ts` around lines 254 - 270, The adoptSiws
method does not handle exceptions thrown by ed25519.verify() when the signature
has an invalid length, which prevents graceful fallback to re-minting a fresh
proof. Add validation to check that the signature from the parsed token has
exactly 64 bytes before calling ed25519.verify(), and wrap the verify() call in
a try-catch block to catch any RangeError or other exceptions, returning false
in both cases to ensure all malformed tokens are treated as non-adoptable.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e23d68aa4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (token && token !== config.siwsToken) { | ||
| await persistConfig(env, { | ||
| ...config, | ||
| ...(seed ? { secretKey: seed } : {}), |
There was a problem hiding this comment.
Do not persist env-supplied CLI keys
When the real CLI is run with TINYPLACE_SECRET_KEY set, seed comes from the environment, but this new SIWS-token persistence writes { secretKey: seed } into the CLI config whenever the token changes. That regresses the previous env-only behavior and can leave a secret on disk for users or CI jobs that supplied it only ephemerally; persist the key only when it came from the config or was generated by the CLI.
Useful? React with 👍 / 👎.
What
Makes the first-party SDKs (TypeScript, Python, Rust) speak the x402 v2 standard wire format. Pairs with backend PR tinyhumansai/backend-tinyplace#47.
Changes (all three SDKs)
X-Tinyplace-SDK: <lang>/<version>so the backend can recognize first-party clients (the Phase-2 cutover signal). The Rust value usesCARGO_PKG_VERSIONto avoid drift.accepts[]array (promoting thefrom/nonce/expiresAtbinding fields out ofextra, mappingpayTo→to, and treating the rest ofextraas the signed metadata), with a fallback to the legacy top-levelpaymentfield.buildX402PaymentEnvelope/encodeX402PaymentHeader(and snake_case equivalents) produce a base64 x402 v2PaymentPayloadenvelope for theX-PAYMENTheader, carrying tiny.place's authorization signature as the scheme-specificpayload.This is Phase 1; the legacy
paymentfallback is removed in Phase 2 once the fleet has upgraded.Tests
test_x402_standard.py(header, accepts[] parsing, envelope) green. (Two unrelated pre-existing SIWS-signer test failures are not touched by this PR.)tests/x402_standard.rs(4 tests) green; clippy clean on touched files.Summary by CodeRabbit
Release Notes
New Features
Improvements