Saltar al contenido principal

Anuncios y Reparto de Ingresos — Referencia técnica

← Volver a Anuncios y Reparto de Ingresos

Dónde vive esto

Backend

Frontend (web)

  • apps/frontend-nextjs/src/components/ProfileAdSlot.tsx — el espacio de anuncio del perfil (AdSense / GAM-GPT / marcador de posición propio) + reporte de impresión/clic
  • apps/frontend-nextjs/src/page-components/PublicProfilePage.tsx — renderiza <ProfileAdSlot profileUserId={user.id} />
  • apps/frontend-nextjs/src/app/layout.tsx — loader de AdSense a nivel de todo el sitio (id="adsense-loader", strategy="beforeInteractive"), condicionado a NEXT_PUBLIC_ADSENSE_CLIENT. Lo exige el paso de verificación de cuenta / Auto ads de AdSense ("pega esto en el <head> de cada página"), independientemente de si algún perfil tiene realmente un espacio profile + una entrada en la lista beta. next/script deduplica por id, así que no se carga dos veces cuando ProfileAdSlot.tsx también renderiza el mismo script en una página de perfil.

Frontend (admin)

Checklist de implementación técnica

  • profileAdsEnabled / activeAdPlacements (cliente) — determinan si el espacio se renderiza y qué se muestra
  • recordAdImpression / recordAdClick — el espectador se toma del contexto de autenticación; el ingreso se estima del lado del servidor, nunca declarado por el cliente
  • myAdEarnings — el libro mayor mensual propio de un creador
  • adminAdSettings / adminUpdateAdSettings — leer/actualizar el reparto, el CPM, los umbrales (validados 0–100 / no negativos)
  • adminAdPlacements / adminCreateAdPlacement / adminUpdateAdPlacement — CRUD de espacios de anuncio
  • adminAdEnabledUsers / adminSetUserAdsEnabled — lista permitida de beta
  • adminSettleAdRevenue — liquidar un período a partir de la estimación del lado del servidor
  • adminAdRevenueSummary / adminAdRevenueLedger — dinero generado + libro mayor por creador
  • adminIngestAdRevenueReport / adminImportAdRevenueFromGam — liquidar a partir de ingresos reales (CSV / GAM)
  • adminCreditUserCoins — transferencia/ajuste manual de monedas
  • Cliente de la API de reportes de GAM cableado dentro de gam-report.service.js#fetchCreatorRevenueimplementado en esta sesión: llamadas REST reales contra la Ad Manager API (Beta) vía GoogleAuth de googleapis, ya no un stub. Todavía necesita credenciales reales de GAM_SERVICE_ACCOUNT_KEY/GAM_NETWORK_CODE para funcionar en producción — ver la salvedad más abajo.
  • Loader de AdSense a nivel de todo el sitio en app/layout.tsx — el <script> que el flujo de configuración de AdSense de Google pide pegar en el <head> de cada página (verificación de cuenta + Auto ads), agregado como un next/script beforeInteractive condicionado a NEXT_PUBLIC_ADSENSE_CLIENT

Superficie de GraphQL

Esquema de cliente (/web/graphql):

type Query {
activeAdPlacements: [AdPlacement!]!
profileAdsEnabled(userId: ID!): Boolean!
myAdEarnings(limit: Int, offset: Int): [AdRevenueLedgerEntry!]!
}

type Mutation {
recordAdImpression(input: RecordAdEventInput!): AdEventResult!
recordAdClick(input: RecordAdEventInput!): AdEventResult!
}

input RecordAdEventInput { placementId: ID!, profileUserId: ID! }

Esquema de administrador (/admin/graphql, todos con prefijo admin, solo super_admin):

type Query {
adminAdSettings: AdSettings!
adminAdPlacements: [AdPlacement!]!
adminAdEnabledUsers: [AdBetaUser!]!
adminAdRevenueSummary(period: String!): AdRevenueSummary!
adminAdRevenueLedger(period: String!, limit: Int, offset: Int): [AdRevenueLedgerAdminEntry!]!
}

type Mutation {
adminUpdateAdSettings(input: AdSettingsInput!): AdSettings!
adminCreateAdPlacement(input: AdPlacementInput!): AdPlacement!
adminUpdateAdPlacement(id: ID!, input: AdPlacementUpdateInput!): AdPlacement!
adminSetUserAdsEnabled(userId: ID!, enabled: Boolean!): Boolean!
adminSettleAdRevenue(period: String!): AdSettlementResult!
adminIngestAdRevenueReport(period: String!, entries: [AdRevenueReportEntryInput!]!): AdSettlementResult!
adminImportAdRevenueFromGam(period: String!): AdSettlementResult!
adminCreditUserCoins(userId: ID!, amount: Int!, reason: String): CoinCreditResult!
}

