Add expired payment retry and auto-sign flow#72
Conversation
…sistence When a pending payment expires, it now records an "expired" transaction in the DB for audit trail and the card stays visible with three new action buttons: Retry, Enable Auto Sign & Reply, and Cancel. - Retry re-executes the full x402 payment flow with original params - Enable Auto Sign & Reply upserts an active autoSign policy for the endpoint origin then retries (auto-signs if session key is available) - Cancel dismisses the expired card (rejectPendingPayment now accepts both "pending" and "expired" preconditions) - Dashboard auto-switches to the chain with pending payments on load when the cookie chain has none (auth-aware-providers override) - MCP tools (check-pending, get-result) now also record expired transactions when detecting expiry https://claude.ai/code/session_01LDPLUmKDP2Y7yMJzw2tcub
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…-expiration-oDC8G # Conflicts: # src/app/actions/payments.ts
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Extract expirePaymentWithAudit into data layer to eliminate duplicated expire+transaction logic across 4 call sites and fix race condition where MCP tools could create duplicate expired transactions - Replace silent parseFloat()||0 with explicit isNaN warning log so unparseable payment amounts are flagged instead of silently zeroed - Transition old expired payment to rejected before creating new one on retry, preventing duplicate actionable entries in the dashboard - Harden ensureAutoSignPolicy: throw on invalid URL instead of silently falling back to raw string; validate via validateEndpointPattern - Parallelize getPendingCount + getPendingPaymentChainId in auth-aware-providers with Promise.all - Add compound index (userId, status, chainId, expiresAt) for efficient pending payment queries - Add tests for ensureAutoSignPolicy URL validation and origin extraction https://claude.ai/code/session_01LDPLUmKDP2Y7yMJzw2tcub
…xport - Add 22 e2e tests covering every payment happy path: create→approve→complete, create→approve→fail, create→expire-with-audit, create→reject, expire idempotency (race-safety), dismiss expired, state precondition enforcement, user scoping, and query filters - Make expirePendingPayment private (only used internally by expirePaymentWithAudit — was dead export caught by code audit) https://claude.ai/code/session_01LDPLUmKDP2Y7yMJzw2tcub
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| const [pendingOnCookie, chainWithPending] = await Promise.all([ | ||
| getPendingCount(user.userId, { chainId: initialChainId }), | ||
| getPendingPaymentChainId(user.userId), | ||
| ]); |
There was a problem hiding this comment.
🟡 Auto-switch logic uses inconsistent definitions of 'actionable payments'
The auto-switch logic in auth-aware-providers.tsx checks whether the cookie chain has actionable payments using getPendingCount, which only counts status: "pending" with expiresAt > now (src/lib/data/payments.ts:42-52). However, it determines the switch target using getPendingPaymentChainId, which counts both "pending" and "expired" payments (src/lib/data/payments.ts:240-249).
Root Cause: mismatched query semantics
With this PR, expired payments are now actionable (retry / auto-sign / dismiss). But the pendingOnCookie check still uses the old definition that excludes expired payments:
// getPendingCount: only status="pending" AND expiresAt > now
const [pendingOnCookie, chainWithPending] = await Promise.all([
getPendingCount(user.userId, { chainId: initialChainId }), // ignores expired
getPendingPaymentChainId(user.userId), // includes expired
]);Scenario: User has 3 expired payments on cookie chain (chain A) that they want to retry, and 1 newer pending payment on chain B.
pendingOnCookie= 0 (expired payments on chain A are not counted)chainWithPending= chain B (newer pending payment)- Result: user is auto-switched to chain B, missing the expired payments on chain A
Expected: The cookie chain should be considered as having actionable payments because it has expired payments with retry/auto-sign actions available.
Impact: Users may be silently switched away from chains where they have actionable expired payments, causing confusion when they expect to see those payments.
Prompt for agents
In src/app/auth-aware-providers.tsx, line 34, replace the getPendingCount call with a query that also considers expired payments as actionable. Two options:
1. Add an includeExpired option to getPendingCount in src/lib/data/payments.ts (similar to how getPendingPayments already supports it), and call it with { chainId: initialChainId, includeExpired: true }.
2. Alternatively, reuse getPendingPayments with { chainId: initialChainId, includeExpired: true } and check the length, though this fetches more data than needed.
Option 1 is preferred. In src/lib/data/payments.ts, update getPendingCount to accept an includeExpired option:
export async function getPendingCount(userId: string, options?: { chainId?: number; includeExpired?: boolean }) {
await connectDB();
const filter: Record<string, unknown> = { userId: new Types.ObjectId(userId) };
if (options?.includeExpired) {
filter.status = { $in: ["pending", "expired"] };
} else {
filter.status = "pending";
filter.expiresAt = { $gt: new Date() };
}
if (options?.chainId !== undefined) filter.chainId = options.chainId;
return PendingPayment.countDocuments(filter);
}
Then in src/app/auth-aware-providers.tsx line 34, change to:
getPendingCount(user.userId, { chainId: initialChainId, includeExpired: true })
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
This PR adds support for retrying expired payments and enabling auto-sign policies for future payments. When a pending payment expires, users can now retry the payment request or enable auto-sign to automatically approve future payments from the same endpoint.
Key Changes
expirePendingPaymentActionretryExpiredPaymentaction to re-execute the payment request and create a new pending payment if neededenableAutoSignAndRetryaction that enables auto-sign policy for the endpoint before retrying, allowing future payments to be auto-approved if conditions are metgetPendingPaymentsnow supportsincludeExpiredoption to show recently expired payments on dashboardusePendingPaymentshook updated to pass through theincludeExpiredflaggetPendingPaymentChainIdhelper to find chains with actionable paymentsImplementation Details
useRefto ensure expired payment recording fires only once per card instanceretryPaymentFlowfunction handles both retry and auto-sign+retry pathsensureAutoSignPolicysafely creates or activates auto-sign policieshttps://claude.ai/code/session_01LDPLUmKDP2Y7yMJzw2tcub