/** * Stripe customer reconciliation for crypto payment flows. * * On each successful crypto payment (Tempo or Base/USDC), we do a best-effort * lookup before creating so the same payer isn't duplicated: * 1. Search by email (most reliable cross-session identifier) * 2. Search by crypto_address in customer metadata * 3. Create a new customer if neither matches * * Enriches an existing customer record if the newly-seen payment carries an * identifier the record is missing (e.g. first payment had only a wallet * address; a later payment includes an email). */ import type Stripe from 'stripe' export type CryptoCustomerInput = { /** Payer's EVM wallet address (e.g. from Base/USDC x-payment header) */ cryptoAddress?: string /** Email from request _meta.email */ email?: string /** Phone from request _meta.phone */ phone?: string /** Payment method that triggered this reconciliation ('tempo' | 'base') */ paymentMethod?: string /** Extra key/value pairs from request _meta.customer_metadata to store on the Stripe customer */ customerMetadata?: Record } /** * Look up an existing Stripe customer by email then by crypto wallet address, * creating one only when neither search returns a match. * * Returns null when no usable identifier is available. */ export async function findOrCreateStripeCustomer( client: Stripe, input: CryptoCustomerInput, ): Promise { const { cryptoAddress, email, phone, paymentMethod, customerMetadata } = input if (!email && !cryptoAddress) return null // ------------------------------------------------------------------ // 1. Lookup by email — most reliable deduplication key // ------------------------------------------------------------------ if (email) { const byEmail = await client.customers.list({ email, limit: 1 }) if (byEmail.data.length > 0) { const existing = byEmail.data[0] // Opportunistically add crypto address if this customer lacks one if (cryptoAddress && !existing.metadata?.['crypto_address']) { return client.customers.update(existing.id, { metadata: { ...existing.metadata, ...customerMetadata, crypto_address: cryptoAddress }, }) as Promise } if (customerMetadata) { return client.customers.update(existing.id, { metadata: { ...existing.metadata, ...customerMetadata }, }) as Promise } return existing } } // ------------------------------------------------------------------ // 2. Lookup by crypto address stored in customer metadata // ------------------------------------------------------------------ if (cryptoAddress) { // Sanitize: Ethereum addresses are 0x + 40 hex chars — safe in query string const safeAddress = /^0x[0-9a-fA-F]{1,64}$/.test(cryptoAddress) ? cryptoAddress : null if (safeAddress) { const byAddress = await client.customers.search({ query: `metadata['crypto_address']:'${safeAddress}'`, limit: 1, }) if (byAddress.data.length > 0) { const existing = byAddress.data[0] // Opportunistically add email if this customer lacks one if (email && !existing.email) { return client.customers.update(existing.id, { email, ...(customerMetadata ? { metadata: { ...existing.metadata, ...customerMetadata } } : {}), }) as Promise } if (customerMetadata) { return client.customers.update(existing.id, { metadata: { ...existing.metadata, ...customerMetadata }, }) as Promise } return existing } } } // ------------------------------------------------------------------ // 3. Create a new customer // ------------------------------------------------------------------ const metadata: Record = { ...customerMetadata } if (cryptoAddress) metadata['crypto_address'] = cryptoAddress if (paymentMethod) metadata['payment_method'] = paymentMethod const params: Stripe.CustomerCreateParams = { metadata } if (email) params.email = email if (phone) params.phone = phone return client.customers.create(params) as Promise }