Skip to content

Conversation

@Woody4618
Copy link

@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

  • New Features

    • Added Solana devnet (SPL‑USDC) as a payable option alongside Base Sepolia USDC and a Stripe simulated card flow; UI now offers Solana selection, prompts for balances, shows explorer links, and issues verifiable receipts after on‑chain payments.
  • Documentation

    • Expanded demo README and ACK‑Pay docs with Solana examples, payment payload sample, verification guidance, and updated payment flow instructions.

- 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!

}
const signedPayload = await createJwt(payload, {
issuer: client.did,
signer: client.jwtSigner
Copy link
Contributor

Choose a reason for hiding this comment

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

This is one of those areas where we'll need to update our docs and patterns in ACK to be a bit clearer, but the message here should be signed by the key that made the actual transaction

@coderabbitai
Copy link

coderabbitai bot commented Oct 13, 2025

Walkthrough

Adds Solana devnet support to the payments demo and docs: new Solana dependencies and constants, server-side Solana key generation and payment option, client Solana USDC payment flow and SOL balance helper, receipt-service Solana transaction verification, and updated documentation/examples.

Changes

Cohort / File(s) Summary of changes
Documentation updates
demos/payments/README.md, docs/ack-pay/payment-request-payload.mdx, docs/ack-pay/receipt-verification.mdx, docs/ack-pay/summary.mdx, docs/demos/demo-payments.mdx
Added Solana devnet payment examples and narrative: payment request payload example for SPL‑USDC, Solana-specific receipt verification guidance, and updated demo/overview text referencing Solana flows and memo-based binding.
Dependencies (Solana libs)
demos/payments/package.json
Added Solana-related dependencies and utilities (@solana-program/*, @solana/*, @solana/kit, @solana/addresses, @solana/keys, bs58, gill).
Solana constants
demos/payments/src/constants.ts
Exported new solana configuration object with CAIP-2 chainId for Solana devnet, RPC URL (env override), USDC mint (env override), and commitment set to "confirmed".
Client payment flow (Solana)
demos/payments/src/index.ts
Added Solana payment path and UI integration: Solana RPC/key initialization, payer signer construction, ATA handling, SOL balance checks, SPL‑USDC transfer building/signing (with memo), explorer link exposure, and posting signed verifiable receipt.
Receipt verification (Solana)
demos/payments/src/receipt-service.ts
Added Solana verification branch: fetch parsed tx, extract memos (including base58 decode), verify memo binds to paymentRequestToken (SHA‑256), and validate postTokenBalances for mint/recipient/amount checks.
Server payment options + keys
demos/payments/src/server.ts
Uses ensureSolanaKeys to obtain/generate server Solana keys, exposes server public key, and injects a Solana USDC devnet payment option into paymentOptions.
Utilities: balances
demos/payments/src/utils/ensure-balances.ts
Added ensureSolanaSolBalance(address) to query SOL balance via RPC and prompt for devnet faucet funding until non-zero.
Utilities: keys
demos/payments/src/utils/ensure-private-keys.ts
Added ensureSolanaKeys(pubEnv, secretEnv) to generate/store Ed25519 keypair, derive Solana public address, and persist secret key JSON to env file.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly captures the primary change introduced by this pull request—adding an end-to-end Solana example—without extraneous detail, accurately reflecting the core objective described in the PR.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

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
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.

const pubkey = solAddress(address)
let lamports = 0n
try {
;({ value: lamports } = await rpc
Copy link
Contributor

Choose a reason for hiding this comment

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

This method is pretty hard to read -- but these kinds of lines in particular are a challenge. Please prefer to load into a variable then set the lamports var from there, e.g.

const balance = await rpc...
lamports = balance.value

console.log("Faucet: https://faucet.solana.com/")
const prefilled = `https://faucet.solana.com/?walletAddress=${encodeURIComponent(
address
)}&amount=0.5`
Copy link
Contributor

Choose a reason for hiding this comment

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

There's an airdrop method to handle this for us: https://www.solanakit.com/api/type-aliases/RequestAirdropApi

"@solana/keys": "^4.0.0",
"@solana/kit": "^4.0.0",
"agentcommercekit": "workspace:*",
"bs58": "^6.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

Needed?

"@solana-program/system": "^0.9.0",
"@solana-program/token": "^0.6.0",
"@solana/addresses": "^4.0.0",
"@solana/keys": "^4.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these strictly necessary if we install @solana/kit?

import { getKeypairInfo } from "./utils/keypair-info"
import type { Rpc, Signature } from "@solana/kit"
import type { paymentOptionSchema } from "agentcommercekit/schemas/valibot"
import type { GetTransactionApi } from "gill"
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this is re-exported via solana/kit as well and we can skip the gill import/package.json entry

@@ -1,6 +1,9 @@
// cspell:ignore lamports
Copy link
Contributor

Choose a reason for hiding this comment

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

Can probably add this to the cspell.config.yaml file at the root

export const solana = {
// CAIP-2 chain id for Solana devnet
chainId: caip2ChainIds.solanaDevnet,
// Example RPC; users can override via environment if desired
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for these comments

? "Stripe"
: option.network?.startsWith("solana:")
? "Solana"
: "Base Sepolia",
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be ideal to avoid nested ternary oeprators

receipt = paymentResult.receipt
details = paymentResult.details
} else if (
typeof selectedPaymentOption.network === "string" &&
Copy link
Contributor

Choose a reason for hiding this comment

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

needed?

const kp = await crypto.subtle.generateKey("Ed25519", true, [
"sign",
"verify"
])
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason not to use the key generation methods already exposed via ACK?

I believe this whole method could be greatly simplified by using those.

@@ -25,32 +25,18 @@
   if (existingPub && existingSecret) {
     return { publicKey: existingPub, secretKeyJson: existingSecret }
   }
   log(colors.dim(`Generating ${pubEnv}/${secretEnv}...`))
-  const kp = await crypto.subtle.generateKey("Ed25519", true, [
-    "sign",
-    "verify"
-  ])
 
-  const privateKeyJwk = await crypto.subtle.exportKey("jwk", kp.privateKey)
-  const privateKeyBase64 = privateKeyJwk.d
-  if (!privateKeyBase64) throw new Error("Failed to get private key bytes")
+  const kp = await generateKeypair("Ed25519")
 
-  const privateKeyBytes = new Uint8Array(
-    Buffer.from(privateKeyBase64, "base64url")
-  )
-  // Export raw 32-byte public key from SPKI (last 32 bytes of the DER-encoded key)
-  const publicKeySpki = await crypto.subtle.exportKey("spki", kp.publicKey)
-  const publicKeyBytes = new Uint8Array(publicKeySpki).slice(-32)
   // Concatenate 32-byte private key + 32-byte public key => 64-byte secret key
-  const secretKey64 = new Uint8Array(
-    privateKeyBytes.length + publicKeyBytes.length
-  )
-  secretKey64.set(privateKeyBytes, 0)
-  secretKey64.set(publicKeyBytes, privateKeyBytes.length)
+  const secretKey64 = new Uint8Array(kp.privateKey.length + kp.publicKey.length)
+  secretKey64.set(kp.privateKey, 0)
+  secretKey64.set(kp.publicKey, kp.privateKey.length)
   const secretKeyJson = JSON.stringify(Array.from(secretKey64))
 
-  const publicKey = await getAddressFromPublicKey(kp.publicKey)
+  const publicKey = bytesToBase58(kp.publicKey)

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