El enrutamiento de queries/mutations entre los dos esquemas se hace por convención de nombres — los campos que coinciden con /^admin[A-Z]/ se exponen solo en /admin/graphql (ver api/server.js#scopeSchemaToAdmin). Por eso el panel de administración usa adminAdPlacements en lugar del activeAdPlacements, exclusivo del cliente.

Configuración (fila de system_setting)

La configuración de anuncios vive en columnas tipadas de la única fila de system_setting (leída vía payout-settings.access-service.js, no el gestor de configuraciones clave/valor). Los valores monetarios están en micros.

ColumnaSignificadoValor por defecto
ads_enabledActivación/desactivación globaltrue
ads_creator_share_percentParte del bruto que corresponde al creador (0–100)55
ads_coins_per_currency_unitMonedas acreditadas por cada unidad monetaria de la parte del creador100
ads_min_credit_coinsMínimo de monedas antes de acreditar a un creador50
ads_impression_cpm_microsIngreso estimado por cada 1,000 impresiones (micros)0
ads_click_value_microsIngreso estimado por clic (micros)0

Edita estos valores desde /adsAd settings, o directamente vía adminUpdateAdSettings.

Modelo de datos

ad_placement (un espacio) → ad_event (impresiones/clics, con estimated_revenue_micros estimado por el servidor) → agregado en la liquidación dentro de ad_revenue_ledger (una fila por creador por período: impresiones, clics, gross_revenue_micros, creator_share_micros, platform_share_micros, creator_share_coins, status, coin_transaction_id). La lista permitida es el booleano ads_enabled en user.

Configuración e instalación

1. Aplica las migraciones

npm run migrate
npm run migrate:test # si ejecutas la base de datos de pruebas

Esto crea las tablas de anuncios, la columna ads_enabled del usuario, y las columnas de configuración de anuncios en system_setting.

2. Elige un proveedor de anuncios / modelo de pago

  • Fondo de creadores (monedas) — no requiere una cuenta publicitaria externa. Configura ads_impression_cpm_micros / ads_click_value_micros, ejecuta la liquidación mensual, y los creadores reciben el pago en monedas. Lo más simple; funciona hoy mismo.
  • Google AdSense — establece NEXT_PUBLIC_ADSENSE_CLIENT (por ejemplo, ca-pub-…) en frontend-nextjs, y crea un espacio de anuncio con proveedor adsense cuyo providerSlotId sea el data-ad-slot de AdSense. Nota: compartir las ganancias de AdSense con terceros va contra la política — usa el modelo de monedas/fondo de creadores para el reparto. Establecer esta variable también activa el loader a nivel de todo el sitio en layout.tsx (ver más abajo) — la verificación de cuenta de AdSense lo exige sin importar si se usa el espacio de anuncio de perfil / reparto de ingresos.
  • Google Ad Manager + MCM — la ruta conforme para ingresos por creador. Establece NEXT_PUBLIC_GAM_NETWORK_CODE en frontend-nextjs, crea un espacio de anuncio con proveedor admanager cuyo providerSlotId sea la ruta de la unidad de anuncio de GAM. El espacio etiqueta cada impresión con un valor clave creator = el id del dueño del perfil, de modo que los reportes de GAM atribuyen el ingreso por creador.

3. Configura el espacio de anuncio y los usuarios beta

En /ads:

  1. Placements → crea un espacio de anuncio activo con surface: profile (los anuncios solo se renderizan cuando existe un espacio profile activo).
  2. Ad settings → establece el reparto y los valores de CPM/clic.
  3. Beta allowlist → busca creadores por nombre de usuario y agrégalos.

4. Variables de entorno

VariableAppPropósito
NEXT_PUBLIC_ADSENSE_CLIENTfrontend-nextjsId de publicador de AdSense; habilita el renderizado de AdSense en el espacio del perfil y el loader de verificación/Auto ads a nivel de todo el sitio en app/layout.tsx
NEXT_PUBLIC_GAM_NETWORK_CODEfrontend-nextjsCódigo de red de GAM; habilita el renderizado de GPT
GAM_NETWORK_CODEbackendCódigo de red de GAM para la API de reportes
GAM_SERVICE_ACCOUNT_KEYbackendClave de cuenta de servicio (ruta o JSON) para la API de reportes
GAM_CREATOR_KEYbackendNombre de la clave de segmentación personalizada (por defecto creator)

Sin las variables NEXT_PUBLIC_* el espacio muestra un marcador de posición propio neutral. Sin las variables GAM_* del backend, adminImportAdRevenueFromGam devuelve un gam.not_configured capturable y en su lugar se usa la importación por CSV.

5. Liquida un período

  • Estimado: /adsMonthly settlement → ingresa YYYY-MMRun settlement. Agrega los eventos, aplica el reparto, acredita las monedas.
  • Real (CSV): exporta un reporte de GAM dimensionado por el valor clave creator con ingreso (micros), impresiones y clics. En /adsImport real revenue, pega las filas identifier, grossMicros[, impressions, clicks] (el identificador se detecta automáticamente como un id de usuario cuando es un UUID, de lo contrario como un nombre de usuario) → Ingest CSV & settle.
  • Real (API): una vez que GAM_* esté establecido y el cliente de reportes esté cableado en gam-report.service.js#fetchCreatorRevenue, usa Import from Google Ad Manager para extraer y liquidar en un solo clic.

La liquidación es idempotente por período: los creadores que ya fueron acreditados se omiten. Las filas de reporte no resueltas se cuentan como skipped.

6. Ver el dinero y pagar manualmente

  • /adsRevenue → elige un período para ver los totales bruto / plataforma / creador (unidades monetarias) y un libro mayor por creador. El botón Transfer coins de cada fila abre un modal precargado con las monedas de la parte del creador ya calculadas — el monto es editable.
  • /adsManual coin transfer → busca cualquier usuario y transfiere una cantidad arbitraria de monedas con un motivo. Ambos pasan por la billetera normal (transactionType: 'reward', relatedType: 'admin_manual') y alimentan el flujo de retiro (cashout).

El cliente de la API de Reportes de GAM

gam-report.service.js#fetchCreatorRevenue(period) es el único punto de integración, y está realmente implementado desde esta sesión (antes era un stub deliberado que arrojaba gam.report_client_unavailable). No existe un cliente de descubrimiento de googleapis (npm) para Ad Manager — solo cubre productos publicitarios heredados (adexchangebuyer, dfareporting, adsense, ...) —, así que esto habla directamente con la API REST moderna (Ad Manager API, Beta, https://admanager.googleapis.com/v1), autenticada con google.auth.GoogleAuth del paquete googleapis ya instalado (el mismo patrón de cuenta de servicio que usa services/iap/google-iap.service.js). El reporte en sí es un flujo asíncrono basado en trabajos, según la API Beta:

  1. POST networks/{net}/reports — crea la definición del reporte
  2. POST networks/{net}/reports/{id}:run — inicia una ejecución (operación de larga duración)
  3. GET networks/{net}/operations/reports/runs/{opId} — sondea hasta que termine
  4. GET networks/{net}/reports/{id}/results/{resultId}:fetchRows — obtención de filas paginada

dimensionado por la clave de segmentación personalizada de la red que coincide con GAM_CREATOR_KEY (resuelta a su id numérico vía networks/{net}/customTargetingKeys, luego pasada como ekvDimensionKeyIds), con las métricas AD_SERVER_ALL_REVENUE (ingreso bruto, ya en micros), AD_SERVER_IMPRESSIONS y AD_SERVER_CLICKS. importFromGam luego alimenta las filas devueltas [{ creatorId, grossMicros, impressions, clicks }] directamente a ingestRevenueReport, que reutiliza la lógica de reparto/crédito — el dinero de GAM ya está en micros, así que no se necesita conversión.

Todavía necesita credenciales reales de GAM_SERVICE_ACCOUNT_KEY/GAM_NETWORK_CODE para funcionar en producción — está listo y probado con mocks, pero aún no se ha ejercitado contra una red de GAM real. El propio comentario de cabecera del archivo señala una salvedad honesta que vale la pena trasladar aquí: la Ad Manager API está en Beta y la documentación de referencia de Google no enumeraba por completo cada valor de enum de Dimension/Metric al momento de escribir esto. La constante de dimensión EKV_0_VALUE (junto con ekvDimensionKeyIds) y el campo usado para hacer coincidir el nombre legible de una clave de segmentación personalizada (verificado como displayName/keyName/name) son el mapeo mejor documentado disponible, pero no fueron verificados de punta a punta contra una red real. Si la API en vivo rechaza una solicitud, revisa la referencia de la Ad Manager API para conocer los nombres de enum vigentes y ajusta las constantes al inicio del archivo.

Un testConnection() de solo lectura (autenticación + resolución de la clave de segmentación personalizada, sin llegar a ejecutar un reporte) respalda la prueba de conectividad del grupo ads en la página de Estado del entorno.