Skip to main content

Parallel analysis lane

[← Source sessions](/audits/fable/key-sessions) · [Transcript archive](/audits/fable/transcripts) · [Executive summary](/audits/fable/findings-summary)
# Parallel analysis lane | Field | Value | | --- | --- | | Session ID | `5ae4abcf-ce2a-48d6-aefb-5f27b6abcf8a` | | Lead session | `059adbec-9820-45a8-9c18-399e4a7f9870` | | Record type | Parallel workstream export | :::note[Appendix record] Readable export of a supplementary review session. Tool outputs and system context are omitted; download the [JSONL archive](/audits/fable-chats-4626-2026-06.zip) for the complete log. ::: ## Engagement brief

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:

  1. Exact line numbers where settleX402Payment is called vs where hasLiveActivationForFeature / insertPendingActivation run, and whether there's any entitlement pre-check before settlement.
  2. The exact transaction scope: which writes (insertPendingActivation, upsertPaymentOrder, recordPaymentEvent) share the same transaction and what happens on insert failure (409 path, db_error path).
  3. 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?).
  4. 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):

StepLinesWhat
Session auth93–99getSessionAddress
Rate limit101–113
Body / feature validation118–144creatorToken, retired feature, catalog, à-la-carte block
DB availability146–156isDbConfigured, getDb
Pricing160–165findActivePriceOverride, applyPriceOverride
Header parse183–190parseXPaymentHeader
Static auth validation192–204validateX402Authorization
On-chain settlement206settleX402Payment — 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),
}
}
})()
OutcomeDB writesHTTP
insertPendingActivation returns !ok (unique violation caught inside helper, no throw)None — early return at line 247 skips upsertPaymentOrder / recordPaymentEvent; tx COMMITs empty409 for live_activation_exists or payment_already_used; 500 for db_error (lines 294–306)
upsertPaymentOrder or recordPaymentEvent throwsROLLBACK — activation insert rolled back too500 via catch → reason: 'db_error' (lines 309–313)
runInTransaction returns null (no DB)N/A503 “Activation persistence unavailable” (lines 288–291)
SuccessAll three committed atomically200 (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 Db handle — does not require a transaction; db from getDb() (line 151) works.
  • orderId is typed as string but DB column is nullable FK to payment_orders (supabase/migrations/20260518190000_control_plane_stages_events_and_payment_ledgers.sql lines 206–210). For orphaned events with no activation/order, you'd need either orderId: '' (would FK-fail) or extend the helper to accept null / use a dedicated synthetic order created via upsertPaymentOrder first.
  • Idempotent via (provider, provider_event_id) unique index — safe to call after failed insert with providerEventId: 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:

  1. upsertPaymentOrder first (non-tx) with orderId: 'orphaned-x402:0x…', status: 'manual_review', then recordPaymentEvent referencing that order, or
  2. Extend recordPaymentEvent to allow orderId: 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:

FileScopeMocking
frontend/server/_lib/creatorStrategy/x402.test.tsbuildPaymentRequirements, parseXPaymentHeader, validateX402Authorization onlyNo settleX402Payment, no DB, no handler
frontend/api/__tests__/paymentControlPlane.test.tsrecordPaymentActivationQueued / recordPaymentProvisioningDispatchMocks @4626/server-core control-plane ops; includes one x402 provider example (lines 84–94) but not the activate handler
frontend/api/__tests__/erc8004PaidReview.test.tsDifferent 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]