Skip to content

Commit 2fc94ff

Browse files
fix(x402): correct Solana instruction assertion and blockhash commitment
- Use compiledInstructions on MessageV0 — tx.message.instructions throws TypeError - Recommend getLatestBlockhash('finalized') to reduce validator timing rejections - Document CDP's 'payer' field in rejections refers to the agent wallet, not feePayer
1 parent 6b35a82 commit 2fc94ff

3 files changed

Lines changed: 8 additions & 3 deletions

File tree

skills/cryptorefills-x402/SKILL.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ const feePayer = new PublicKey(extra.feePayer); // CDP signs this sl
375375

376376
const senderAta = await getAssociatedTokenAddress(mint, signer.publicKey);
377377
const recipientAta = await getAssociatedTokenAddress(mint, owner);
378-
const {blockhash} = await conn.getLatestBlockhash();
378+
const {blockhash} = await conn.getLatestBlockhash('finalized'); // 'finalized' is older but accepted by all validators — reduces timing sensitivity
379379

380380
const message = new TransactionMessage({
381381
payerKey: feePayer,
@@ -390,6 +390,10 @@ const message = new TransactionMessage({
390390

391391
const tx = new VersionedTransaction(message);
392392
tx.sign([signer]); // partial — leaves feePayer slot
393+
// Assert on the pre-compilation array — MessageV0 exposes `compiledInstructions`, not `instructions`.
394+
// Reading `tx.message.instructions` throws TypeError. Use the source array passed to TransactionMessage.
395+
if (message.version !== 0) throw new Error('expected v0 message');
396+
if (tx.message.compiledInstructions.length !== 3) throw new Error('expected exactly 3 instructions');
393397
if (tx.serialize().length > 1232) throw new Error('tx too large');
394398

395399
const txB64 = Buffer.from(tx.serialize()).toString('base64'); // inner: plain base64

skills/cryptorefills-x402/references/protocol.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ The CDP facilitator enforces a 3-to-6 instruction range and rejects payloads out
225225
| Field | Value |
226226
|---|---|
227227
| `payerKey` | `extra.feePayer` from PAYMENT-REQUIRED — CDP signs this slot |
228-
| `recentBlockhash` | Fresh from a Solana RPC (default `https://api.mainnet-beta.solana.com`); validity ~60–90 s on the Solana network, but the gateway session TTL is 60 s — that's the binding deadline |
228+
| `recentBlockhash` | Fresh from a Solana RPC (default `https://api.mainnet-beta.solana.com`) at commitment `'finalized'` — older but accepted by every validator, reducing timing-sensitivity rejections. Validity ~60–90 s on the Solana network, but the gateway session TTL is 60 s — that's the binding deadline |
229229
| `instructions` | The 3 above, in order |
230230
| Compile to | v0 message — `compileToV0Message()` |
231231
| Wrap in | `new VersionedTransaction(message)` |
@@ -268,7 +268,7 @@ The 60-second blockhash window is short. Don't insert any user prompts between P
268268
- **Verify `extra.feePayer` (Solana)**: must match the CDP-published value. A wrong feePayer means CDP refuses to settle.
269269
- **Nonce uniqueness (Base)**: never reuse a nonce — cryptographically random per transaction.
270270
- **Blockhash freshness (Solana)**: re-fetch the blockhash immediately before partial-signing, never reuse.
271-
- **Instruction-count assertion (Solana)**: assert exactly 3 instructions before serializing.
271+
- **Instruction-count assertion (Solana)**: assert exactly 3 instructions before serializing. Assert on `tx.message.compiledInstructions.length` (or the pre-compilation array passed to `TransactionMessage`) — `MessageV0` does not expose `tx.message.instructions`; reading it throws `TypeError`.
272272
- **Domain verification (Base)**: EIP-712 domain must match USDC on Base (chain 8453).
273273
- **Wallet isolation**: dedicated wallet, limited balance per rail.
274274
- **No rail mixing**: a session is pinned to the rail chosen in Phase 1. Don't try to switch rails on Phase 2 — the session ID won't match and the signature is rejected.

skills/cryptorefills-x402/references/troubleshooting.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Error handling and recovery for x402 autonomous purchases. Organized by category
2727
| `unknown_error` from `/verify` | You used SPL `Transfer` (tag `0x03`). Switch to `createTransferCheckedInstruction` (tag `0x0c`). |
2828
| `transaction instructions length mismatch: 1 < 3 or 1 > 6` | Wrong instruction count. Use the exact 3-instruction preamble: ComputeBudget set-limit + ComputeBudget set-price + TransferChecked. |
2929
| `feePayer not managed` | The `extra.feePayer` you used is not recognised by CDP. Restart Phase 1 to get a fresh `extra.feePayer` and use it verbatim. |
30+
| CDP rejection echoes a `"payer":"<pubkey>"` field that doesn't match your `payerKey` | Not a bug. CDP's rejection messages use `"payer"` to mean **the paying agent's wallet** (the SPL `TransferChecked` authority / signer), not the Solana fee payer. The transaction's `payerKey` correctly stays set to `extra.feePayer` (CDP's key) — the field name collision is just CDP's terminology. Don't change `payerKey` in response to this. |
3031
| `blockhash expired` | You took longer than ~60 s between fetching the blockhash and submitting Phase 2. Re-fetch the blockhash and re-sign. |
3132
| Phase 2 rejected with "X-Session-Id required" | You omitted `X-Session-Id` on Solana Phase 2. Echo the UUID from the Phase 1 `X-Session-Id` response header. |
3233
| Wrong `payTo` interpretation | `payTo` is the **owner pubkey, not an ATA**. Derive the recipient ATA from `(payTo, mint)` via `getAssociatedTokenAddress`. |

0 commit comments

Comments
 (0)