Skip to content

Add e2e solana example#37

Merged
venables merged 9 commits intoagentcommercekit:mainfrom
Woody4618:add-e2e-solana-example
Feb 14, 2026
Merged

Add e2e solana example#37
venables merged 9 commits intoagentcommercekit:mainfrom
Woody4618:add-e2e-solana-example

Conversation

@Woody4618
Copy link
Contributor

@Woody4618 Woody4618 commented Sep 29, 2025

I added an e2e example using Solana Devnet and USDC mint.
I also added a memo instruction to prevent replay attacks that is vcalidated in the receipts service.

image

Here is an example payment:
https://explorer.solana.com/tx/4y19PDDTBnFCRGtmcSK9egmx6mYMZ1YD8ijJ9vCKZz3uWSzuCnweeo6Fc12hJ8o9T75ANof55JYVKRyNuevoxHz5?cluster=devnet

Here the successful demo succeeding locally:
image

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Solana devnet as an additional payment option alongside Base Sepolia and Stripe
  • Bug Fixes

    • Fixed broken documentation link paths
    • Corrected context URL in payment receipt example
  • Documentation

    • Updated demo documentation and payment request examples with Solana devnet configuration
  • Chores

    • Updated build scripts and added Solana-related dependencies
    • Expanded spell check dictionary

- fixed some type errors
- added a memo instruction that is verified in the receipt service to prevent replay attacks
Copy link

@gitteri gitteri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some documentation around how Solana works for agentic payments. There are a number of references to Base / EVM in doc files
https://github.com/search?q=repo%3Aagentcommercekit%2Fack+path%3A%2F%5Edocs%5C%2F%2F++base&type=code

@venables
Copy link
Contributor

venables commented Oct 9, 2025

Thanks @Woody4618 - digging into this now. Going a bit slower because the Circle Faucet is not working for Solana Devnet at the moment, but will have more soon!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
demos/payments/src/server.ts (1)

19-27: Remove unused type guard.

The isSolanaKeysResult type guard is defined but never used. TypeScript already provides the correct typing for ensureSolanaKeys() return value, making this type guard unnecessary.

Apply this diff to remove the unused code:

-function isSolanaKeysResult(
-  v: unknown
-): v is { publicKey: string; secretKeyJson: string } {
-  return (
-    !!v &&
-    typeof (v as { publicKey?: unknown }).publicKey === "string" &&
-    typeof (v as { secretKeyJson?: unknown }).secretKeyJson === "string"
-  )
-}
-

Based on past review comment from venables.

🧹 Nitpick comments (4)
demos/payments/src/utils/ensure-balances.ts (1)

48-73: Consider adding error handling for RPC calls.

The function correctly implements the balance-checking flow with user prompts, but RPC calls to getBalance lack error handling. Network failures or RPC unavailability could cause unhandled exceptions.

Consider wrapping the RPC call in a try-catch block:

   while (lamports === BigInt(0)) {
     console.log("We need to fund this Solana address with devnet SOL:", address)
     console.log("Faucet: https://faucet.solana.com/")
     const prefilled = `https://faucet.solana.com/?walletAddress=${encodeURIComponent(
       address
     )}&amount=0.5`
     console.log("Prefilled faucet (0.5 SOL):", prefilled)
     console.log("Once funded, press enter to check balance again")
     await waitForEnter()
     console.log("Attempting to fetch SOL balance...  " + pubkey)
-    ;({ value: lamports } = await rpc
-      .getBalance(pubkey, { commitment: solana.commitment })
-      .send())
+    try {
+      ;({ value: lamports } = await rpc
+        .getBalance(pubkey, { commitment: solana.commitment })
+        .send())
+    } catch (error) {
+      console.error("Failed to fetch balance:", error)
+      console.log("Will retry after next attempt...")
+      continue
+    }
     console.log("SOL balance fetched (lamports):", lamports)
   }

Note: The same error handling could be added to the initial balance fetch on line 51-53.

demos/payments/src/index.ts (2)

484-487: Add empty accounts array to memo instruction.

Some instruction builders expect accounts. Make it explicit to avoid encoding issues.

