diff --git a/cli/src/chat.tsx b/cli/src/chat.tsx
index b555d67ed..6663c7e1e 100644
--- a/cli/src/chat.tsx
+++ b/cli/src/chat.tsx
@@ -11,9 +11,8 @@ import {
} from 'react'
import { useShallow } from 'zustand/react/shallow'
-import { getAdsEnabled, handleAdsDisable } from './commands/ads'
+import { getAdsEnabled } from './commands/ads'
import { routeUserPrompt, addBashMessageToHistory } from './commands/router'
-import { AdBanner } from './components/ad-banner'
import { ChoiceAdBanner } from './components/choice-ad-banner'
import { ChatInputBar } from './components/chat-input-bar'
import { LoadPreviousButton } from './components/load-previous-button'
@@ -175,13 +174,7 @@ export const Chat = ({
})
const hasSubscription = subscriptionData?.hasSubscription ?? false
- const { ad, adData, recordImpression } = useGravityAd({ enabled: IS_FREEBUFF || !hasSubscription })
- const [adsManuallyDisabled, setAdsManuallyDisabled] = useState(false)
-
- const handleDisableAds = useCallback(() => {
- handleAdsDisable()
- setAdsManuallyDisabled(true)
- }, [])
+ const { adData, recordImpression } = useGravityAd({ enabled: IS_FREEBUFF || !hasSubscription })
// Set initial mode from CLI flag on mount
useEffect(() => {
@@ -1466,19 +1459,11 @@ export const Chat = ({
/>
)}
- {ad && (IS_FREEBUFF || (!adsManuallyDisabled && getAdsEnabled())) && (
- adData?.variant === 'choice' ? (
-
- ) : (
-
- )
+ {adData && (IS_FREEBUFF || getAdsEnabled()) && (
+
)}
{reviewMode ? (
diff --git a/cli/src/components/ad-banner.tsx b/cli/src/components/ad-banner.tsx
deleted file mode 100644
index 4910952a7..000000000
--- a/cli/src/components/ad-banner.tsx
+++ /dev/null
@@ -1,236 +0,0 @@
-import { TextAttributes } from '@opentui/core'
-import { safeOpen } from '../utils/open-url'
-import React, { useState } from 'react'
-
-import { Button } from './button'
-import { Clickable } from './clickable'
-import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
-import { useTheme } from '../hooks/use-theme'
-import { IS_FREEBUFF } from '../utils/constants'
-
-import type { AdResponse } from '../hooks/use-gravity-ad'
-
-interface AdBannerProps {
- ad: AdResponse
- onDisableAds: () => void
- isFreeMode: boolean
-}
-
-const extractDomain = (url: string): string => {
- try {
- const parsed = new URL(url)
- return parsed.hostname.replace(/^www\./, '')
- } catch {
- return url
- }
-}
-
-export const AdBanner: React.FC = ({ ad, onDisableAds, isFreeMode }) => {
- const theme = useTheme()
- const { separatorWidth, terminalWidth } = useTerminalDimensions()
- const [isLinkHovered, setIsLinkHovered] = useState(false)
- const [showInfoPanel, setShowInfoPanel] = useState(false)
- const [isAdLabelHovered, setIsAdLabelHovered] = useState(false)
- const [isHideHovered, setIsHideHovered] = useState(false)
- const [isCloseHovered, setIsCloseHovered] = useState(false)
-
- // Use 'url' field for display domain (the actual destination)
- const domain = extractDomain(ad.url)
- // Use cta field for button text, with title as fallback
- const ctaText = ad.cta || ad.title || 'Learn more'
-
- // Calculate available width for ad text
- // Account for: padding (2), "Ad ?" label with space (5)
- const maxTextWidth = separatorWidth - 7
-
- // Wrapper for hover detection - makes entire ad content clickable
- const handleAdMouseOver = () => setIsLinkHovered(true)
- const handleAdMouseOut = () => setIsLinkHovered(false)
- const handleAdClick = () => {
- if (ad.clickUrl) {
- safeOpen(ad.clickUrl)
- }
- }
-
- return (
-
- {/* Horizontal divider line */}
- {'─'.repeat(terminalWidth)}
- {/* Clickable ad content area - wrapped in Button for click detection */}
-
- {/* Info panel: shown when Ad label is clicked, below the ad */}
- {showInfoPanel && (
-
- {' ' + '┄'.repeat(separatorWidth - 2)}
-
-
- {IS_FREEBUFF
- ? 'Ads help keep Freebuff free.'
- : 'Ads are optional. Feel free to hide them anytime.'}
-
-
-
-
- {isFreeMode && !IS_FREEBUFF ? (
-
- Ads are required in Free mode.
-
- ) : (
- <>
-
- ·
-
- Use /ads:enable to show again
-
- >
- )}
-
-
- )}
-
- )
-}
diff --git a/cli/src/components/waiting-room-screen.tsx b/cli/src/components/waiting-room-screen.tsx
index 8913093a2..e67823f7a 100644
--- a/cli/src/components/waiting-room-screen.tsx
+++ b/cli/src/components/waiting-room-screen.tsx
@@ -2,7 +2,6 @@ import { TextAttributes } from '@opentui/core'
import { useRenderer } from '@opentui/react'
import React, { useMemo, useState } from 'react'
-import { AdBanner } from './ad-banner'
import { Button } from './button'
import { ChoiceAdBanner } from './choice-ad-banner'
import { FreebuffModelSelector } from './freebuff-model-selector'
@@ -73,9 +72,11 @@ export const WaitingRoomScreen: React.FC = ({
// Always enable ads in the waiting room — this is where monetization lives.
// forceStart bypasses the "wait for first user message" gate inside the hook,
// which would otherwise block ads here since no conversation exists yet.
- const { ad, adData, recordImpression } = useGravityAd({
+ // Uses Carbon (BuySellAds); in-chat ads still use the Gravity default.
+ const { adData, recordImpression } = useGravityAd({
enabled: true,
forceStart: true,
+ provider: 'carbon',
})
useFreebuffCtrlCExit()
@@ -261,21 +262,17 @@ export const WaitingRoomScreen: React.FC = ({
{/* Ad banner pinned to the bottom, same look-and-feel as in chat. */}
- {ad && (
+ {adData && (
- {adData?.variant === 'choice' ? (
-
- ) : (
- {}} isFreeMode />
- )}
+
)}
{/* Horizontal separator (mirrors chat input divider style) */}
- {!ad && (
+ {!adData && (
{'─'.repeat(terminalWidth)}
diff --git a/cli/src/hooks/helpers/send-message.ts b/cli/src/hooks/helpers/send-message.ts
index 02e419b30..a86870fe5 100644
--- a/cli/src/hooks/helpers/send-message.ts
+++ b/cli/src/hooks/helpers/send-message.ts
@@ -1,15 +1,18 @@
import { getErrorObject } from '@codebuff/common/util/error'
import {
+ markFreebuffSessionCountryBlocked,
markFreebuffSessionEnded,
markFreebuffSessionSuperseded,
refreshFreebuffSession,
} from '../use-freebuff-session'
import { getProjectRoot } from '../../project-files'
import { useChatStore } from '../../state/chat-store'
+import { IS_FREEBUFF } from '../../utils/constants'
import { processBashContext } from '../../utils/bash-context-processor'
import { markRunningAgentsAsCancelled } from '../../utils/block-operations'
import {
+ getCountryCodeFromFreeModeError,
getFreebuffGateErrorKind,
isOutOfCreditsError,
isFreeModeUnavailableError,
@@ -389,6 +392,11 @@ export const handleRunCompletion = (params: {
if (isFreeModeUnavailableError(output)) {
updater.setError(FREE_MODE_UNAVAILABLE_MESSAGE)
+ if (IS_FREEBUFF) {
+ markFreebuffSessionCountryBlocked(
+ getCountryCodeFromFreeModeError(output) ?? 'UNKNOWN',
+ )
+ }
finalizeAfterError()
return
}
@@ -484,6 +492,11 @@ export const handleRunError = (params: {
if (isFreeModeUnavailableError(error)) {
updater.setError(FREE_MODE_UNAVAILABLE_MESSAGE)
+ if (IS_FREEBUFF) {
+ markFreebuffSessionCountryBlocked(
+ getCountryCodeFromFreeModeError(error) ?? 'UNKNOWN',
+ )
+ }
return
}
diff --git a/cli/src/hooks/use-freebuff-session.ts b/cli/src/hooks/use-freebuff-session.ts
index 407d4afd4..79deea1cf 100644
--- a/cli/src/hooks/use-freebuff-session.ts
+++ b/cli/src/hooks/use-freebuff-session.ts
@@ -280,6 +280,21 @@ export function markFreebuffSessionSuperseded(): void {
controller?.apply({ status: 'superseded' })
}
+/** Flip into the terminal `country_blocked` state from outside the poll loop.
+ * Used when the chat-completions gate rejects on country even though the
+ * session-level country check had failed open (null detection → admitted).
+ * Transitioning the session state here unmounts the Chat surface in favor of
+ * the waiting-room's country_blocked message, so the user can't keep typing
+ * and sending doomed requests. */
+export function markFreebuffSessionCountryBlocked(countryCode: string): void {
+ if (!IS_FREEBUFF) return
+ controller?.abort()
+ controller?.apply({ status: 'country_blocked', countryCode })
+ // Best-effort DELETE so we don't hold a waiting-room seat on a session the
+ // server is already refusing to serve at chat time.
+ releaseFreebuffSlot().catch(() => {})
+}
+
/** Flip into the local `ended` state without an instanceId (server has lost
* our row). The chat surface stays mounted with the rejoin banner. */
export function markFreebuffSessionEnded(): void {
diff --git a/cli/src/hooks/use-gravity-ad.ts b/cli/src/hooks/use-gravity-ad.ts
index 5b48a97f2..e52b4bdd8 100644
--- a/cli/src/hooks/use-gravity-ad.ts
+++ b/cli/src/hooks/use-gravity-ad.ts
@@ -16,7 +16,7 @@ const MAX_ADS_AFTER_ACTIVITY = 3 // Show up to 3 ads after last activity, then p
const ACTIVITY_THRESHOLD_MS = 30_000 // 30 seconds idle threshold for fetching new ads
const MAX_AD_CACHE_SIZE = 50 // Maximum number of ads to keep in cache
-// Ad response type (matches Gravity API response, credits added after impression)
+// Ad response type (normalized shape across providers; credits added after impression)
export type AdResponse = {
adText: string
title: string
@@ -30,6 +30,12 @@ export type AdResponse = {
export type AdVariant = 'banner' | 'choice'
+/**
+ * Which upstream ad network to query. The server maps each provider onto the
+ * same normalized response shape, so the rest of the hook is provider-agnostic.
+ */
+export type AdProvider = 'gravity' | 'carbon'
+
export type AdData =
| { variant: 'banner'; ad: AdResponse }
| { variant: 'choice'; ads: AdResponse[] }
@@ -102,9 +108,12 @@ export const useGravityAd = (options?: {
/** Skip the "wait for first user message" gate. Used by the freebuff
* waiting room, which has no conversation but still needs ads. */
forceStart?: boolean
+ /** Which ad network to query. Defaults to Gravity. */
+ provider?: AdProvider
}): GravityAdState => {
const enabled = options?.enabled ?? true
const forceStart = options?.forceStart ?? false
+ const provider: AdProvider = options?.provider ?? 'gravity'
const [ad, setAd] = useState(null)
const [adData, setAdData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
@@ -159,7 +168,7 @@ export const useGravityAd = (options?: {
const authToken = getAuthToken()
if (!authToken) {
- logger.warn('[gravity] No auth token, skipping impression recording')
+ logger.warn('[ads] No auth token, skipping impression recording')
return
}
@@ -179,7 +188,7 @@ export const useGravityAd = (options?: {
if (data.creditsGranted > 0) {
logger.info(
{ creditsGranted: data.creditsGranted },
- '[gravity] Ad impression credits granted',
+ '[ads] Ad impression credits granted',
)
setAd((cur) =>
cur?.impUrl === impUrl
@@ -205,7 +214,7 @@ export const useGravityAd = (options?: {
}
})
.catch((err) => {
- logger.debug({ err }, '[gravity] Failed to record ad impression')
+ logger.debug({ err }, '[ads] Failed to record ad impression')
})
}
@@ -235,7 +244,7 @@ export const useGravityAd = (options?: {
const authToken = getAuthToken()
if (!authToken) {
- logger.warn('[gravity] No auth token available')
+ logger.warn('[ads] No auth token available')
return null
}
@@ -277,16 +286,21 @@ export const useGravityAd = (options?: {
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({
+ provider,
messages: adMessages,
sessionId: useChatStore.getState().chatSessionId,
device: getDeviceInfo(),
+ // Carbon requires a real browser-ish useragent for targeting/fraud
+ // detection. Gravity ignores it. We source one centrally so every
+ // provider that needs it sees the same value.
+ userAgent: getAdUserAgent(),
}),
})
if (!response.ok) {
logger.warn(
- { status: response.status, response: await response.json() },
- '[gravity] Web API returned error',
+ { provider, status: response.status, response: await response.json() },
+ '[ads] Web API returned error',
)
return null
}
@@ -304,7 +318,7 @@ export const useGravityAd = (options?: {
return null
} catch (err) {
- logger.error({ err }, '[gravity] Failed to fetch ad')
+ logger.error({ err }, '[ads] Failed to fetch ad')
return null
}
}
@@ -465,3 +479,22 @@ function getDeviceInfo(): DeviceInfo {
return { os, timezone, locale }
}
+
+/**
+ * Useragent string passed to ad providers. Carbon (BuySellAds) requires a
+ * plausible browser useragent for targeting and fraud screening. We send a
+ * stable desktop Chrome-on-{os} UA per platform so targeting is consistent
+ * across users on the same platform without sharing anything identifying.
+ *
+ * Chrome version needs bumping periodically — stale UAs look bot-ish to ad
+ * networks. Last bumped: 2026-04-21. Revisit roughly every 6 months.
+ */
+const AD_CHROME_VERSION = '124.0.0.0'
+function getAdUserAgent(): string {
+ const osUA: Record = {
+ darwin: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${AD_CHROME_VERSION} Safari/537.36`,
+ win32: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${AD_CHROME_VERSION} Safari/537.36`,
+ linux: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${AD_CHROME_VERSION} Safari/537.36`,
+ }
+ return osUA[process.platform] ?? osUA.linux
+}
diff --git a/cli/src/utils/error-handling.ts b/cli/src/utils/error-handling.ts
index 0ff889482..5bedce5d4 100644
--- a/cli/src/utils/error-handling.ts
+++ b/cli/src/utils/error-handling.ts
@@ -57,6 +57,22 @@ export const isFreeModeUnavailableError = (error: unknown): boolean => {
return false
}
+/**
+ * Extract the detected countryCode off a free_mode_unavailable error, if the
+ * server included one. Used to populate the country_blocked screen after the
+ * chat-completions gate rejects a user whose session-level country check had
+ * previously failed open (null country detection → admitted → now blocked).
+ */
+export const getCountryCodeFromFreeModeError = (
+ error: unknown,
+): string | null => {
+ if (!isFreeModeUnavailableError(error)) return null
+ const candidate = (error as { countryCode?: unknown }).countryCode
+ return typeof candidate === 'string' && candidate.length > 0
+ ? candidate
+ : null
+}
+
/**
* Freebuff waiting-room gate errors returned by /api/v1/chat/completions.
*
diff --git a/packages/agent-runtime/src/__tests__/tool-validation-error.test.ts b/packages/agent-runtime/src/__tests__/tool-validation-error.test.ts
index d3d1d65bd..eb982d368 100644
--- a/packages/agent-runtime/src/__tests__/tool-validation-error.test.ts
+++ b/packages/agent-runtime/src/__tests__/tool-validation-error.test.ts
@@ -233,6 +233,152 @@ describe('tool validation error handling', () => {
expect(errorEvents.length).toBe(0)
})
+ it('should parse input JSON string from AI SDK before validation', async () => {
+ // The AI SDK can emit tool-call chunks with `input` as a raw JSON string
+ // when upstream schema validation fails and the repair function returns
+ // the original tool call unchanged. The stream parser should parse the
+ // string into an object before handing it to the tool executor.
+ const agentWithReadFiles: AgentTemplate = {
+ ...testAgentTemplate,
+ toolNames: ['read_files', 'end_turn'],
+ }
+
+ const stringInputToolCallChunk = {
+ type: 'tool-call' as const,
+ toolName: 'read_files',
+ toolCallId: 'string-input-tool-call-id',
+ input: JSON.stringify({ paths: ['test.ts'] }) as any,
+ }
+
+ async function* mockStream() {
+ yield stringInputToolCallChunk
+ return promptSuccess('mock-message-id')
+ }
+
+ const sessionState = getInitialSessionState(mockFileContext)
+ const agentState = sessionState.mainAgentState
+
+ agentRuntimeImpl.requestFiles = async () => ({
+ 'test.ts': 'console.log("test")',
+ })
+
+ const responseChunks: (string | PrintModeEvent)[] = []
+
+ await processStream({
+ ...agentRuntimeImpl,
+ agentContext: {},
+ agentState,
+ agentStepId: 'test-step-id',
+ agentTemplate: agentWithReadFiles,
+ ancestorRunIds: [],
+ clientSessionId: 'test-session',
+ fileContext: mockFileContext,
+ fingerprintId: 'test-fingerprint',
+ fullResponse: '',
+ localAgentTemplates: { 'test-agent': agentWithReadFiles },
+ messages: [],
+ prompt: 'test prompt',
+ repoId: undefined,
+ repoUrl: undefined,
+ runId: 'test-run-id',
+ signal: new AbortController().signal,
+ stream: mockStream(),
+ system: 'test system',
+ tools: {},
+ userId: 'test-user',
+ userInputId: 'test-input-id',
+ onCostCalculated: async () => {},
+ onResponseChunk: (chunk) => {
+ responseChunks.push(chunk)
+ },
+ })
+
+ const toolCallEvents = responseChunks.filter(
+ (chunk): chunk is Extract =>
+ typeof chunk !== 'string' && chunk.type === 'tool_call',
+ )
+ expect(toolCallEvents.length).toBe(1)
+ expect(toolCallEvents[0].toolName).toBe('read_files')
+ expect(toolCallEvents[0].input).toEqual({ paths: ['test.ts'] })
+
+ const errorEvents = responseChunks.filter(
+ (chunk): chunk is Extract =>
+ typeof chunk !== 'string' && chunk.type === 'error',
+ )
+ expect(errorEvents.length).toBe(0)
+ })
+
+ it('should emit a clear error when tool input is an unparseable string', async () => {
+ const agentWithReadFiles: AgentTemplate = {
+ ...testAgentTemplate,
+ toolNames: ['read_files', 'end_turn'],
+ }
+
+ const invalidStringToolCallChunk = {
+ type: 'tool-call' as const,
+ toolName: 'read_files',
+ toolCallId: 'invalid-string-tool-call-id',
+ input: '{"paths": ["test.ts"' as any, // truncated/malformed JSON
+ }
+
+ async function* mockStream() {
+ yield invalidStringToolCallChunk
+ return promptSuccess('mock-message-id')
+ }
+
+ const sessionState = getInitialSessionState(mockFileContext)
+ const agentState = sessionState.mainAgentState
+
+ const responseChunks: (string | PrintModeEvent)[] = []
+
+ const result = await processStream({
+ ...agentRuntimeImpl,
+ agentContext: {},
+ agentState,
+ agentStepId: 'test-step-id',
+ agentTemplate: agentWithReadFiles,
+ ancestorRunIds: [],
+ clientSessionId: 'test-session',
+ fileContext: mockFileContext,
+ fingerprintId: 'test-fingerprint',
+ fullResponse: '',
+ localAgentTemplates: { 'test-agent': agentWithReadFiles },
+ messages: [],
+ prompt: 'test prompt',
+ repoId: undefined,
+ repoUrl: undefined,
+ runId: 'test-run-id',
+ signal: new AbortController().signal,
+ stream: mockStream(),
+ system: 'test system',
+ tools: {},
+ userId: 'test-user',
+ userInputId: 'test-input-id',
+ onCostCalculated: async () => {},
+ onResponseChunk: (chunk) => {
+ responseChunks.push(chunk)
+ },
+ })
+
+ const errorEvents = responseChunks.filter(
+ (chunk): chunk is Extract =>
+ typeof chunk !== 'string' && chunk.type === 'error',
+ )
+ expect(errorEvents.length).toBe(1)
+ expect(errorEvents[0].message).toContain(
+ 'tool arguments were a string, not a JSON object',
+ )
+ expect(errorEvents[0].message).toContain('Original tool call input:')
+
+ expect(result.hadToolCallError).toBe(true)
+
+ const toolCallEvents = responseChunks.filter(
+ (chunk): chunk is Extract =>
+ typeof chunk !== 'string' && chunk.type === 'tool_call',
+ )
+ expect(toolCallEvents.length).toBe(0)
+ })
+
it('should preserve tool_call/tool_result ordering when custom tool setup is async', async () => {
const toolName = 'delayed_custom_tool'
const agentWithCustomTool: AgentTemplate = {
diff --git a/packages/agent-runtime/src/tool-stream-parser.ts b/packages/agent-runtime/src/tool-stream-parser.ts
index 82a37111b..cd4ca58df 100644
--- a/packages/agent-runtime/src/tool-stream-parser.ts
+++ b/packages/agent-runtime/src/tool-stream-parser.ts
@@ -77,7 +77,17 @@ export async function* processStreamWithTools(params: {
input: any
contents?: string
}): Promise {
- const { toolName, input, contents } = params
+ const { toolName, contents } = params
+ let { input } = params
+
+ // AI SDK sometimes emits tool-call chunks with a raw JSON string as `input`
+ // when its repair pass can't produce a parsed object. Try to parse; if it
+ // fails, leave as string — the executor surfaces a clear error.
+ if (typeof input === 'string') {
+ try {
+ input = JSON.parse(input)
+ } catch {}
+ }
const processor = processors[toolName] ?? defaultProcessor(toolName)
diff --git a/packages/agent-runtime/src/tools/tool-executor.ts b/packages/agent-runtime/src/tools/tool-executor.ts
index da0cfbd3b..78906f4ab 100644
--- a/packages/agent-runtime/src/tools/tool-executor.ts
+++ b/packages/agent-runtime/src/tools/tool-executor.ts
@@ -51,6 +51,18 @@ export type ToolCallError = {
error: string
} & Pick
+function stringInputError(
+ toolName: string,
+ toolCallId: string,
+): ToolCallError {
+ return {
+ toolName,
+ toolCallId,
+ input: {},
+ error: `Invalid parameters for ${toolName}: tool arguments were a string, not a JSON object. This usually means the model emitted malformed JSON (e.g. unescaped newlines or quotes inside a string value). Re-issue the tool call with properly escaped JSON.`,
+ }
+}
+
export function parseRawToolCall(params: {
rawToolCall: {
toolName: T
@@ -64,6 +76,10 @@ export function parseRawToolCall(params: {
const processedParameters = rawToolCall.input
const paramsSchema = toolParams[toolName].inputSchema
+ if (typeof processedParameters === 'string') {
+ return stringInputError(toolName, rawToolCall.toolCallId)
+ }
+
const result = paramsSchema.safeParse(processedParameters)
if (!result.success) {
@@ -388,6 +404,10 @@ export function parseRawCustomToolCall(params: {
}
}
+ if (typeof rawToolCall.input === 'string') {
+ return stringInputError(toolName, rawToolCall.toolCallId)
+ }
+
const processedParameters: Record = {}
for (const [param, val] of Object.entries(rawToolCall.input ?? {})) {
processedParameters[param] = val
diff --git a/packages/internal/src/db/migrations/0045_mean_sleeper.sql b/packages/internal/src/db/migrations/0045_mean_sleeper.sql
new file mode 100644
index 000000000..0f0f9c4d7
--- /dev/null
+++ b/packages/internal/src/db/migrations/0045_mean_sleeper.sql
@@ -0,0 +1,3 @@
+ALTER TABLE "ad_impression" ALTER COLUMN "payout" DROP NOT NULL;--> statement-breakpoint
+ALTER TABLE "ad_impression" ADD COLUMN "provider" text DEFAULT 'gravity' NOT NULL;--> statement-breakpoint
+ALTER TABLE "ad_impression" ADD COLUMN "extra_pixels" text[];
\ No newline at end of file
diff --git a/packages/internal/src/db/migrations/meta/0045_snapshot.json b/packages/internal/src/db/migrations/meta/0045_snapshot.json
new file mode 100644
index 000000000..a421bd575
--- /dev/null
+++ b/packages/internal/src/db/migrations/meta/0045_snapshot.json
@@ -0,0 +1,3227 @@
+{
+ "id": "76196ef1-2384-4edd-b832-c9ff8085d809",
+ "prevId": "108f2bd2-7ddc-4c15-b351-28f2b55d5348",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "providerAccountId": {
+ "name": "providerAccountId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_userId_user_id_fk": {
+ "name": "account_userId_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "account_provider_providerAccountId_pk": {
+ "name": "account_provider_providerAccountId_pk",
+ "columns": [
+ "provider",
+ "providerAccountId"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ad_impression": {
+ "name": "ad_impression",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'gravity'"
+ },
+ "ad_text": {
+ "name": "ad_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cta": {
+ "name": "cta",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "favicon": {
+ "name": "favicon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "click_url": {
+ "name": "click_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "imp_url": {
+ "name": "imp_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "extra_pixels": {
+ "name": "extra_pixels",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payout": {
+ "name": "payout",
+ "type": "numeric(10, 6)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credits_granted": {
+ "name": "credits_granted",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "grant_operation_id": {
+ "name": "grant_operation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "served_at": {
+ "name": "served_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "impression_fired_at": {
+ "name": "impression_fired_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "clicked_at": {
+ "name": "clicked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "idx_ad_impression_user": {
+ "name": "idx_ad_impression_user",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "served_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_ad_impression_imp_url": {
+ "name": "idx_ad_impression_imp_url",
+ "columns": [
+ {
+ "expression": "imp_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ad_impression_user_id_user_id_fk": {
+ "name": "ad_impression_user_id_user_id_fk",
+ "tableFrom": "ad_impression",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "ad_impression_imp_url_unique": {
+ "name": "ad_impression_imp_url_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "imp_url"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.agent_config": {
+ "name": "agent_config",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "publisher_id": {
+ "name": "publisher_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "major": {
+ "name": "major",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 1) AS INTEGER)",
+ "type": "stored"
+ }
+ },
+ "minor": {
+ "name": "minor",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 2) AS INTEGER)",
+ "type": "stored"
+ }
+ },
+ "patch": {
+ "name": "patch",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 3) AS INTEGER)",
+ "type": "stored"
+ }
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_agent_config_publisher": {
+ "name": "idx_agent_config_publisher",
+ "columns": [
+ {
+ "expression": "publisher_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "agent_config_publisher_id_publisher_id_fk": {
+ "name": "agent_config_publisher_id_publisher_id_fk",
+ "tableFrom": "agent_config",
+ "tableTo": "publisher",
+ "columnsFrom": [
+ "publisher_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "agent_config_publisher_id_id_version_pk": {
+ "name": "agent_config_publisher_id_id_version_pk",
+ "columns": [
+ "publisher_id",
+ "id",
+ "version"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.agent_run": {
+ "name": "agent_run",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "publisher_id": {
+ "name": "publisher_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '/', 1)\n ELSE NULL\n END",
+ "type": "stored"
+ }
+ },
+ "agent_name": {
+ "name": "agent_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(split_part(agent_id, '/', 2), '@', 1)\n ELSE agent_id\n END",
+ "type": "stored"
+ }
+ },
+ "agent_version": {
+ "name": "agent_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '@', 2)\n ELSE NULL\n END",
+ "type": "stored"
+ }
+ },
+ "ancestor_run_ids": {
+ "name": "ancestor_run_ids",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "root_run_id": {
+ "name": "root_run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[1] ELSE id END",
+ "type": "stored"
+ }
+ },
+ "parent_run_id": {
+ "name": "parent_run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[array_length(ancestor_run_ids, 1)] ELSE NULL END",
+ "type": "stored"
+ }
+ },
+ "depth": {
+ "name": "depth",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "COALESCE(array_length(ancestor_run_ids, 1), 1)",
+ "type": "stored"
+ }
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer",
+ "type": "stored"
+ }
+ },
+ "total_steps": {
+ "name": "total_steps",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "direct_credits": {
+ "name": "direct_credits",
+ "type": "numeric(10, 6)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "total_credits": {
+ "name": "total_credits",
+ "type": "numeric(10, 6)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "status": {
+ "name": "status",
+ "type": "agent_run_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "idx_agent_run_user_id": {
+ "name": "idx_agent_run_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_agent_run_parent": {
+ "name": "idx_agent_run_parent",
+ "columns": [
+ {
+ "expression": "parent_run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_agent_run_root": {
+ "name": "idx_agent_run_root",
+ "columns": [
+ {
+ "expression": "root_run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_agent_run_agent_id": {
+ "name": "idx_agent_run_agent_id",
+ "columns": [
+ {
+ "expression": "agent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_agent_run_publisher": {
+ "name": "idx_agent_run_publisher",
+ "columns": [
+ {
+ "expression": "publisher_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_agent_run_status": {
+ "name": "idx_agent_run_status",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"agent_run\".\"status\" = 'running'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_agent_run_ancestors_gin": {
+ "name": "idx_agent_run_ancestors_gin",
+ "columns": [
+ {
+ "expression": "ancestor_run_ids",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "idx_agent_run_completed_publisher_agent": {
+ "name": "idx_agent_run_completed_publisher_agent",
+ "columns": [
+ {
+ "expression": "publisher_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "agent_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"agent_run\".\"status\" = 'completed'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_agent_run_completed_recent": {
+ "name": "idx_agent_run_completed_recent",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "publisher_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "agent_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"agent_run\".\"status\" = 'completed'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_agent_run_completed_version": {
+ "name": "idx_agent_run_completed_version",
+ "columns": [
+ {
+ "expression": "publisher_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "agent_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "agent_version",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"agent_run\".\"status\" = 'completed'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_agent_run_completed_user": {
+ "name": "idx_agent_run_completed_user",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"agent_run\".\"status\" = 'completed'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "agent_run_user_id_user_id_fk": {
+ "name": "agent_run_user_id_user_id_fk",
+ "tableFrom": "agent_run",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.agent_step": {
+ "name": "agent_step",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "agent_run_id": {
+ "name": "agent_run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "step_number": {
+ "name": "step_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer",
+ "type": "stored"
+ }
+ },
+ "credits": {
+ "name": "credits",
+ "type": "numeric(10, 6)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "child_run_ids": {
+ "name": "child_run_ids",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "spawned_count": {
+ "name": "spawned_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "array_length(child_run_ids, 1)",
+ "type": "stored"
+ }
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "agent_step_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'completed'"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "unique_step_number_per_run": {
+ "name": "unique_step_number_per_run",
+ "columns": [
+ {
+ "expression": "agent_run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "step_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_agent_step_run_id": {
+ "name": "idx_agent_step_run_id",
+ "columns": [
+ {
+ "expression": "agent_run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_agent_step_children_gin": {
+ "name": "idx_agent_step_children_gin",
+ "columns": [
+ {
+ "expression": "child_run_ids",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "agent_step_agent_run_id_agent_run_id_fk": {
+ "name": "agent_step_agent_run_id_agent_run_id_fk",
+ "tableFrom": "agent_step",
+ "tableTo": "agent_run",
+ "columnsFrom": [
+ "agent_run_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.credit_ledger": {
+ "name": "credit_ledger",
+ "schema": "",
+ "columns": {
+ "operation_id": {
+ "name": "operation_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "principal": {
+ "name": "principal",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "balance": {
+ "name": "balance",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "grant_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "org_id": {
+ "name": "org_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_subscription_id": {
+ "name": "stripe_subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "idx_credit_ledger_active_balance": {
+ "name": "idx_credit_ledger_active_balance",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "balance",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "priority",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"credit_ledger\".\"balance\" != 0 AND \"credit_ledger\".\"expires_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_credit_ledger_org": {
+ "name": "idx_credit_ledger_org",
+ "columns": [
+ {
+ "expression": "org_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_credit_ledger_subscription": {
+ "name": "idx_credit_ledger_subscription",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "credit_ledger_user_id_user_id_fk": {
+ "name": "credit_ledger_user_id_user_id_fk",
+ "tableFrom": "credit_ledger",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credit_ledger_org_id_org_id_fk": {
+ "name": "credit_ledger_org_id_org_id_fk",
+ "tableFrom": "credit_ledger",
+ "tableTo": "org",
+ "columnsFrom": [
+ "org_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.encrypted_api_keys": {
+ "name": "encrypted_api_keys",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "api_key_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "api_key": {
+ "name": "api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "encrypted_api_keys_user_id_user_id_fk": {
+ "name": "encrypted_api_keys_user_id_user_id_fk",
+ "tableFrom": "encrypted_api_keys",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "encrypted_api_keys_user_id_type_pk": {
+ "name": "encrypted_api_keys_user_id_type_pk",
+ "columns": [
+ "user_id",
+ "type"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.fingerprint": {
+ "name": "fingerprint",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "sig_hash": {
+ "name": "sig_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.free_session": {
+ "name": "free_session",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "free_session_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active_instance_id": {
+ "name": "active_instance_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "queued_at": {
+ "name": "queued_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "admitted_at": {
+ "name": "admitted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_free_session_queue": {
+ "name": "idx_free_session_queue",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "model",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "queued_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_free_session_expiry": {
+ "name": "idx_free_session_expiry",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "free_session_user_id_user_id_fk": {
+ "name": "free_session_user_id_user_id_fk",
+ "tableFrom": "free_session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.git_eval_results": {
+ "name": "git_eval_results",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "cost_mode": {
+ "name": "cost_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reasoner_model": {
+ "name": "reasoner_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent_model": {
+ "name": "agent_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cost": {
+ "name": "cost",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.limit_override": {
+ "name": "limit_override",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "credits_per_block": {
+ "name": "credits_per_block",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "block_duration_hours": {
+ "name": "block_duration_hours",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "weekly_credit_limit": {
+ "name": "weekly_credit_limit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "limit_override_user_id_user_id_fk": {
+ "name": "limit_override_user_id_user_id_fk",
+ "tableFrom": "limit_override",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.message": {
+ "name": "message",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_request_id": {
+ "name": "client_request_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "request": {
+ "name": "request",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_message": {
+ "name": "last_message",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "\"message\".\"request\" -> -1",
+ "type": "stored"
+ }
+ },
+ "reasoning_text": {
+ "name": "reasoning_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response": {
+ "name": "response",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "input_tokens": {
+ "name": "input_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "cache_creation_input_tokens": {
+ "name": "cache_creation_input_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cache_read_input_tokens": {
+ "name": "cache_read_input_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "reasoning_tokens": {
+ "name": "reasoning_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "output_tokens": {
+ "name": "output_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cost": {
+ "name": "cost",
+ "type": "numeric(100, 20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credits": {
+ "name": "credits",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "byok": {
+ "name": "byok",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "latency_ms": {
+ "name": "latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ttft_ms": {
+ "name": "ttft_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "org_id": {
+ "name": "org_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repo_url": {
+ "name": "repo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "message_user_id_idx": {
+ "name": "message_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "message_finished_at_user_id_idx": {
+ "name": "message_finished_at_user_id_idx",
+ "columns": [
+ {
+ "expression": "finished_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "message_org_id_idx": {
+ "name": "message_org_id_idx",
+ "columns": [
+ {
+ "expression": "org_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "message_org_id_finished_at_idx": {
+ "name": "message_org_id_finished_at_idx",
+ "columns": [
+ {
+ "expression": "org_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "finished_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "message_user_id_user_id_fk": {
+ "name": "message_user_id_user_id_fk",
+ "tableFrom": "message",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "message_org_id_org_id_fk": {
+ "name": "message_org_id_org_id_fk",
+ "tableFrom": "message",
+ "tableTo": "org",
+ "columnsFrom": [
+ "org_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.org": {
+ "name": "org",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_subscription_id": {
+ "name": "stripe_subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_period_start": {
+ "name": "current_period_start",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_period_end": {
+ "name": "current_period_end",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auto_topup_enabled": {
+ "name": "auto_topup_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "auto_topup_threshold": {
+ "name": "auto_topup_threshold",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "auto_topup_amount": {
+ "name": "auto_topup_amount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credit_limit": {
+ "name": "credit_limit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "billing_alerts": {
+ "name": "billing_alerts",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "usage_alerts": {
+ "name": "usage_alerts",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "weekly_reports": {
+ "name": "weekly_reports",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "org_owner_id_user_id_fk": {
+ "name": "org_owner_id_user_id_fk",
+ "tableFrom": "org",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "org_slug_unique": {
+ "name": "org_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ },
+ "org_stripe_customer_id_unique": {
+ "name": "org_stripe_customer_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "stripe_customer_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.org_feature": {
+ "name": "org_feature",
+ "schema": "",
+ "columns": {
+ "org_id": {
+ "name": "org_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "feature": {
+ "name": "feature",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_org_feature_active": {
+ "name": "idx_org_feature_active",
+ "columns": [
+ {
+ "expression": "org_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "org_feature_org_id_org_id_fk": {
+ "name": "org_feature_org_id_org_id_fk",
+ "tableFrom": "org_feature",
+ "tableTo": "org",
+ "columnsFrom": [
+ "org_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "org_feature_org_id_feature_pk": {
+ "name": "org_feature_org_id_feature_pk",
+ "columns": [
+ "org_id",
+ "feature"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.org_invite": {
+ "name": "org_invite",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "org_id": {
+ "name": "org_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "org_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "invited_by": {
+ "name": "invited_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "accepted_at": {
+ "name": "accepted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "accepted_by": {
+ "name": "accepted_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "idx_org_invite_token": {
+ "name": "idx_org_invite_token",
+ "columns": [
+ {
+ "expression": "token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_org_invite_email": {
+ "name": "idx_org_invite_email",
+ "columns": [
+ {
+ "expression": "org_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_org_invite_expires": {
+ "name": "idx_org_invite_expires",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "org_invite_org_id_org_id_fk": {
+ "name": "org_invite_org_id_org_id_fk",
+ "tableFrom": "org_invite",
+ "tableTo": "org",
+ "columnsFrom": [
+ "org_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "org_invite_invited_by_user_id_fk": {
+ "name": "org_invite_invited_by_user_id_fk",
+ "tableFrom": "org_invite",
+ "tableTo": "user",
+ "columnsFrom": [
+ "invited_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "org_invite_accepted_by_user_id_fk": {
+ "name": "org_invite_accepted_by_user_id_fk",
+ "tableFrom": "org_invite",
+ "tableTo": "user",
+ "columnsFrom": [
+ "accepted_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "org_invite_token_unique": {
+ "name": "org_invite_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.org_member": {
+ "name": "org_member",
+ "schema": "",
+ "columns": {
+ "org_id": {
+ "name": "org_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "org_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "joined_at": {
+ "name": "joined_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "org_member_org_id_org_id_fk": {
+ "name": "org_member_org_id_org_id_fk",
+ "tableFrom": "org_member",
+ "tableTo": "org",
+ "columnsFrom": [
+ "org_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "org_member_user_id_user_id_fk": {
+ "name": "org_member_user_id_user_id_fk",
+ "tableFrom": "org_member",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "org_member_org_id_user_id_pk": {
+ "name": "org_member_org_id_user_id_pk",
+ "columns": [
+ "org_id",
+ "user_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.org_repo": {
+ "name": "org_repo",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "org_id": {
+ "name": "org_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repo_url": {
+ "name": "repo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repo_name": {
+ "name": "repo_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repo_owner": {
+ "name": "repo_owner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "approved_by": {
+ "name": "approved_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "approved_at": {
+ "name": "approved_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ }
+ },
+ "indexes": {
+ "idx_org_repo_active": {
+ "name": "idx_org_repo_active",
+ "columns": [
+ {
+ "expression": "org_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_org_repo_unique": {
+ "name": "idx_org_repo_unique",
+ "columns": [
+ {
+ "expression": "org_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repo_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "org_repo_org_id_org_id_fk": {
+ "name": "org_repo_org_id_org_id_fk",
+ "tableFrom": "org_repo",
+ "tableTo": "org",
+ "columnsFrom": [
+ "org_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "org_repo_approved_by_user_id_fk": {
+ "name": "org_repo_approved_by_user_id_fk",
+ "tableFrom": "org_repo",
+ "tableTo": "user",
+ "columnsFrom": [
+ "approved_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.publisher": {
+ "name": "publisher",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "verified": {
+ "name": "verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "bio": {
+ "name": "bio",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar_url": {
+ "name": "avatar_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "org_id": {
+ "name": "org_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "publisher_user_id_user_id_fk": {
+ "name": "publisher_user_id_user_id_fk",
+ "tableFrom": "publisher",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "publisher_org_id_org_id_fk": {
+ "name": "publisher_org_id_org_id_fk",
+ "tableFrom": "publisher",
+ "tableTo": "org",
+ "columnsFrom": [
+ "org_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "publisher_created_by_user_id_fk": {
+ "name": "publisher_created_by_user_id_fk",
+ "tableFrom": "publisher",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "publisher_single_owner": {
+ "name": "publisher_single_owner",
+ "value": "(\"publisher\".\"user_id\" IS NOT NULL AND \"publisher\".\"org_id\" IS NULL) OR\n (\"publisher\".\"user_id\" IS NULL AND \"publisher\".\"org_id\" IS NOT NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.referral": {
+ "name": "referral",
+ "schema": "",
+ "columns": {
+ "referrer_id": {
+ "name": "referrer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "referred_id": {
+ "name": "referred_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "referral_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "credits": {
+ "name": "credits",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_legacy": {
+ "name": "is_legacy",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "referral_referrer_id_user_id_fk": {
+ "name": "referral_referrer_id_user_id_fk",
+ "tableFrom": "referral",
+ "tableTo": "user",
+ "columnsFrom": [
+ "referrer_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "referral_referred_id_user_id_fk": {
+ "name": "referral_referred_id_user_id_fk",
+ "tableFrom": "referral",
+ "tableTo": "user",
+ "columnsFrom": [
+ "referred_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "referral_referrer_id_referred_id_pk": {
+ "name": "referral_referrer_id_referred_id_pk",
+ "columns": [
+ "referrer_id",
+ "referred_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "sessionToken": {
+ "name": "sessionToken",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fingerprint_id": {
+ "name": "fingerprint_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "session_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'web'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_userId_user_id_fk": {
+ "name": "session_userId_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "session_fingerprint_id_fingerprint_id_fk": {
+ "name": "session_fingerprint_id_fingerprint_id_fk",
+ "tableFrom": "session",
+ "tableTo": "fingerprint",
+ "columnsFrom": [
+ "fingerprint_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.subscription": {
+ "name": "subscription",
+ "schema": "",
+ "columns": {
+ "stripe_subscription_id": {
+ "name": "stripe_subscription_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_price_id": {
+ "name": "stripe_price_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tier": {
+ "name": "tier",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scheduled_tier": {
+ "name": "scheduled_tier",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "subscription_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "billing_period_start": {
+ "name": "billing_period_start",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "billing_period_end": {
+ "name": "billing_period_end",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cancel_at_period_end": {
+ "name": "cancel_at_period_end",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canceled_at": {
+ "name": "canceled_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_subscription_customer": {
+ "name": "idx_subscription_customer",
+ "columns": [
+ {
+ "expression": "stripe_customer_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_subscription_user": {
+ "name": "idx_subscription_user",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_subscription_status": {
+ "name": "idx_subscription_status",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"subscription\".\"status\" = 'active'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "subscription_user_id_user_id_fk": {
+ "name": "subscription_user_id_user_id_fk",
+ "tableFrom": "subscription",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sync_failure": {
+ "name": "sync_failure",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_attempt_at": {
+ "name": "last_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "retry_count": {
+ "name": "retry_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_sync_failure_retry": {
+ "name": "idx_sync_failure_retry",
+ "columns": [
+ {
+ "expression": "retry_count",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"sync_failure\".\"retry_count\" < 5",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_quota_reset": {
+ "name": "next_quota_reset",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now() + INTERVAL '1 month'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "referral_code": {
+ "name": "referral_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'ref-' || gen_random_uuid()"
+ },
+ "referral_limit": {
+ "name": "referral_limit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 5
+ },
+ "discord_id": {
+ "name": "discord_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "handle": {
+ "name": "handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auto_topup_enabled": {
+ "name": "auto_topup_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "auto_topup_threshold": {
+ "name": "auto_topup_threshold",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auto_topup_amount": {
+ "name": "auto_topup_amount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "banned": {
+ "name": "banned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "fallback_to_a_la_carte": {
+ "name": "fallback_to_a_la_carte",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ },
+ "user_stripe_customer_id_unique": {
+ "name": "user_stripe_customer_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "stripe_customer_id"
+ ]
+ },
+ "user_referral_code_unique": {
+ "name": "user_referral_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "referral_code"
+ ]
+ },
+ "user_discord_id_unique": {
+ "name": "user_discord_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "discord_id"
+ ]
+ },
+ "user_handle_unique": {
+ "name": "user_handle_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "handle"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verificationToken": {
+ "name": "verificationToken",
+ "schema": "",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verificationToken_identifier_token_pk": {
+ "name": "verificationToken_identifier_token_pk",
+ "columns": [
+ "identifier",
+ "token"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.referral_status": {
+ "name": "referral_status",
+ "schema": "public",
+ "values": [
+ "pending",
+ "completed"
+ ]
+ },
+ "public.agent_run_status": {
+ "name": "agent_run_status",
+ "schema": "public",
+ "values": [
+ "running",
+ "completed",
+ "failed",
+ "cancelled"
+ ]
+ },
+ "public.agent_step_status": {
+ "name": "agent_step_status",
+ "schema": "public",
+ "values": [
+ "running",
+ "completed",
+ "skipped"
+ ]
+ },
+ "public.api_key_type": {
+ "name": "api_key_type",
+ "schema": "public",
+ "values": [
+ "anthropic",
+ "gemini",
+ "openai"
+ ]
+ },
+ "public.free_session_status": {
+ "name": "free_session_status",
+ "schema": "public",
+ "values": [
+ "queued",
+ "active"
+ ]
+ },
+ "public.grant_type": {
+ "name": "grant_type",
+ "schema": "public",
+ "values": [
+ "free",
+ "referral",
+ "referral_legacy",
+ "subscription",
+ "purchase",
+ "admin",
+ "organization",
+ "ad"
+ ]
+ },
+ "public.org_role": {
+ "name": "org_role",
+ "schema": "public",
+ "values": [
+ "owner",
+ "admin",
+ "member"
+ ]
+ },
+ "public.session_type": {
+ "name": "session_type",
+ "schema": "public",
+ "values": [
+ "web",
+ "pat",
+ "cli"
+ ]
+ },
+ "public.subscription_status": {
+ "name": "subscription_status",
+ "schema": "public",
+ "values": [
+ "incomplete",
+ "incomplete_expired",
+ "trialing",
+ "active",
+ "past_due",
+ "canceled",
+ "unpaid",
+ "paused"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/packages/internal/src/db/migrations/meta/_journal.json b/packages/internal/src/db/migrations/meta/_journal.json
index bba4ab5ed..f67ef37dc 100644
--- a/packages/internal/src/db/migrations/meta/_journal.json
+++ b/packages/internal/src/db/migrations/meta/_journal.json
@@ -316,6 +316,13 @@
"when": 1776719872222,
"tag": "0044_violet_stingray",
"breakpoints": true
+ },
+ {
+ "idx": 45,
+ "version": "7",
+ "when": 1776813242936,
+ "tag": "0045_mean_sleeper",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/internal/src/db/schema.ts b/packages/internal/src/db/schema.ts
index ba481c89a..b6f170d29 100644
--- a/packages/internal/src/db/schema.ts
+++ b/packages/internal/src/db/schema.ts
@@ -431,7 +431,10 @@ export const adImpression = pgTable(
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
- // Ad content from Gravity API
+ // Which upstream ad network served this ad ('gravity', 'carbon', 'zeroclick', ...)
+ provider: text('provider').notNull().default('gravity'),
+
+ // Ad content (normalized across providers)
ad_text: text('ad_text').notNull(),
title: text('title').notNull(),
cta: text('cta').notNull().default(''),
@@ -439,7 +442,13 @@ export const adImpression = pgTable(
favicon: text('favicon').notNull(),
click_url: text('click_url').notNull(),
imp_url: text('imp_url').notNull().unique(), // Unique to prevent duplicates
- payout: numeric('payout', { precision: 10, scale: 6 }).notNull(),
+ // Extra tracking pixel URLs (e.g. Carbon's `pixel` field, `||`-separated).
+ // Each string may contain `[timestamp]` which is substituted at fire time.
+ extra_pixels: text('extra_pixels').array(),
+ // Payout is Gravity-shaped; Carbon uses CPM and reports no per-impression
+ // payout, so this is nullable to avoid polluting revenue dashboards with
+ // fake numbers.
+ payout: numeric('payout', { precision: 10, scale: 6 }),
// Credit tracking
credits_granted: integer('credits_granted').notNull(),
diff --git a/packages/internal/src/env-schema.ts b/packages/internal/src/env-schema.ts
index 25ce2931d..98a874a7a 100644
--- a/packages/internal/src/env-schema.ts
+++ b/packages/internal/src/env-schema.ts
@@ -12,6 +12,11 @@ export const serverEnvSchema = clientEnvSchema.extend({
LINKUP_API_KEY: z.string().min(1),
CONTEXT7_API_KEY: z.string().optional(),
GRAVITY_API_KEY: z.string().min(1),
+ // BuySellAds (Carbon) zone key used for the Freebuff waiting-room ad.
+ // Optional: when unset the Carbon provider returns no ad and callers fall
+ // back to their cached ads / fallback content. `CVADC53U` is the public
+ // test key from BSA docs and is safe to use in dev.
+ CARBON_ZONE_KEY: z.string().min(1).optional(),
PORT: z.coerce.number().min(1000),
// Web/Database variables
@@ -82,6 +87,7 @@ export const serverProcessEnv: ServerInput = {
LINKUP_API_KEY: process.env.LINKUP_API_KEY,
CONTEXT7_API_KEY: process.env.CONTEXT7_API_KEY,
GRAVITY_API_KEY: process.env.GRAVITY_API_KEY,
+ CARBON_ZONE_KEY: process.env.CARBON_ZONE_KEY,
PORT: process.env.PORT,
// Web/Database variables
diff --git a/packages/internal/src/env.ts b/packages/internal/src/env.ts
index a0af1c971..b32f90564 100644
--- a/packages/internal/src/env.ts
+++ b/packages/internal/src/env.ts
@@ -35,6 +35,15 @@ if (isCI) {
// Only log environment in non-production
if (process.env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod') {
console.log('Using environment:', process.env.NEXT_PUBLIC_CB_ENVIRONMENT)
+
+ // `CVADC53U` is the public test zone documented by BuySellAds — safe to use
+ // in dev/CI so nobody has to configure anything to see Carbon ads render.
+ // Prod intentionally has no default: if CARBON_ZONE_KEY isn't set there,
+ // waiting-room requests return no ad rather than silently hitting test
+ // inventory.
+ if (!process.env.CARBON_ZONE_KEY) {
+ process.env.CARBON_ZONE_KEY = 'CVADC53U'
+ }
}
export const env = serverEnvSchema.parse(serverProcessEnv)
diff --git a/web/src/app/api/v1/ads/_post.ts b/web/src/app/api/v1/ads/_post.ts
index 39daa5d31..fc1fa07a5 100644
--- a/web/src/app/api/v1/ads/_post.ts
+++ b/web/src/app/api/v1/ads/_post.ts
@@ -1,7 +1,4 @@
-import { createHash } from 'crypto'
-
import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
-import { buildArray } from '@codebuff/common/util/array'
import { getErrorObject } from '@codebuff/common/util/error'
import db from '@codebuff/internal/db'
import * as schema from '@codebuff/internal/db/schema'
@@ -10,6 +7,14 @@ import { z } from 'zod'
import { requireUserFromApiKey } from '../_helpers'
+import { createCarbonProvider } from '@/lib/ad-providers/carbon'
+import { createGravityProvider } from '@/lib/ad-providers/gravity'
+
+import type {
+ AdProvider,
+ AdProviderId,
+ NormalizedAd,
+} from '@/lib/ad-providers/types'
import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics'
import type { GetUserInfoFromApiKeyFn } from '@codebuff/common/types/contracts/database'
import type {
@@ -18,28 +23,6 @@ import type {
} from '@codebuff/common/types/contracts/logger'
import type { NextRequest } from 'next/server'
-const DEFAULT_PAYOUT = 0.04
-
-// A/B test: 50% of users see the "choice" ad variant (4 ads as bullet points)
-type AdVariant = 'banner' | 'choice'
-
-const CHOICE_AD_PLACEMENT_IDS = [
- 'choice-ad-1',
- 'choice-ad-2',
- 'choice-ad-3',
- 'choice-ad-4',
-]
-
-/**
- * Deterministically assign a user to an ad variant based on their userId.
- * Uses a hash so the assignment is stable across requests.
- */
-function getAdVariant(userId: string): AdVariant {
- const hash = createHash('sha256').update(`ad-variant:${userId}`).digest()
- // Use first byte: even = banner, odd = choice (50/50 split)
- return hash[0] % 2 === 0 ? 'banner' : 'choice'
-}
-
const messageSchema = z.object({
role: z.string(),
content: z.string(),
@@ -51,14 +34,20 @@ const deviceSchema = z.object({
locale: z.string().optional(),
})
+const providerSchema = z.enum(['gravity', 'carbon']).default('gravity')
+
const bodySchema = z.object({
- messages: z.array(messageSchema),
+ provider: providerSchema.optional(),
+ messages: z.array(messageSchema).optional().default([]),
sessionId: z.string().optional(),
device: deviceSchema.optional(),
+ /** Browser/CLI useragent passed through to providers that require it. */
+ userAgent: z.string().optional(),
})
-export type GravityEnv = {
+export type AdsEnv = {
GRAVITY_API_KEY: string
+ CARBON_ZONE_KEY?: string
CB_ENVIRONMENT: string
}
@@ -69,7 +58,7 @@ export async function postAds(params: {
loggerWithContext: LoggerWithContextFn
trackEvent: TrackEventFn
fetch: typeof globalThis.fetch
- serverEnv: GravityEnv
+ serverEnv: AdsEnv
}) {
const {
req,
@@ -92,22 +81,14 @@ export async function postAds(params: {
const { userId, userInfo, logger } = authed.data
- // Check if Gravity API key is configured
- if (!serverEnv.GRAVITY_API_KEY) {
- logger.warn('[ads] GRAVITY_API_KEY not configured')
- return NextResponse.json({ ad: null }, { status: 200 })
- }
-
- // Extract client IP from request headers
+ // Client IP comes in via the load balancer's X-Forwarded-For header. Every
+ // provider that targets or bills by IP (Gravity, Carbon, ...) needs this.
const forwardedFor = req.headers.get('x-forwarded-for')
const clientIp = forwardedFor
? forwardedFor.split(',')[0].trim()
: (req.headers.get('x-real-ip') ?? undefined)
- // Parse and validate request body
- let messages: z.infer['messages']
- let sessionId: string | undefined
- let deviceInfo: z.infer | undefined
+ let parsedBody: z.infer
try {
const json = await req.json()
const parsed = bodySchema.safeParse(json)
@@ -118,243 +99,144 @@ export async function postAds(params: {
{ status: 400 },
)
}
-
- // Filter out messages with no content and extract user message content from tags
- messages = parsed.data.messages
- .filter((message) => message.content)
- .map((message) => {
- // For user messages, extract content from the last tag if present
- if (message.role === 'user') {
- return {
- ...message,
- content: extractLastUserMessageContent(message.content),
- }
- }
- return message
- })
- sessionId = parsed.data.sessionId
- deviceInfo = parsed.data.device
+ parsedBody = parsed.data
} catch {
- logger.error(
- { error: 'Invalid JSON in request body' },
- '[ads] Invalid request body',
- )
return NextResponse.json(
{ error: 'Invalid JSON in request body' },
{ status: 400 },
)
}
- // Keep just the last user message and the last assistant message before it
- const lastUserMessageIndex = messages.findLastIndex(
- (message) => message.role === 'user',
- )
- const lastUserMessage = messages[lastUserMessageIndex]
- const lastAssistantMessage = messages
- .slice(0, lastUserMessageIndex)
- .findLast((message) => message.role === 'assistant')
- const filteredMessages = buildArray(lastAssistantMessage, lastUserMessage)
-
- // Build device object for Gravity API
- const device = clientIp
- ? {
- ip: clientIp,
- ...(deviceInfo?.os ? { os: deviceInfo.os } : {}),
- ...(deviceInfo?.timezone ? { timezone: deviceInfo.timezone } : {}),
- ...(deviceInfo?.locale ? { locale: deviceInfo.locale } : {}),
+ const providerId: AdProviderId = parsedBody.provider ?? 'gravity'
+ const userAgent =
+ parsedBody.userAgent ?? req.headers.get('user-agent') ?? undefined
+
+ // Pick a provider. If the requested one isn't configured, return no ad
+ // rather than failing — the client falls back to its cache / fallback UI.
+ let provider: AdProvider | null = null
+ if (providerId === 'carbon') {
+ if (!serverEnv.CARBON_ZONE_KEY) {
+ logger.warn('[ads] CARBON_ZONE_KEY not configured')
+ return NextResponse.json({ ad: null, provider: providerId }, { status: 200 })
}
- : undefined
-
- // Determine A/B test variant for this user
- const variant = getAdVariant(userId)
-
- // Build placements based on variant
- const placements =
- variant === 'choice'
- ? CHOICE_AD_PLACEMENT_IDS.map((id) => ({
- placement: 'below_response',
- placement_id: id,
- }))
- : [{ placement: 'below_response', placement_id: 'code-assist-ad' }]
+ provider = createCarbonProvider({ zoneKey: serverEnv.CARBON_ZONE_KEY })
+ } else {
+ if (!serverEnv.GRAVITY_API_KEY) {
+ logger.warn('[ads] GRAVITY_API_KEY not configured')
+ return NextResponse.json({ ad: null, provider: providerId }, { status: 200 })
+ }
+ provider = createGravityProvider({ apiKey: serverEnv.GRAVITY_API_KEY })
+ }
try {
- const requestBody = {
- messages: filteredMessages,
- sessionId: sessionId ?? userId,
- placements,
- testAd: serverEnv.CB_ENVIRONMENT !== 'prod',
- relevancy: 0,
- ...(device ? { device } : {}),
- user: {
- id: userId,
- email: userInfo.email,
- },
- }
- // Call Gravity API
- const response = await fetch('https://server.trygravity.ai/api/v1/ad', {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${serverEnv.GRAVITY_API_KEY}`,
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(requestBody),
+ const result = await provider.fetchAd({
+ userId,
+ userEmail: userInfo.email ?? null,
+ sessionId: parsedBody.sessionId,
+ clientIp,
+ userAgent,
+ device: parsedBody.device,
+ messages: parsedBody.messages,
+ testMode: serverEnv.CB_ENVIRONMENT !== 'prod',
+ logger,
+ fetch,
})
- // Handle 204 No Content first (no body to parse)
- if (response.status === 204) {
- logger.debug(
- { request: requestBody, status: response.status },
- '[ads] No ad available from Gravity API',
- )
- return NextResponse.json({ ad: null, variant }, { status: 200 })
- }
-
- // Check response.ok BEFORE parsing JSON to handle HTML error pages gracefully
- if (!response.ok) {
- // Try to get response body for logging, but don't fail if it's not JSON
- let errorBody: unknown
- try {
- const contentType = response.headers.get('content-type') ?? ''
- if (contentType.includes('application/json')) {
- errorBody = await response.json()
- } else {
- // Likely an HTML error page from load balancer/CDN
- errorBody = await response.text()
- }
- } catch {
- errorBody = 'Unable to parse error response'
- }
- logger.error(
- { request: requestBody, response: errorBody, status: response.status },
- '[ads] Gravity API returned error',
+ if (!result) {
+ return NextResponse.json(
+ { ad: null, provider: provider.id },
+ { status: 200 },
)
- return NextResponse.json({ ad: null, variant }, { status: 200 })
}
- // Now safe to parse JSON body since response.ok is true
- const ads = await response.json()
+ const adsToPersist: NormalizedAd[] =
+ result.variant === 'choice' ? result.ads : [result.ad]
- if (!Array.isArray(ads) || ads.length === 0) {
- logger.debug(
- { request: requestBody, response: ads, status: response.status },
- '[ads] No ads returned from Gravity API',
- )
- return NextResponse.json({ ad: null, variant }, { status: 200 })
- }
-
- // Store all returned ads in the database (skip duplicates via imp_url unique constraint)
- // Wrapped in try/catch so DB failures don't prevent serving ads to the client
+ // Persist served ads so the impression endpoint can validate + fire the
+ // correct pixels. Any DB failure is logged but doesn't block serving.
try {
- for (const ad of ads) {
- const payout = ad.payout || DEFAULT_PAYOUT
- await db
- .insert(schema.adImpression)
- .values({
- user_id: userId,
- ad_text: ad.adText,
- title: ad.title,
- cta: ad.cta,
- url: ad.url,
- favicon: ad.favicon,
- click_url: ad.clickUrl,
- imp_url: ad.impUrl,
- payout: String(payout),
- credits_granted: 0,
- })
- .onConflictDoNothing()
- }
+ await Promise.all(
+ adsToPersist.map((ad) =>
+ db
+ .insert(schema.adImpression)
+ .values({
+ user_id: userId,
+ provider: provider.id,
+ ad_text: ad.adText,
+ title: ad.title,
+ cta: ad.cta,
+ url: ad.url,
+ favicon: ad.favicon,
+ click_url: ad.clickUrl,
+ imp_url: ad.impUrl,
+ extra_pixels: ad.extraPixels ?? null,
+ payout: ad.payout != null ? String(ad.payout) : null,
+ credits_granted: 0,
+ })
+ .onConflictDoNothing(),
+ ),
+ )
} catch (dbError) {
logger.warn(
{
userId,
- adCount: ads.length,
+ provider: provider.id,
+ adCount: adsToPersist.length,
error:
dbError instanceof Error
? { name: dbError.name, message: dbError.message }
: dbError,
},
- '[ads] Failed to persist ad_impression rows, serving ads anyway',
+ '[ads] Failed to persist ad_impression rows, serving anyway',
)
}
- // Strip payout from all ads before returning to client
- const sanitizeAd = (ad: Record) => {
- const { payout: _payout, ...rest } = ad
+ // Strip server-only fields before sending to the CLI.
+ const toClient = (ad: NormalizedAd) => {
+ const { payout: _p, extraPixels: _e, ...rest } = ad
return rest
}
- if (variant === 'choice') {
- // Return all ads for the choice variant (up to 4)
- const sanitizedAds = ads.map(sanitizeAd)
-
+ if (result.variant === 'choice') {
logger.info(
- {
- variant,
- adCount: sanitizedAds.length,
- request: requestBody,
- status: response.status,
- },
- '[ads] Fetched choice ads from Gravity API',
+ { provider: provider.id, variant: 'choice', adCount: result.ads.length },
+ '[ads] Fetched choice ads',
)
-
- return NextResponse.json({ ads: sanitizedAds, variant })
+ return NextResponse.json({
+ ads: result.ads.map(toClient),
+ variant: 'choice',
+ provider: provider.id,
+ })
}
- // Banner variant: return single ad (existing behavior)
- const ad = ads[0]
- const payout = ad.payout || DEFAULT_PAYOUT
-
logger.info(
- {
- ad,
- variant,
- request: requestBody,
- status: response.status,
- payout: {
- included: ad.payout && ad.payout > 0,
- recieved: ad.payout,
- default: DEFAULT_PAYOUT,
- final: payout,
- },
- },
- '[ads] Fetched ad from Gravity API',
+ { provider: provider.id, variant: 'banner' },
+ '[ads] Fetched banner ad',
)
-
- return NextResponse.json({ ad: sanitizeAd(ad), variant })
+ return NextResponse.json({
+ ad: toClient(result.ad),
+ variant: 'banner',
+ provider: provider.id,
+ })
} catch (error) {
logger.error(
{
userId,
- messages,
- status: 500,
+ provider: providerId,
error:
error instanceof Error
? { name: error.name, message: error.message }
: error,
},
- '[ads] Failed to fetch ad from Gravity API',
+ '[ads] Failed to fetch ad',
)
return NextResponse.json(
- { ad: null, variant, error: getErrorObject(error) },
+ {
+ ad: null,
+ provider: providerId,
+ error: getErrorObject(error),
+ },
{ status: 500 },
)
}
}
-
-/**
- * Extract the content from the last tag in a string.
- * If no tag is found, returns the original content.
- */
-function extractLastUserMessageContent(content: string): string {
- // Find all ... matches
- const regex = /([\s\S]*?)<\/user_message>/gi
- const matches = [...content.matchAll(regex)]
-
- if (matches.length > 0) {
- // Return the content from the last match
- const lastMatch = matches[matches.length - 1]
- return lastMatch[1].trim()
- }
-
- return content
-}
diff --git a/web/src/app/api/v1/ads/impression/_post.ts b/web/src/app/api/v1/ads/impression/_post.ts
index 51482b9f3..3d6e53aee 100644
--- a/web/src/app/api/v1/ads/impression/_post.ts
+++ b/web/src/app/api/v1/ads/impression/_post.ts
@@ -178,23 +178,37 @@ export async function postAdImpression(params: {
)
}
- // Fire the impression pixel to Gravity
- try {
- await fetch(impUrl)
- logger.info({ userId, impUrl }, '[ads] Fired impression pixel')
- } catch (error) {
- logger.warn(
- {
- impUrl,
- error:
- error instanceof Error
- ? { name: error.name, message: error.message }
- : error,
- },
- '[ads] Failed to fire impression pixel',
- )
- // Continue anyway - we still want to record the impression
- }
+ // Fire the primary impression pixel plus any provider-specific extra
+ // tracking pixels (Carbon returns these via the `pixel` field). Each extra
+ // pixel may contain `[timestamp]` which we substitute with unix seconds.
+ const now = Math.floor(Date.now() / 1000).toString()
+ const extraPixels = (adRecord.extra_pixels ?? []).map((p) =>
+ p.replaceAll('[timestamp]', now),
+ )
+ const pixelUrls = [impUrl, ...extraPixels]
+
+ await Promise.all(
+ pixelUrls.map(async (pixelUrl) => {
+ try {
+ await fetch(pixelUrl)
+ } catch (error) {
+ logger.warn(
+ {
+ pixelUrl,
+ error:
+ error instanceof Error
+ ? { name: error.name, message: error.message }
+ : error,
+ },
+ '[ads] Failed to fire impression pixel',
+ )
+ }
+ }),
+ )
+ logger.info(
+ { userId, provider: adRecord.provider, pixelCount: pixelUrls.length },
+ '[ads] Fired impression pixels',
+ )
// No credits granted for ad impressions
const creditsGranted = 0
diff --git a/web/src/app/api/v1/ads/route.ts b/web/src/app/api/v1/ads/route.ts
index 6023c1483..0b90fd1ee 100644
--- a/web/src/app/api/v1/ads/route.ts
+++ b/web/src/app/api/v1/ads/route.ts
@@ -18,6 +18,7 @@ export async function POST(req: NextRequest) {
fetch,
serverEnv: {
GRAVITY_API_KEY: env.GRAVITY_API_KEY,
+ CARBON_ZONE_KEY: env.CARBON_ZONE_KEY,
CB_ENVIRONMENT: env.NEXT_PUBLIC_CB_ENVIRONMENT,
},
})
diff --git a/web/src/app/api/v1/chat/completions/_post.ts b/web/src/app/api/v1/chat/completions/_post.ts
index 8809697f3..1f71b7792 100644
--- a/web/src/app/api/v1/chat/completions/_post.ts
+++ b/web/src/app/api/v1/chat/completions/_post.ts
@@ -286,6 +286,7 @@ export async function postChatCompletions(params: {
{
error: 'free_mode_unavailable',
message: 'Free mode is not available in your country.',
+ countryCode,
},
{ status: 403 },
)
diff --git a/web/src/lib/ad-providers/carbon.ts b/web/src/lib/ad-providers/carbon.ts
new file mode 100644
index 000000000..64a926436
--- /dev/null
+++ b/web/src/lib/ad-providers/carbon.ts
@@ -0,0 +1,170 @@
+import type {
+ AdProvider,
+ FetchAdInput,
+ FetchAdResult,
+ NormalizedAd,
+} from './types'
+
+/**
+ * BuySellAds (Carbon) Ad Serving API.
+ *
+ * Docs: https://docs.buysellads.com/ad-serving-api
+ *
+ * Key facts:
+ * - GET https://srv.buysellads.com/ads/{zonekey}.json
+ * - Required query params: `useragent` (URL-encoded) and `forwardedip` (IPv4)
+ * - The test zone key `CVADC53U` is public and safe to use while developing.
+ * - Response has an `ads` array. An ad is only considered filled if the first
+ * entry has a `statlink` (click URL). `statimp` is the primary impression
+ * pixel. An optional `pixel` field contains additional tracking pixels
+ * separated by `||`, each of which may contain `[timestamp]`.
+ * - A single zone request returns one ad. To populate the choice ad panel we
+ * issue multiple concurrent requests and dedupe by description — Carbon
+ * rotates through its fill pool per-request, so repeated calls usually yield
+ * different creatives.
+ */
+const CARBON_URL_BASE = 'https://srv.buysellads.com/ads'
+
+// How many concurrent zone fetches to issue when filling the choice panel.
+// Four matches the Gravity choice layout and gives enough headroom that
+// dedupe still leaves us multiple distinct ads on typical fill rates.
+const CARBON_CHOICE_FETCH_COUNT = 4
+
+type CarbonAd = {
+ statlink?: string
+ statimp?: string
+ statview?: string
+ description?: string
+ company?: string
+ callToAction?: string
+ image?: string
+ logo?: string
+ pixel?: string
+}
+
+type CarbonResponse = {
+ ads?: CarbonAd[]
+}
+
+/**
+ * Carbon returns `//srv.buysellads.com/...` for its pixel URLs. Normalize to
+ * https:// so we (and the CLI) can fetch them directly.
+ */
+function withScheme(url: string): string {
+ if (url.startsWith('//')) return `https:${url}`
+ return url
+}
+
+function splitPixels(pixel: string | undefined): string[] {
+ if (!pixel) return []
+ return pixel
+ .split('||')
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .map(withScheme)
+}
+
+function normalizeCarbonAd(raw: CarbonAd): NormalizedAd | null {
+ // Per Carbon docs: if `statlink` is missing the zone had no fill.
+ if (!raw.statlink || !raw.statimp) return null
+
+ const clickUrl = withScheme(raw.statlink)
+ const impUrl = withScheme(raw.statimp)
+
+ // `statview` is Carbon's IAB viewable-impression pixel (separate from the
+ // regular impression `statimp`). Our CLI ad is definitively viewable when
+ // rendered, so fire it alongside any advertiser pixels.
+ const extraPixels = [
+ ...(raw.statview ? [withScheme(raw.statview)] : []),
+ ...splitPixels(raw.pixel),
+ ]
+
+ return {
+ adText: raw.description ?? '',
+ title: raw.company ?? '',
+ cta: raw.callToAction ?? 'Learn more',
+ // Carbon doesn't expose a destination URL — `statlink` is a tracker
+ // that 302s to the advertiser. Leave `url` empty so the UI doesn't
+ // render "srv.buysellads.com" as the ad's domain. Clicks use
+ // `clickUrl` and get correctly routed through tracking.
+ url: '',
+ favicon: raw.image ?? raw.logo ?? '',
+ clickUrl,
+ impUrl,
+ extraPixels,
+ }
+}
+
+export function createCarbonProvider(config: {
+ zoneKey: string
+}): AdProvider {
+ return {
+ id: 'carbon',
+ fetchAd: async (input: FetchAdInput): Promise => {
+ const { clientIp, userAgent, testMode, logger, fetch } = input
+
+ if (!clientIp || !userAgent) {
+ logger.debug(
+ { hasIp: !!clientIp, hasUA: !!userAgent },
+ '[ads:carbon] Missing required clientIp or userAgent',
+ )
+ return null
+ }
+
+ const params = new URLSearchParams({
+ useragent: userAgent,
+ forwardedip: clientIp,
+ })
+ // Carbon's `ignore=yes` loads ads without counting impressions. Use it
+ // in non-prod so we never accidentally bill advertisers for dev traffic.
+ if (testMode) params.set('ignore', 'yes')
+
+ const url = `${CARBON_URL_BASE}/${config.zoneKey}.json?${params.toString()}`
+
+ const fetchOne = async (): Promise => {
+ const response = await fetch(url, { method: 'GET' })
+ if (!response.ok) {
+ let body: unknown
+ try {
+ body = await response.text()
+ } catch {
+ body = 'Unable to parse error response'
+ }
+ logger.error(
+ { url, status: response.status, body },
+ '[ads:carbon] API returned error',
+ )
+ return null
+ }
+ const data = (await response.json()) as CarbonResponse
+ const first = data.ads?.[0]
+ if (!first) return null
+ return normalizeCarbonAd(first)
+ }
+
+ const results = await Promise.all(
+ Array.from({ length: CARBON_CHOICE_FETCH_COUNT }, fetchOne),
+ )
+
+ // Dedupe by description — Carbon issues a fresh tracker URL per request
+ // even for the same creative, so clickUrl/impUrl can't serve as a
+ // stable identity key.
+ const seen = new Set()
+ const ads: NormalizedAd[] = []
+ for (const ad of results) {
+ if (!ad) continue
+ const key = ad.adText || ad.title
+ if (!key || seen.has(key)) continue
+ seen.add(key)
+ ads.push(ad)
+ }
+
+ if (ads.length === 0) {
+ logger.debug({ url }, '[ads:carbon] No ad fill')
+ return null
+ }
+
+ return { variant: 'choice', ads }
+ },
+ }
+}
diff --git a/web/src/lib/ad-providers/gravity.ts b/web/src/lib/ad-providers/gravity.ts
new file mode 100644
index 000000000..ed9209cb0
--- /dev/null
+++ b/web/src/lib/ad-providers/gravity.ts
@@ -0,0 +1,190 @@
+import { createHash } from 'crypto'
+
+import { buildArray } from '@codebuff/common/util/array'
+
+import type {
+ AdMessage,
+ AdProvider,
+ AdVariant,
+ FetchAdInput,
+ FetchAdResult,
+ NormalizedAd,
+} from './types'
+
+const GRAVITY_URL = 'https://server.trygravity.ai/api/v1/ad'
+const BANNER_PLACEMENT_ID = 'code-assist-ad'
+const CHOICE_PLACEMENT_IDS = [
+ 'choice-ad-1',
+ 'choice-ad-2',
+ 'choice-ad-3',
+ 'choice-ad-4',
+]
+
+type GravityRawAd = {
+ adText: string
+ title: string
+ cta: string
+ url: string
+ favicon: string
+ clickUrl: string
+ impUrl: string
+ payout?: number
+}
+
+function normalize(raw: GravityRawAd): NormalizedAd {
+ return {
+ adText: raw.adText,
+ title: raw.title,
+ cta: raw.cta,
+ url: raw.url,
+ favicon: raw.favicon,
+ clickUrl: raw.clickUrl,
+ impUrl: raw.impUrl,
+ payout: raw.payout,
+ }
+}
+
+/**
+ * A/B test: deterministically assign a user to the `banner` or `choice`
+ * variant based on their userId. Stable across requests.
+ */
+function getGravityVariant(userId: string): AdVariant {
+ const hash = createHash('sha256').update(`ad-variant:${userId}`).digest()
+ return hash[0] % 2 === 0 ? 'banner' : 'choice'
+}
+
+/**
+ * Extract the content from the last tag in a string.
+ * The CLI wraps raw user text in that tag; if no tag is found, returns the
+ * original content.
+ */
+function extractLastUserMessageContent(content: string): string {
+ const regex = /([\s\S]*?)<\/user_message>/gi
+ const matches = [...content.matchAll(regex)]
+ if (matches.length > 0) {
+ const lastMatch = matches[matches.length - 1]
+ return lastMatch[1].trim()
+ }
+ return content
+}
+
+/**
+ * Gravity only wants the last user turn plus the last preceding assistant
+ * turn for relevancy signals. We also strip empties and normalize user
+ * messages through the tag.
+ */
+function prepareGravityMessages(messages: AdMessage[]): AdMessage[] {
+ const cleaned = messages
+ .filter((m) => m.content)
+ .map((m) =>
+ m.role === 'user'
+ ? { ...m, content: extractLastUserMessageContent(m.content) }
+ : m,
+ )
+ const lastUserIndex = cleaned.findLastIndex((m) => m.role === 'user')
+ const lastUser = lastUserIndex >= 0 ? cleaned[lastUserIndex] : undefined
+ const lastAssistant = cleaned
+ .slice(0, lastUserIndex >= 0 ? lastUserIndex : cleaned.length)
+ .findLast((m) => m.role === 'assistant')
+ return buildArray(lastAssistant, lastUser)
+}
+
+export function createGravityProvider(config: { apiKey: string }): AdProvider {
+ return {
+ id: 'gravity',
+ fetchAd: async (input: FetchAdInput): Promise => {
+ const {
+ userId,
+ userEmail,
+ sessionId,
+ clientIp,
+ device,
+ messages = [],
+ testMode,
+ logger,
+ fetch,
+ } = input
+
+ const variant = getGravityVariant(userId)
+ const filteredMessages = prepareGravityMessages(messages)
+
+ const placements =
+ variant === 'choice'
+ ? CHOICE_PLACEMENT_IDS.map((id) => ({
+ placement: 'below_response',
+ placement_id: id,
+ }))
+ : [{ placement: 'below_response', placement_id: BANNER_PLACEMENT_ID }]
+
+ const deviceBody = clientIp
+ ? {
+ ip: clientIp,
+ ...(device?.os ? { os: device.os } : {}),
+ ...(device?.timezone ? { timezone: device.timezone } : {}),
+ ...(device?.locale ? { locale: device.locale } : {}),
+ }
+ : undefined
+
+ const requestBody = {
+ messages: filteredMessages,
+ sessionId: sessionId ?? userId,
+ placements,
+ testAd: testMode,
+ relevancy: 0,
+ ...(deviceBody ? { device: deviceBody } : {}),
+ user: {
+ id: userId,
+ email: userEmail ?? undefined,
+ },
+ }
+
+ const response = await fetch(GRAVITY_URL, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${config.apiKey}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(requestBody),
+ })
+
+ if (response.status === 204) {
+ logger.debug(
+ { request: requestBody, status: response.status },
+ '[ads:gravity] No ad available',
+ )
+ return null
+ }
+
+ if (!response.ok) {
+ let errorBody: unknown
+ try {
+ const contentType = response.headers.get('content-type') ?? ''
+ errorBody = contentType.includes('application/json')
+ ? await response.json()
+ : await response.text()
+ } catch {
+ errorBody = 'Unable to parse error response'
+ }
+ logger.error(
+ { request: requestBody, response: errorBody, status: response.status },
+ '[ads:gravity] API returned error',
+ )
+ return null
+ }
+
+ const ads = (await response.json()) as GravityRawAd[] | unknown
+ if (!Array.isArray(ads) || ads.length === 0) {
+ logger.debug(
+ { request: requestBody, status: response.status },
+ '[ads:gravity] No ads returned',
+ )
+ return null
+ }
+
+ if (variant === 'choice') {
+ return { variant: 'choice', ads: ads.map(normalize) }
+ }
+ return { variant: 'banner', ad: normalize(ads[0]) }
+ },
+ }
+}
diff --git a/web/src/lib/ad-providers/types.ts b/web/src/lib/ad-providers/types.ts
new file mode 100644
index 000000000..5b664332b
--- /dev/null
+++ b/web/src/lib/ad-providers/types.ts
@@ -0,0 +1,69 @@
+import type { Logger } from '@codebuff/common/types/contracts/logger'
+
+/**
+ * Identifies which upstream ad network served an ad. Stored on
+ * `ad_impression.provider` so we can slice analytics and know which request
+ * shape to expect when firing impressions. Add a new id here when wiring in
+ * another provider (e.g. 'zeroclick').
+ */
+export type AdProviderId = 'gravity' | 'carbon'
+
+export type AdVariant = 'banner' | 'choice'
+
+/**
+ * Normalized ad shape returned by every provider. The CLI renders against
+ * this shape; provider modules are responsible for mapping their upstream
+ * response into it.
+ */
+export type NormalizedAd = {
+ adText: string
+ title: string
+ cta: string
+ url: string
+ favicon: string
+ clickUrl: string
+ /** Primary impression pixel URL. Fired once when the ad becomes visible. */
+ impUrl: string
+ /**
+ * Additional impression pixels (e.g. Carbon's `pixel` field). Each string
+ * may contain `[timestamp]` which must be substituted at fire time.
+ */
+ extraPixels?: string[]
+ /** Server-only: stripped before the ad is sent to the client. */
+ payout?: number
+}
+
+export type AdMessage = { role: string; content: string }
+
+export type AdDeviceInfo = {
+ os?: 'macos' | 'windows' | 'linux'
+ timezone?: string
+ locale?: string
+}
+
+export type FetchAdInput = {
+ userId: string
+ userEmail: string | null
+ sessionId?: string
+ /** Client IP, parsed from X-Forwarded-For upstream. */
+ clientIp?: string
+ /** Browser/CLI useragent string, passed through to upstream. */
+ userAgent?: string
+ device?: AdDeviceInfo
+ /** Last user + last preceding assistant message, if any. Used by Gravity. */
+ messages?: AdMessage[]
+ /** Set in non-prod so providers can request test ads. */
+ testMode: boolean
+ logger: Logger
+ fetch: typeof globalThis.fetch
+}
+
+export type FetchAdResult =
+ | { variant: 'banner'; ad: NormalizedAd }
+ | { variant: 'choice'; ads: NormalizedAd[] }
+ | null
+
+export type AdProvider = {
+ id: AdProviderId
+ fetchAd: (input: FetchAdInput) => Promise
+}
diff --git a/web/src/server/free-session/abuse-detection.ts b/web/src/server/free-session/abuse-detection.ts
index c6675021e..b62a04835 100644
--- a/web/src/server/free-session/abuse-detection.ts
+++ b/web/src/server/free-session/abuse-detection.ts
@@ -141,28 +141,54 @@ export async function identifyBotSuspects(params: {
agentDiversity.map((a) => [a.user_id!, Number(a.distinctAgents24h)]),
)
- // Max inter-message quiet gap in the 24h window (in hours). A gap ≥ 4h is
- // a strong "user slept" counter-signal — bots don't take circadian breaks.
- // Uses LAG() so it needs a CTE; run as raw SQL.
+ // Largest gap of usage (in hours) within the observation window — where
+ // the window is bounded by GREATEST(user.created_at, now - 24h). For each
+ // user we consider three kinds of gap: window_start → first msg, gaps
+ // between consecutive msgs, and last msg → now. Max of those is the
+ // quiet gap.
+ //
+ // Clipping the window to signup matters: a 0.2d-old account can only
+ // plausibly have a gap up to its age. Without the clip, LAG() on an empty
+ // pre-window history would silently omit any leading-boundary gap, so a
+ // fresh bot with dense activity reads as "low quiet gap" correctly — but
+ // for heavy accounts that only started hitting us within the last few
+ // hours, we also want to count post-activity quiet time toward the gap.
+ const nowIso = now.toISOString()
const quietGaps = await db.execute(sql`
- WITH ordered AS (
- SELECT user_id, finished_at,
- LAG(finished_at) OVER (PARTITION BY user_id ORDER BY finished_at) AS prev
- FROM ${schema.message}
- WHERE user_id IN (${sql.join(
+ WITH bounds AS (
+ SELECT id AS user_id,
+ GREATEST(created_at, ${cutoffIso}::timestamptz) AS window_start
+ FROM ${schema.user}
+ WHERE id IN (${sql.join(
userIds.map((id) => sql`${id}`),
sql`, `,
)})
- AND agent_id IN (${sql.join(
+ ),
+ msgs AS (
+ SELECT m.user_id, m.finished_at, b.window_start
+ FROM ${schema.message} m
+ JOIN bounds b ON b.user_id = m.user_id
+ WHERE m.finished_at >= b.window_start
+ AND m.agent_id IN (${sql.join(
FREEBUFF_ROOT_AGENT_IDS.map((a) => sql`${a}`),
sql`, `,
)})
- AND finished_at >= ${cutoffIso}::timestamptz
+ ),
+ gaps AS (
+ SELECT user_id,
+ finished_at,
+ COALESCE(
+ LAG(finished_at) OVER (PARTITION BY user_id ORDER BY finished_at),
+ window_start
+ ) AS prev
+ FROM msgs
)
SELECT user_id,
- MAX(EXTRACT(EPOCH FROM (finished_at - prev))) / 3600.0 AS max_gap_hours
- FROM ordered
- WHERE prev IS NOT NULL
+ GREATEST(
+ MAX(EXTRACT(EPOCH FROM (finished_at - prev)) / 3600.0),
+ EXTRACT(EPOCH FROM (${nowIso}::timestamptz - MAX(finished_at))) / 3600.0
+ ) AS max_gap_hours
+ FROM gaps
GROUP BY user_id
`)
const quietGapByUser = new Map()
@@ -245,6 +271,38 @@ export async function identifyBotSuspects(params: {
score += 15
}
+ // --- Region signal (corroborating, scored only when stacked with usage) ---
+ // The free tier is intended for users in approved regions: English-speaking
+ // (US, UK, Canada, Australia, NZ, Ireland) and western-European markets.
+ // We have no IP data, so region is inferred from email provider and the
+ // unicode characters in the display name. CJK indicators (Chinese/Japanese/
+ // Korean Unicode in name, Chinese-provider emails, .edu.cn domains) are
+ // the only signal we can detect reliably, and empirically our abuse
+ // clusters are overwhelmingly from these provider pools. Diaspora users
+ // from approved regions may trip this flag, so it only contributes to the
+ // score when combined with heavy usage (the combination, not the region
+ // alone, is what justifies the score bump).
+ const hasCjkName =
+ !!s.name &&
+ /[一-鿿-ヿ가-]/.test(s.name)
+ const hasChineseDomain =
+ !!s.email &&
+ /@(qq|163|126|sina|sina\.cn|foxmail|aliyun|139|yeah|tom)\.(com|cn|net)$/i.test(
+ s.email,
+ )
+ const hasCnEduDomain = !!s.email && /\.edu\.cn$/i.test(s.email)
+ const nonApprovedRegion =
+ hasCjkName || hasChineseDomain || hasCnEduDomain
+ if (nonApprovedRegion) {
+ const reasons: string[] = []
+ if (hasCjkName) reasons.push('cjk-name')
+ if (hasChineseDomain) reasons.push('cn-provider')
+ if (hasCnEduDomain) reasons.push('cn-edu')
+ flags.push(`non-approved-region[${reasons.join(',')}]`)
+ if (msgs24h >= 500) score += 40
+ else if (msgs24h >= 300) score += 25
+ }
+
// --- Email/handle pattern flags (purely informational) ---
// These are too noisy in isolation (many real users have digits in their
// email, use plus-aliases for privacy, or sign up via duck.com). They're
diff --git a/web/src/server/free-session/abuse-review.ts b/web/src/server/free-session/abuse-review.ts
index bf079ea78..4c833805c 100644
--- a/web/src/server/free-session/abuse-review.ts
+++ b/web/src/server/free-session/abuse-review.ts
@@ -40,24 +40,30 @@ You will see:
- Creation clusters: sets of codebuff accounts created within 30 minutes of each other.
Counter-signals are mitigating evidence that should PULL DOWN your confidence:
-- \`quiet-gap:Xh\` — the user went X hours between messages in the last 24h. Bots don't sleep; a gap ≥ 4h is strong evidence of a human circadian pattern, ≥ 8h is nearly conclusive.
-- \`diverse-agents:N\` — the user invoked N distinct agents in 24h. Real developers pipeline through basher, file-picker, code-reviewer, thinker alongside the root agent. Bot farms stay narrow (typically 1–3 agents). N ≥ 6 is a meaningful counter-signal, N ≥ 10 is very strong.
+- \`quiet-gap:Xh\` — the user went X hours between messages in the last 24h. Bots don't sleep; a gap ≥ 3h is a real circadian signal, ≥ 5h is strong, ≥ 8h is nearly conclusive. A ≥5h gap by itself defeats any "round-the-clock" claim: the account is demonstrably NOT running 24/7, full stop.
+- \`diverse-agents:N\` — the user invoked N distinct agents in 24h. Real developers pipeline through basher, file-picker, code-reviewer, thinker alongside the root agent. Bot farms stay narrow (typically 1–3 agents). N ≥ 5 is a meaningful counter-signal, N ≥ 8 is very strong.
- \`gh-established:Xy\` — the linked GitHub account is X years old. Buying an old GitHub is rare at our scale.
-When an account has strong counter-signals alongside its red flags, tier it DOWN. A user with \`very-heavy:1000/24h\` AND \`quiet-gap:10h diverse-agents:12 gh-established:3y\` is almost certainly a legitimate power user, not a bot, no matter how high the raw message count is.
+When an account has strong counter-signals alongside its red flags, tier it DOWN. A user with \`very-heavy:1000/24h\` AND \`quiet-gap:6h diverse-agents:6 gh-established:1y\` is almost certainly a legitimate power user, not a bot, no matter how high the raw message count is.
-A very young GitHub account (gh_age < 7d, especially < 1d) combined with heavy usage is one of the strongest bot signals we have: real developers almost never create a GitHub account on the same day they start running an agent. Weigh this heavily in tiering.
+A very young GitHub account (gh_age < 7d, especially < 1d) combined with heavy usage is one of the strongest bot signals we have: real developers almost never create a GitHub account on the same day they start running an agent. Weigh this heavily — fresh GH + heavy usage is TIER 1 even with a moderate (3–6h) quiet gap, because the fresh-GH signal is difficult to fake at scale.
-Conversely, an established GitHub account (gh_age ≥ 1 year, especially ≥ 3 years) is a strong counter-signal. Account-age spoofing by buying old accounts is possible but uncommon at our abuse scale. An established GitHub + a natural agent mix (basher, code-reviewer, file-picker alongside the root agent) + some activity gaps during the day reads like an excited first-day power user, not a bot. Don't tier these as HIGH unless there are two independent per-account signals (e.g. true 24/7 distinct_hours AND suspicious email pattern).
+Conversely, a GitHub account older than ~30 days is meaningful counter-evidence. The "day-1 of coding = day-1 of GitHub" pattern that makes fresh-GH such a strong bot signal doesn't apply once the GH predates the codebuff account by a month or more. gh_age ≥ 30d + a moderate quiet gap (≥4h) + any agent diversity reads like an excited power user, not a bot. Don't tier these as HIGH unless there's a genuinely unambiguous per-account signal (true near-continuous activity, see below).
+
+The free tier is intended for users in approved regions: English-speaking (US, UK, Canada, Australia, NZ, Ireland) and western-European markets. We have no IP geolocation, so region is inferred heuristically — the \`non-approved-region[...]\` flag fires when the account has a CJK-character display name (\`cjk-name\`), a Chinese email provider (\`cn-provider\` — qq.com, 163.com, 126.com, sina.com, foxmail.com, aliyun.com, 139.com, yeah.net, tom.com), or a \`.edu.cn\` domain (\`cn-edu\`). Empirically our abuse clusters are overwhelmingly from these provider pools, and heavy free-tier usage from them strongly correlates with VPN-based farming. BUT real diaspora developers from approved regions exist and trip this flag too. So: region alone is NEVER grounds for a ban. Treat it as corroborating evidence that RAISES confidence when stacked with heavy usage (msgs_24h ≥ 300) or other bot signals — a \`non-approved-region\` user with \`very-heavy\` usage on a young account is TIER 1; the same user with established-GH + low usage + diverse-agents stays in TIER 2.
Creation-cluster membership is a WEAK signal on its own. The detector is purely temporal — accounts created within 30 minutes of each other. At normal signup volume, unrelated real users routinely land in the same window (product launches, HN/Reddit posts, timezone-aligned bursts). A cluster is only actionable when its members share a concrete cross-account pattern: matching email-local stems or digit siblings (\`v6apiworker\` / \`v8apiworker\`), a shared uncommon domain (\`@mail.hnust.edu.cn\`), sequential-number naming, or near-identical msgs_24h / distinct_hours footprints across multiple members. Absent such a shared pattern, treat a cluster list as background noise and tier members purely on their per-account signals. When you do use a cluster as evidence, name the shared pattern explicitly — "cluster sharing the \`vNNapiworker\` stem", not "member of 5-account creation cluster".
Produce a markdown report with two sections:
## TIER 1 — HIGH CONFIDENCE (ban)
-Accounts whose OWN behavior shows strong automation: round-the-clock usage (distinct_hours_24h ≥ 20 AND msgs_24h ≥ 50), or heavy day-1 activity (msgs_24h ≥ 400) on a <1d-old codebuff account linked to a <7d-old GitHub login. A single account may also qualify when multiple weaker signals stack (e.g. heavy usage + fresh GH + throwaway-domain email + round-the-clock pattern).
+The bar is high — if you are choosing between TIER 1 and TIER 2, choose TIER 2.
-Cluster membership is NOT sufficient for TIER 1 on its own. Include it only as corroboration when the cluster shares an explicit cross-account pattern (see above); lead each reason line with the strongest per-account signal, and mention the cluster last.
+Qualifying signals (any one of these, taken on its own, justifies TIER 1):
+1. **Near-continuous activity** — distinct_hours_24h ≥ 18. 15–18 distinct hours is NOT near-continuous, even with heavy message counts — that's a normal motivated power user.
+2. **No quiet gap and heavy usage** — max_quiet_gap < 6h AND high message count (msgs_24h ≥ 700).
+2. **Fresh-GH + another signal** — gh_age < 7d AND (msgs_24h ≥ 700, or cluster with email pattern, or another signal). The fresh GitHub is a strong signal, but you also need something else to justify a ban.
+3. **Multi-signal stack with independent automation evidence** — e.g. cluster of accounts with a shared pattern and heavy usage.
One line of reasoning per account. Group cluster members together under a cluster heading ONLY when the cluster shares a concrete pattern.