Parallel analysis lane
In the repo /home/akitav2/projects/4626, read frontend/api/_handlers/creator/strategy/_x402-activate.ts in full, and frontend/server/_lib/creatorStrategy/activations.ts (focus on hasLiveActivationForFeature ~line 114, insertPendingActivation, upsertPaymentOrder, recordPaymentEvent, isUniqueViolation). Also check how settleX402Payment works (find where it's defined) and how the surrounding transaction (runInTransaction) is structured.
Report back:
- Exact line numbers where settleX402Payment is called vs where hasLiveActivationForFeature / insertPendingActivation run, and whether there's any entitlement pre-check before settlement.
- The exact transaction scope: which writes (insertPendingActivation, upsertPaymentOrder, recordPaymentEvent) share the same transaction and what happens on insert failure (409 path, db_error path).
- Function signatures needed to add a pre-check (hasLiveActivationForFeature signature, what db handle it needs) and where an "orphaned settlement" durable record could be written outside the transaction (is there an existing payment_events table/helper usable with a non-tx db handle?).
- Where existing tests for this handler live (search frontend/api/tests for x402 / creator-strategy tests) and how they mock settle/db. Be precise with file:line citations.
Analysis
Analyzing the x402 activation flow: reading the handler, activation helpers, and locating settleX402Payment and related tests.
[REDACTED]
Tools invoked: UpdateCurrentStep, Read, Read, Grep, Grep
Analysis
[REDACTED]
Tools invoked: Read, Read, Read, Grep, Glob
Analysis
[REDACTED]
Tools invoked: Grep, Grep, Read, Grep
Analysis
[REDACTED]
Tools invoked: Grep, Grep, Read, Glob
Analysis
[REDACTED]
Tools invoked: Grep, Glob, Read, Grep
Analysis
[REDACTED]
Tools invoked: Grep, Read, Grep, Grep
Analysis
[REDACTED]
Tools invoked: Read, Read, Grep
Analysis
[REDACTED]
Tools invoked: Grep, Grep, Read, Grep
Analysis
[REDACTED]
Tools invoked: Read, Read
Analysis
1. Call order and entitlement pre-check
settleX402Payment is called at line 206, after static validation and before any DB persistence:
const staticValidation = validateX402Authorization({
payment: parsed.payment,
expectedFrom: sessionAddress,
expectedTo: treasury,
minAmount: pricing.effectivePriceUsdc,
})
if (!staticValidation.ok) {
return res.status(400).json({
success: false,
error: 'x402_authorization_invalid',
reason: staticValidation.reason,
} satisfies ApiEnvelope<never>)
}
const settlement = await settleX402Payment(parsed.payment)
if (!settlement.ok) {
// ... error response 207-220
}
insertPendingActivation runs at line 226, inside runInTransaction starting at line 225 — only after settlement succeeds.
hasLiveActivationForFeature is not imported or called anywhere in _x402-activate.ts. The handler never runs an entitlement pre-check before settlement.
Pre-settlement checks that do exist (lines 93–204):
| Step | Lines | What |
|---|---|---|
| Session auth | 93–99 | getSessionAddress |
| Rate limit | 101–113 | |
| Body / feature validation | 118–144 | creatorToken, retired feature, catalog, à-la-carte block |
| DB availability | 146–156 | isDbConfigured, getDb |
| Pricing | 160–165 | findActivePriceOverride, applyPriceOverride |
| Header parse | 183–190 | parseXPaymentHeader |
| Static auth validation | 192–204 | validateX402Authorization |
| On-chain settlement | 206 | settleX402Payment — broadcasts EIP-3009 tx |
There is no hasLiveActivationForFeature (or equivalent SELECT) before line 206.
By contrast, hasLiveActivationForFeature is used elsewhere (e.g. deploy paywall) at:
export async function hasAnyFeatureActivation(params: {
db: DbLike
creatorToken: Address
featureKeys: readonly CreatorStrategyFeatureKey[]
}): Promise<boolean> {
for (const featureKey of params.featureKeys) {
const ok = await hasLiveActivationForFeature(params.db as any, {
creatorToken: params.creatorToken,
featureKey,
})
if (ok) return true
}
return false
}
2. Transaction scope and failure paths
runInTransaction implementation
export async function runInTransaction<T>(fn: (db: DbPool) => Promise<T>): Promise<T | null> {
const db = await getDb()
if (!db) return null
const pool = cachedRawPool
if (!pool || typeof pool.connect !== 'function') {
return fn(db)
}
const client = await pool.connect()
const txDb = buildClientDb(client)
try {
await client.query('BEGIN')
const result = await fn(txDb)
await client.query('COMMIT')
return result
} catch (error) {
await client.query('ROLLBACK').catch(() => {})
throw error
} finally {
client.release()
}
}
Handler transaction block (lines 223–286)
All three writes share txDb inside one callback:
const persistedActivation = await (async () => {
try {
return await runInTransaction(async (txDb) => {
const insertResult = await insertPendingActivation(txDb as any, { ... })
if (!insertResult.ok) return { ok: false as const, insertResult }
await upsertPaymentOrder({ db: txDb as any, orderId: `activation:${insertResult.row.id}`, ... })
await recordPaymentEvent({ db: txDb as any, provider: 'x402', ... })
return { ok: true as const, row: insertResult.row }
})
} catch (error) {
return {
ok: false as const,
reason: 'db_error' as const,
message: error instanceof Error ? error.message : String(error),
}
}
})()
| Outcome | DB writes | HTTP |
|---|---|---|
insertPendingActivation returns !ok (unique violation caught inside helper, no throw) | None — early return at line 247 skips upsertPaymentOrder / recordPaymentEvent; tx COMMITs empty | 409 for live_activation_exists or payment_already_used; 500 for db_error (lines 294–306) |
upsertPaymentOrder or recordPaymentEvent throws | ROLLBACK — activation insert rolled back too | 500 via catch → reason: 'db_error' (lines 309–313) |
runInTransaction returns null (no DB) | N/A | 503 “Activation persistence unavailable” (lines 288–291) |
| Success | All three committed atomically | 200 (line 373) |
insertPendingActivation failure mapping (lines 344–363)
} catch (error) {
if (isUniqueViolation(error, 'creator_strategy_features_one_live_per_feature')) {
return {
ok: false,
reason: 'live_activation_exists',
message: 'A pending or active activation already exists for this creator and feature',
}
}
if (isUniqueViolation(error, 'creator_strategy_features_unique_payment_tx')) {
return {
ok: false,
reason: 'payment_already_used',
message: 'This payment tx hash has already been used for an activation',
}
}
return {
ok: false,
reason: 'db_error',
message: error instanceof Error ? error.message : String(error),
}
}
isUniqueViolation (lines 193–200) matches duplicate key value plus constraint name or creator_strategy_features_ prefix.
Critical gap: settlement at line 206 is already on-chain before any of this runs. A 409 from live_activation_exists after settlement = orphaned USDC transfer with no ledger row.
Post-transaction work (outside tx): recordPaymentActivationQueued (319), dispatchProvisioning (343), recordPaymentProvisioningDispatch (358).
3. Pre-check signatures and orphaned-settlement recording
hasLiveActivationForFeature
export async function hasLiveActivationForFeature(
db: Db,
params: { creatorToken: Address; featureKey: string },
): Promise<boolean> {
const creatorKey = params.creatorToken.toLowerCase()
const lookupKeys = listEntitlementLookupKeys(String(params.featureKey ?? ''))
if (lookupKeys.length === 0) return false
for (const featureKey of lookupKeys) {
const result = await db.sql`
SELECT 1
FROM creator_strategy_features
WHERE creator_token = ${creatorKey}
AND feature_key = ${featureKey}
AND status IN ('pending', 'active')
AND payment_verified_at IS NOT NULL
LIMIT 1;
`
if (Array.isArray(result.rows) && result.rows.length > 0) return true
}
return false
}
Db type (same file, lines 14–16):
type Db = {
sql: (strings: TemplateStringsArray, ...values: any[]) => Promise<{ rows: any[] }>
}
Pre-check call site: use the non-transactional db from line 151:
const alreadyEntitled = await hasLiveActivationForFeature(db as any, {
creatorToken,
featureKey: feature.key,
})
if (alreadyEntitled) { /* 409 before settleX402Payment */ }
Nuance: hasLiveActivationForFeature requires payment_verified_at IS NOT NULL. Unpaid Stripe pending rows (no payment_verified_at) would not trip this pre-check, but would still block insertPendingActivation via the partial unique index creator_strategy_features_one_live_per_feature (any pending/active, verified or not). A pre-check alone is weaker than the insert constraint for in-flight Stripe; you may want an additional “any live row” query if you want to block settlement before broadcast for those too.
Orphaned settlement durable record (outside rolled-back tx)
Existing helper: recordPaymentEvent in paymentLedger.ts:
export async function recordPaymentEvent(params: {
db: Db
provider: 'stripe' | 'x402' | 'manual'
providerEventId: string
orderId: string
eventType: string
amountAtomic?: bigint | null
currency?: string | null
payload?: Record<string, unknown> | null
}): Promise<void> {
// INSERT INTO public.payment_events ... ON CONFLICT (provider, provider_event_id) DO NOTHING
}
- Accepts any
Dbhandle — does not require a transaction;dbfromgetDb()(line 151) works. orderIdis typed asstringbut DB column is nullable FK topayment_orders(supabase/migrations/20260518190000_control_plane_stages_events_and_payment_ledgers.sqllines 206–210). For orphaned events with no activation/order, you'd need eitherorderId: ''(would FK-fail) or extend the helper to acceptnull/ use a dedicated synthetic order created viaupsertPaymentOrderfirst.- Idempotent via
(provider, provider_event_id)unique index — safe to call after failed insert withproviderEventId: settlement.txHash. - Suggested pattern: after post-settlement insert failure, outside the rolled-back/failed tx:
await recordPaymentEvent({
db: db as any, // non-tx handle from line 151
provider: 'x402',
providerEventId: settlement.txHash,
orderId: `orphaned:${settlement.txHash}`, // requires payment_orders row OR helper change for null
eventType: 'x402.settlement_orphaned',
amountAtomic: settlement.value,
currency: 'USDC',
payload: { creatorToken, featureKey, reason: insertResult.reason, ... },
})
For FK compliance without an activation row, either:
upsertPaymentOrderfirst (non-tx) withorderId: 'orphaned-x402:0x…',status: 'manual_review', thenrecordPaymentEventreferencing that order, or- Extend
recordPaymentEventto alloworderId: null(schema supports it; TypeScript signature currently does not).
Also available: public.payment_rail_attempts (migration lines 248–259) — no exported helper in paymentLedger.ts; would need new code.
settleX402Payment definition
export async function settleX402Payment(
payment: X402PaymentPayload,
deps: SettleDeps = {},
): Promise<
| { ok: true; txHash: Hex; blockNumber: bigint; from: Address; to: Address; value: bigint }
| { ok: false; reason: 'x402_relayer_not_configured' | 'x402_settlement_reverted' | 'x402_rpc_error' | 'x402_transfer_not_found'; message: string }
>
Broadcasts via relayer (sendTransaction ~326), waits for receipt (~338), parses Transfer log (~352–377). Optional deps.publicClient, deps.walletClient, deps.relayer for tests.
4. Tests
No handler-level tests for _x402-activate.ts under frontend/api/__tests__/ (grep finds zero files matching x402-activate or importing the handler).
Closest tests:
| File | Scope | Mocking |
|---|---|---|
| frontend/server/_lib/creatorStrategy/x402.test.ts | buildPaymentRequirements, parseXPaymentHeader, validateX402Authorization only | No settleX402Payment, no DB, no handler |
| frontend/api/__tests__/paymentControlPlane.test.ts | recordPaymentActivationQueued / recordPaymentProvisioningDispatch | Mocks @4626/server-core control-plane ops; includes one x402 provider example (lines 84–94) but not the activate handler |
| frontend/api/__tests__/erc8004PaidReview.test.ts | Different x402 surface (evaluateX402Payment from payments/x402Service) | Unrelated to creator strategy |
No tests for hasLiveActivationForFeature, insertPendingActivation, recordPaymentEvent, or settleX402Payment anywhere in frontend/**/*.test.ts.
Legacy USDC hash path _activate.ts mirrors the same post-settlement transaction pattern (lines 178–220) and also lacks a pre-settlement hasLiveActivationForFeature call — it verifies an existing tx hash instead of broadcasting.
Summary diagram
402 path (no header) → return 402 (172-179)
X-PAYMENT path:
parse + validate → 400 (183-204)
settleX402Payment → 206 ← ON-CHAIN (irreversible)
runInTransaction:
insertPendingActivation → 226
upsertPaymentOrder → 249 (skipped if insert !ok)
recordPaymentEvent → 262 (skipped if insert !ok)
control plane + provision → 319+ (outside tx)
The audit note in docs/audits/full-repo-review-2026-06.md (~line 115) correctly identifies settlement-before-pre-check as the orphan-settlement risk.
[REDACTED]
Was this page helpful?