-  const memoInstruction = {
-    programAddress: address("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
-    data: new TextEncoder().encode(expectedMemo)
-  }
+  const memoInstruction = {
+    programAddress: address("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
+    accounts: [],
+    data: new TextEncoder().encode(expectedMemo)
+  }

571-573: Avoid skipPreflight: true in production flows.

Preflight catches account/mint/ATA mistakes early. Consider turning it off by default or gating via env.

-    .sendTransaction(wireTx, { encoding: "base64", skipPreflight: true })
+    .sendTransaction(wireTx, { encoding: "base64", skipPreflight: false })

Or:

- skipPreflight: true
+ skipPreflight: process.env.SOLANA_SKIP_PREFLIGHT === "1"
demos/payments/src/receipt-service.ts (1)

276-285: Make tx polling a bit more robust.

RPC availability can lag; consider increasing attempts or adding backoff.

-  const maxAttempts = 10
-  const delayMs = 1000
+  const maxAttempts = 20
+  const delayMs = 1500

Optionally exponential backoff and early exit on confirmationStatus === "confirmed".

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95aec55 and d63da19.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • demos/payments/README.md (2 hunks)
  • demos/payments/package.json (1 hunks)
  • demos/payments/src/constants.ts (1 hunks)
  • demos/payments/src/index.ts (6 hunks)
  • demos/payments/src/receipt-service.ts (5 hunks)
  • demos/payments/src/server.ts (4 hunks)
  • demos/payments/src/utils/ensure-balances.ts (2 hunks)
  • demos/payments/src/utils/ensure-private-keys.ts (2 hunks)
  • docs/ack-pay/payment-request-payload.mdx (2 hunks)
  • docs/ack-pay/receipt-verification.mdx (1 hunks)
  • docs/ack-pay/summary.mdx (1 hunks)
  • docs/demos/demo-payments.mdx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
demos/payments/src/server.ts (2)
demos/payments/src/utils/ensure-private-keys.ts (1)
  • ensureSolanaKeys (19-59)
demos/payments/src/constants.ts (1)
  • RECEIPT_SERVICE_URL (10-10)
demos/payments/src/utils/ensure-private-keys.ts (2)
tools/cli-tools/src/prompts.ts (1)
  • log (35-54)
demos/payments/src/constants.ts (1)
  • envFilePath (17-17)
demos/payments/src/constants.ts (1)
packages/caip/src/caips/caip-2.ts (1)
  • caip2ChainIds (30-39)
demos/payments/src/utils/ensure-balances.ts (2)
demos/payments/src/constants.ts (1)
  • solana (31-42)
tools/cli-tools/src/prompts.ts (1)
  • waitForEnter (8-14)
demos/payments/src/receipt-service.ts (2)
demos/payments/src/constants.ts (1)
  • solana (31-42)
packages/ack-pay/src/schemas/valibot.ts (1)
  • paymentOptionSchema (7-16)
demos/payments/src/index.ts (8)
demos/payments/src/utils/keypair-info.ts (1)
  • KeypairInfo (15-24)
tools/cli-tools/src/prompts.ts (2)
  • log (35-54)
  • waitForEnter (8-14)
demos/payments/src/constants.ts (1)
  • solana (31-42)
demos/payments/src/utils/ensure-private-keys.ts (1)
  • ensureSolanaKeys (19-59)
demos/payments/src/utils/ensure-balances.ts (1)
  • ensureSolanaSolBalance (48-73)
packages/jwt/src/signer.ts (1)
  • createJwtSigner (17-28)
packages/did/src/methods/did-pkh.ts (1)
  • createDidPkhUri (138-143)
packages/jwt/src/create-jwt.ts (1)
  • createJwt (29-44)
🔇 Additional comments (11)
docs/ack-pay/summary.mdx (1)

8-8: LGTM!

The documentation accurately reflects the new Solana on-chain payment support added in this PR. The explanation is clear and correctly positions on-chain examples (EVM and Solana) alongside traditional finance and card network options.

demos/payments/README.md (1)

9-9: LGTM!

The documentation accurately reflects the expanded payment options now available in the demo, including the new Solana devnet path. The updates are clear and align with the implementation changes in this PR.

Also applies to: 54-55

docs/ack-pay/payment-request-payload.mdx (1)

49-59: LGTM!

The new Solana payment option example is well-documented with appropriate CAIP-2 chain ID format for Solana devnet. The explanatory note about Solana-specific addressing (CAIP-2 chain IDs and base58 wallet addresses) is helpful for implementers.

Also applies to: 102-102

docs/ack-pay/receipt-verification.mdx (1)

107-118: LGTM!

The on-chain verification steps for Solana are well-documented and provide clear guidance on how memo-based binding, mint validation, recipient verification, and balance delta checks should be performed. This complements the general VC verification process effectively.

demos/payments/src/server.ts (2)

51-56: LGTM!

The Solana key initialization is correct. The keys are properly ensured at request time and the public key is extracted for use as the recipient address in the payment option.


99-109: LGTM!

The Solana payment option is properly configured with the correct CAIP-2 chain ID for devnet, appropriate amount/decimals for USDC, and uses the server's Solana public key as the recipient. The configuration aligns with the documented payment request payload format.

docs/demos/demo-payments.mdx (1)

69-73: LGTM!

The documentation clearly describes the Solana devnet payment path, including the important detail about memo-based binding for replay protection. This helps users understand how the Solana flow differs from the Base Sepolia path.

demos/payments/src/utils/ensure-balances.ts (1)

48-73: Otherwise, LGTM!

The balance-checking implementation follows the same pattern as the existing EVM balance check (ensureNonZeroBalances) and provides helpful faucet URLs with a prefilled link for user convenience. The commitment level is correctly pulled from the Solana configuration.

demos/payments/package.json (1)

32-36: Confirm latest package versions: both @solana-program/system@^0.9.0 and @solana-program/token@^0.6.0 are the current stable releases with no reported vulnerabilities.

demos/payments/src/utils/ensure-private-keys.ts (2)

29-33: Ensure WebCrypto Ed25519 is available in your Node runtime.

crypto.subtle.generateKey("Ed25519", ...) requires modern Node (WebCrypto enabled). If portability is a concern, import explicitly.

Apply this optional import to avoid reliance on globals:

+import { webcrypto as crypto } from "node:crypto"

Run node -v and ensure CI uses a Node version supporting Ed25519 WebCrypto.


41-44: Use raw Ed25519 export and pass bytes to getAddressFromPublicKey

  • Replace exportKey("spki")+.slice(-32) with await crypto.subtle.exportKey("raw", kp.publicKey) and new Uint8Array(...).
  • Confirm getAddressFromPublicKey takes a Uint8Array and is synchronous (remove await and pass publicKeyBytes).

- Verify Issues key to be a signer
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
demos/payments/src/server.ts (1)

41-46: Cache Solana keys at startup instead of per-request.

Calling ensureSolanaKeys on every request is inefficient. If the keys exist in process.env, this repeatedly reads environment variables; if they don't exist, key generation blocks the request.

Move this call to module initialization (outside the request handler) to compute it once at startup:

+const solanaKeys = await ensureSolanaKeys(
+  "SOLANA_SERVER_PUBLIC_KEY",
+  "SOLANA_SERVER_SECRET_KEY_JSON"
+)
+const solanaServerPublicKey = solanaKeys.publicKey
+
 const app = new Hono<Env>()

Then reference solanaServerPublicKey directly in the request handler:

 app.get("/", async (c): Promise<TypedResponse<{ message: string }>> => {
   const serverIdentity = await getKeypairInfo(env(c).SERVER_PRIVATE_KEY_HEX)
   const didResolver = getDidResolver()
 
-  // Ensure Solana server keys are present
-  const solanaKeys = await ensureSolanaKeys(
-    "SOLANA_SERVER_PUBLIC_KEY",
-    "SOLANA_SERVER_SECRET_KEY_JSON"
-  )
-  const solanaServerPublicKey: string = solanaKeys.publicKey
-
   const { did: receiptIssuerDid } = await getKeypairInfo(
demos/payments/src/index.ts (1)

463-471: Validate secret key format.

The code assumes clientSolKeys.secretKeyJson is a valid JSON array representing a 64-byte Solana keypair, but doesn't validate the format or length before passing it to createKeyPairSignerFromBytes.

Add validation to ensure the key is 64 bytes:

   const keyBytes = new Uint8Array(
     JSON.parse(clientSolKeys.secretKeyJson) as number[]
   )
+  if (keyBytes.length !== 64) {
+    throw new Error("Invalid Solana secret key length (expected 64 bytes)")
+  }
   const payerSigner = await createKeyPairSignerFromBytes(keyBytes)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d63da19 and 1d7ff40.

📒 Files selected for processing (5)
  • demos/payments/src/index.ts (6 hunks)
  • demos/payments/src/receipt-service.ts (5 hunks)
  • demos/payments/src/server.ts (4 hunks)
  • demos/payments/src/utils/ensure-balances.ts (2 hunks)
  • demos/payments/src/utils/ensure-private-keys.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • demos/payments/src/utils/ensure-private-keys.ts
🧰 Additional context used
🧬 Code graph analysis (4)
demos/payments/src/utils/ensure-balances.ts (2)
demos/payments/src/constants.ts (1)
  • solana (31-42)
tools/cli-tools/src/prompts.ts (1)
  • waitForEnter (8-14)
demos/payments/src/server.ts (2)
demos/payments/src/utils/ensure-private-keys.ts (1)
  • ensureSolanaKeys (19-59)
demos/payments/src/constants.ts (1)
  • RECEIPT_SERVICE_URL (10-10)
demos/payments/src/receipt-service.ts (3)
demos/payments/src/constants.ts (1)
  • solana (31-42)
packages/ack-pay/src/schemas/valibot.ts (1)
  • paymentOptionSchema (7-16)
packages/did/src/methods/did-pkh.ts (1)
  • addressFromDidPkhUri (95-100)
demos/payments/src/index.ts (7)
demos/payments/src/utils/keypair-info.ts (1)
  • KeypairInfo (15-24)
demos/payments/src/constants.ts (1)
  • solana (31-42)
demos/payments/src/utils/ensure-private-keys.ts (1)
  • ensureSolanaKeys (19-59)
demos/payments/src/utils/ensure-balances.ts (1)
  • ensureSolanaSolBalance (49-86)
packages/jwt/src/signer.ts (1)
  • createJwtSigner (17-28)
packages/did/src/methods/did-pkh.ts (1)
  • createDidPkhUri (138-143)
packages/jwt/src/create-jwt.ts (1)
  • createJwt (29-44)
🔇 Additional comments (15)
demos/payments/src/server.ts (2)

14-14: LGTM!

The import follows the existing pattern and is placed appropriately.


89-99: LGTM!

The Solana payment option is properly structured and follows the same pattern as the existing Base Sepolia and Stripe options. The network identifier, amount, decimals, and recipient are all correctly configured for Solana devnet.

demos/payments/src/utils/ensure-balances.ts (2)

1-6: LGTM!

The imports are necessary for the Solana balance checking functionality and follow the existing patterns in the file.


49-86: LGTM!

The function mirrors the pattern established by ensureNonZeroBalances and includes appropriate error handling with retry logic. The user experience is clear, providing faucet URLs and a prefilled link for convenience.

demos/payments/src/receipt-service.ts (6)

1-37: LGTM!

The imports are necessary for Solana transaction verification and follow best practices by using narrowed type imports to minimize dependencies.


121-128: LGTM!

The routing logic correctly dispatches to the Solana verification flow when the network matches the Solana devnet chain ID, with appropriate error handling for unsupported networks.


246-258: LGTM!

The helper function and type definitions are well-structured. Using jsonParsed encoding and the configured commitment level from constants is appropriate for transaction verification.


288-317: LGTM! Signer verification now implemented.

The signer verification correctly ensures the JWT issuer DID matches a transaction signer, addressing the critical security concern raised in the previous review. The implementation properly handles both string and object pubkey formats from the jsonParsed transaction.


376-424: LGTM!

The memo extraction function comprehensively handles both parsed and partially-decoded memo formats, including inner instructions. The error handling for malformed base58 data is appropriate.


331-373: Verify mint address matches expected USDC mint.

The token balance verification checks the mint from the transaction but doesn't validate that it matches solana.usdcMint from constants. An attacker could potentially send a different SPL token with the same amount and decimals.

Add mint validation before the balance checks:

   // Validate postTokenBalances reflect the transfer to recipient for the mint
   const mint = solana.usdcMint
+  
   const recipient =
     typeof paymentOption.recipient === "string"
       ? paymentOption.recipient
       : String(paymentOption.recipient)
 
   const dec = paymentOption.decimals
   const expectedAmount = BigInt(paymentOption.amount)
 
   type TokenBalance = {
     mint: string
     owner: string
     uiTokenAmount: { amount: string; decimals: number }
   }
   const post = (tx.meta?.postTokenBalances ?? []) as unknown as TokenBalance[]
   const pre = (tx.meta?.preTokenBalances ?? []) as unknown as TokenBalance[]
 
   const preBal = pre.find((b) => b.mint === mint && b.owner === recipient)
   const postBal = post.find((b) => b.mint === mint && b.owner === recipient)
 
   if (!postBal) {
     log(errorMessage("Recipient post token balance not found"))
     throw new HTTPException(400, { message: "Recipient not credited" })
   }
+  if (postBal.mint !== mint) {
+    log(errorMessage("Invalid mint address"))
+    throw new HTTPException(400, { message: "Invalid mint" })
+  }
   if (postBal.uiTokenAmount.decimals !== dec) {
     log(errorMessage("Invalid token decimals"))
     throw new HTTPException(400, { message: "Invalid token decimals" })
   }

Likely an incorrect or invalid review comment.

demos/payments/src/index.ts (5)

1-63: LGTM!

The imports are necessary for implementing the Solana payment flow, including transaction construction, signing, and SPL token transfers.


182-196: LGTM!

The UI updates correctly identify Solana-based payment options by checking if the network starts with "solana:", which aligns with the CAIP-2 chain ID format.


218-239: LGTM!

The routing logic correctly dispatches to the Solana payment flow when the network is a string starting with "solana:", with appropriate fallback to other payment methods and error handling.


501-580: LGTM!

The transaction construction is comprehensive and correct:

  • Ensures the sender has USDC balance (prompting Circle faucet if needed)
  • Creates recipient ATA if it doesn't exist
  • Properly constructs the transaction with memo, optional ATA creation, and transfer instructions
  • Uses the pipe pattern for clean transaction building

The memo placement first in the instruction list is good practice.


582-617: LGTM!

The receipt signing flow correctly derives an ACK-compatible Ed25519 keypair from the Solana keypair seed, ensuring cryptographic binding between the Solana transaction signer and the ACK JWT issuer. The payerDid is properly constructed as a did:pkh URI with the Solana chain ID and public key.

- I think the Memo is probably not necessary when we check the JWT signature in the receipt. Will make it easier for agents to use.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d7ff40 and 3d4fcbc.

📒 Files selected for processing (4)
  • demos/payments/src/index.ts (5 hunks)
  • demos/payments/src/receipt-service.ts (4 hunks)
  • docs/ack-pay/receipt-verification.mdx (1 hunks)
  • docs/demos/demo-payments.mdx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
demos/payments/src/receipt-service.ts (3)
demos/payments/src/constants.ts (1)
  • solana (31-42)
packages/ack-pay/src/schemas/valibot.ts (1)
  • paymentOptionSchema (7-16)
packages/did/src/methods/did-pkh.ts (1)
  • addressFromDidPkhUri (95-100)
demos/payments/src/index.ts (8)
demos/payments/src/utils/keypair-info.ts (1)
  • KeypairInfo (15-24)
tools/cli-tools/src/prompts.ts (2)
  • log (35-54)
  • waitForEnter (8-14)
demos/payments/src/constants.ts (1)
  • solana (31-42)
demos/payments/src/utils/ensure-private-keys.ts (1)
  • ensureSolanaKeys (19-59)
demos/payments/src/utils/ensure-balances.ts (1)
  • ensureSolanaSolBalance (49-86)
packages/jwt/src/signer.ts (1)
  • createJwtSigner (17-28)
packages/did/src/methods/did-pkh.ts (1)
  • createDidPkhUri (138-143)
packages/jwt/src/create-jwt.ts (1)
  • createJwt (29-44)
🪛 LanguageTool
docs/demos/demo-payments.mdx

[grammar] ~70-~70: There might be a mistake here.
Context: ...ctly to the Server’s Solana address. The Receipt Service verifies the mint, recip...

(QB_NEW_EN)


[grammar] ~71-~71: There might be a mistake here.
Context: ...ecipient and exact amount before issuing a receipt. <Step title="Receip...

(QB_NEW_EN)

docs/ack-pay/receipt-verification.mdx

[grammar] ~113-~113: There might be a mistake here.
Context: ...iated token account for recipient+mint). - Compute the delta in recipient `postToke...

(QB_NEW_EN)

🔇 Additional comments (15)
docs/demos/demo-payments.mdx (1)

68-73: LGTM! Clear documentation of the Solana payment flow.

The added documentation clearly describes the Solana devnet payment path, including the transfer of SPL-USDC and verification by the Receipt Service.

docs/ack-pay/receipt-verification.mdx (1)

106-117: LGTM! Comprehensive Solana verification documentation.

The new section provides clear guidance on verifying Solana on-chain payments, covering all essential verification steps including transaction fetching, mint/recipient validation, balance delta computation, and idempotency checks.

demos/payments/src/receipt-service.ts (4)

10-10: LGTM! Appropriate imports for Solana verification.

The imports are minimal and well-structured, with type-only imports used where appropriate to minimize dependencies.

Also applies to: 12-12, 28-28, 31-31, 33-33


118-119: LGTM! Consistent routing for Solana payments.

The routing logic follows the same pattern as other payment methods and correctly dispatches to the Solana verification function.


243-251: LGTM! Clean transaction fetching helper.

The helper function appropriately configures the transaction fetch with the correct commitment level and encoding for verification needs.


255-358: LGTM! Comprehensive Solana payment verification.

The verification function properly implements all necessary checks:

  1. Network validation (lines 260-263): Ensures correct chain
  2. Transaction polling (lines 269-281): Includes retry logic with appropriate delays
  3. Signer verification (lines 283-312): Correctly binds JWT issuer DID to transaction signer, addressing the previous review concern
  4. Token balance verification (lines 314-357): Validates mint, recipient, decimals, and amount delta

The implementation includes appropriate error handling and type safety measures throughout.

demos/payments/src/index.ts (9)

15-32: LGTM! Well-organized Solana imports.

The imports are properly grouped by library and include all necessary utilities for constructing, signing, and sending Solana transactions.


33-43: LGTM! Necessary imports for Solana payment flow.

The additional imports provide the required utilities for DID creation, JWT signing, and wallet management for the Solana payment path.

Also applies to: 55-55, 59-62


181-195: LGTM! Clear payment option UI for Solana.

The updated UI logic correctly identifies Solana networks using the CAIP-2 format and provides clear, user-friendly labels and descriptions.


217-227: LGTM! Consistent routing for Solana payments.

The routing logic properly type-checks the network field and calls the Solana payment handler when appropriate.


450-475: LGTM! Proper initialization and key setup.

The function correctly initializes the Solana RPC connection, loads client keys, and creates the payer signer. The SOL balance check ensures the client has funds for transaction fees.


479-488: LGTM! Correct ATA derivation.

The function properly derives the associated token accounts (ATAs) for both the sender and recipient using the standard token program.


490-530: LGTM! Robust balance checking and ATA creation.

The implementation includes:

  1. Balance verification: Checks sender USDC balance with proper error handling
  2. User guidance: Prompts for Circle faucet if balance is zero
  3. Conditional ATA creation: Creates recipient ATA only if it doesn't exist, avoiding unnecessary transactions

532-567: LGTM! Well-constructed Solana transaction.

The transaction construction properly:

  1. Creates the transfer instruction with checked amounts and decimals
  2. Uses the pipe pattern for clean transaction message building
  3. Sets appropriate lifetime using latest blockhash
  4. Signs with the payer signer
  5. Includes optional ATA creation when needed

The transaction flow is clean and follows Solana best practices.


572-593: Ed25519 seed extraction validated. The first 32 bytes of secretKey64 correspond to the JWK d private scalar from ensureSolanaKeys.

@Woody4618
Copy link
Contributor Author

hey @venables What do you think about the PR? I would like to produce some video content about the different x402 options . Would be great to have this merged. I built some examples using ACK here: https://solana-paywal.vercel.app/ and a twitter bot that can pay using ACK to animate images: https://x.com/solanadevhelper/status/1978483896598544449

Copy link
Contributor

@venables venables left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for putting this together @Woody4618 . I think it's getting close.

# Conflicts:
#	demos/payments/package.json
#	demos/payments/src/index.ts
#	demos/payments/src/receipt-service.ts
#	demos/payments/src/server.ts
#	demos/payments/src/utils/ensure-balances.ts
#	demos/payments/src/utils/ensure-private-keys.ts
#	pnpm-lock.yaml
Address review feedback and improve project conformance:

- Remove gill, bs58, @solana/keys, @solana/addresses dependencies
- Use ACK's generateKeypair and bytesToBase58 for Solana key generation
- Replace hardcoded chain ID with solana.chainId constant
- Extract helpers: networkLabel, executePayment, fetchBalance,
  extractSignerPubkeys, fetchTransaction
- Replace console.log/console.error with log() from cli-tools
- Remove unnecessary comments and unused catch parameters
- Add lamports to cspell config
- Clean up docs: placeholder address, remove chain-specific sections
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
demos/payments/src/index.ts (1)

425-431: ⚠️ Potential issue | 🟠 Major

Same missing error handling in performOnChainPayment.

The fetch call to the receipt service here also lacks an ok check, same as in performSolanaPayment. Both should be guarded.

Suggested fix
   const response2 = await fetch(receiptServiceUrl, {
     method: "POST",
     body: JSON.stringify({
       payload: signedPayload,
     }),
   })
+  if (!response2.ok) {
+    throw new Error(errorMessage(`Receipt service returned ${response2.status}`))
+  }
 
   const { receipt, details } = (await response2.json()) as {
🤖 Fix all issues with AI agents
In `@demos/payments/src/index.ts`:
- Around line 584-591: In performSolanaPayment, add HTTP error handling after
the fetch to the receipt service (receiptServiceUrl) by checking response.ok
before calling response.json(); if !response.ok, read response.text() or include
response.status and throw or return a clear error (similar to
performStripePayment’s response1.ok check) so you don't attempt to parse an
error body as JSON and you surface useful status/error details instead of
failing with a confusing parse error.

In `@demos/payments/src/receipt-service.ts`:
- Around line 309-321: The retry loop currently keeps polling for the full
maxAttempts even when fetchTransaction(rpc, signature) returns a transaction
with tx.meta?.err (a permanent failure); update the loop in receipt-service.ts
so that as soon as a transaction is returned (tx != null) you break out of the
for loop regardless of tx.meta?.err, and then after the loop inspect
tx.meta?.err to throw the HTTPException for a failed transaction or proceed for
a successful one; target the variables and functions tx, fetchTransaction, rpc,
signature, tx.meta?.err and the existing for loop/maxAttempts/delayMs logic when
applying this change.
🧹 Nitpick comments (3)
demos/payments/src/receipt-service.ts (1)

289-294: toBigInt silently returns 0n for unexpected types.

If an unexpected type is passed (e.g., undefined, object), toBigInt silently returns 0n, which could mask data-parsing bugs. Consider throwing on unexpected types or at least logging a warning.

demos/payments/src/index.ts (2)

559-563: Simplify the Uint8Array slice.

new Uint8Array(Array.from(keyBytes).slice(0, 32)) converts to Array and back unnecessarily. Uint8Array.prototype.slice already returns a new Uint8Array.

Suggested fix
-  const ackEd25519Keypair = await generateKeypair(
-    "Ed25519",
-    new Uint8Array(Array.from(keyBytes).slice(0, 32)),
-  )
+  const ackEd25519Keypair = await generateKeypair(
+    "Ed25519",
+    keyBytes.slice(0, 32),
+  )

176-180: networkLabel fallback assumes Base Sepolia for any non-Stripe/non-Solana network.

If additional EVM networks are added later, they'd all be labeled "Base Sepolia". Consider matching chainId explicitly and using the network string as a fallback:

Suggested fix
   function networkLabel(network: string | undefined): string {
     if (network === "stripe") return "Stripe"
     if (network?.startsWith("solana:")) return "Solana"
+    if (network === chainId) return "Base Sepolia"
-    return "Base Sepolia"
+    return network ?? "Unknown"
   }

@agentcommercekit agentcommercekit deleted a comment from coderabbitai bot Feb 14, 2026
@venables
Copy link
Contributor

@Woody4618 thanks again for all of this work. i just pushed to this branch to get it through -- there were some merge conflicts along the way.

@venables venables merged commit 71c95cf into agentcommercekit:main Feb 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants