Oracle3
Oracle3: 예측시장 거래 엔진·MCP 서버
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Agregador uruguayo de beneficios de tarjetas: descuentos, reintegros y cuotas de bancos, emisores y billeteras, filtrados por las tarjetas que tenés, con lista y mapa. Decisiones de producto: docs/03-spec.md. Investigación: docs/01-research-manguito.md, docs/02-research-uruguay.md. Requiere Node 20+ y pnpm (corepack enable pnpm). Otros comandos: pnpm build, pnpm typecheck, pnpm lint.README 전문 보기 ↗
Agregador uruguayo de beneficios de tarjetas: descuentos, reintegros y cuotas de bancos, emisores y billeteras, filtrados por las tarjetas que tenés, con lista y mapa.
Decisiones de producto: docs/03-spec.md.
Investigación: docs/01-research-manguito.md,
docs/02-research-uruguay.md.
apps/web Next.js 15 (App Router) + Tailwind v4 + shadcn/ui + lucide
packages/core Schemas Zod, tipos compartidos, catálogo de fuentes y rubros
packages/scrapers Un módulo por fuente (BROU primero)
supabase/ Migraciones (PostGIS) y seed del catálogo
Requiere Node 20+ y pnpm (corepack enable pnpm).
pnpm install
cp .env.example .env.local # completar con las claves de Supabase
pnpm dev # http://localhost:3000
Otros comandos: pnpm build, pnpm typecheck, pnpm lint.
/admin es el panel interno (decisiones en docs/04-backoffice.md).
Entra solo quien tenga su id de usuario de Supabase (Authentication > Users) en
ADMIN_USER_IDS, separados por coma; al resto le responde 404. Si esa variable no está, se
usa ADMIN_EMAILS (emails confirmados, separados por coma); sin ninguna de las dos no
entra nadie. El middleware corta la ruta y cada página o server action vuelve a llamar a
exigirAdmin() (apps/web/src/lib/admin.ts). Lee y escribe con SUPABASE_SERVICE_ROLE_KEY,
solo del lado del servidor. En Vercel hay que cargar ADMIN_USER_IDS y la service role key.
Con la CLI de Supabase:
supabase start # Postgres + PostGIS local
supabase db reset # aplica todas las migraciones
Contra el proyecto hosteado (Supabase Free):
supabase link --project-ref <ref>
supabase db push
Migraciones en supabase/migrations/: el esquema, el catálogo (rubros, fuentes y
productos; idempotente), los productos reales de BROU y el caché de páginas crudas.
El esquema define las tablas fuente,
producto, categoria, comercio, sucursal (geography Point 4326), beneficio y la
cola beneficio_revision, más la función sucursales_cercanas() para el mapa y triggers
que recalculan los derivados de comercio. Lectura pública vía RLS; escribe solo el
pipeline con la service role key.
Un módulo por fuente en packages/scrapers/src/fuentes/. El pipeline es
descubrir URLs → bajar → texto plano → normalizar con Claude → validar Zod → upsert.
pnpm --filter @tarjetazo/scrapers scrape brou --limite=3
Las reglas de mapeo de tarjetas viven en normalizador.ts; encima se aplican los alias y
las reglas de ignorar creados desde /admin/revision (tablas producto_alias y
regla_ignorar), que el runner lee al arrancar.
Otros comandos: scrape revisiones revalida la cola de revisión manual contra las
reglas de mapeo actuales, y scrape derivados recalcula el mejor descuento y los conteos de
cada comercio (el cron lo hace al cerrar cada corrida: un beneficio que vence por fecha no
toca ninguna fila y el trigger no se entera), y scrape destrabar cierra corridas que quedaron
interrumpidas (el runner se niega a arrancar si hay una abierta de hace menos de una
hora, para que dos corridas no se pisen el caché).
--solo-fetch baja y cachea las páginas sin llamar a Claude (útil para probar el
descubrimiento sin gastar tokens). Esas páginas quedan con normalizada_en en null, así
que la corrida real siguiente las normaliza aunque el hash no haya cambiado. Necesita SUPABASE_URL,
SUPABASE_SERVICE_ROLE_KEY y ANTHROPIC_API_KEY en el entorno.
Cada página se guarda en pagina_cruda con un hash: si en la corrida siguiente el
hash no cambió, no se vuelve a normalizar y no cuesta nada. Lo que la fuente dejó
de publicar se marca estado_revision = 'descartado' en vez de borrarse.
Dar de baja tiene tres guardas, porque "no lo vimos" no es lo mismo que "la fuente lo
quitó": los beneficios de una página que falló cuentan como vistos, si cae más del 20%
de las páginas no se da de baja nada, y una corrida con --limite nunca da de baja.
Sin esto, una caída de red o un corte de crédito vacían media base en silencio y la
corrida igual termina con éxito. Los
tramos que no validan, o cuyas tarjetas no supimos mapear, van a
beneficio_revision. Cada corrida queda registrada en corrida.
Al final de cada corrida del cron, el workflow manda un resumen a Telegram (scrape resumen):
una línea por fuente, la cola de revisión, las alertas de salud y el costo estimado del
modelo. Se manda siempre: si un día no llega, esa es la alerta. Necesita los secrets
TELEGRAM_BOT_TOKEN y TELEGRAM_CHAT_ID; las corridas manuales con limite no mandan nada.
Corre solo con el workflow .github/workflows/scrapers.yml (cron diario a las
06:00 de Uruguay), que además mantiene despierto el proyecto de Supabase Free.
Secrets del repo: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, ANTHROPIC_API_KEY.
Itaú publica un XML con todas sus campañas (inst/aci/inst_camp.xml), con las bases
completas y locales con coordenadas. Pero ahí los restaurantes adheridos son un único
"15% menos en restaurantes": los nombres están en las landings por rubro
(restaurantes.html, restaurantesplatinum.html, restaurantespersonalbank.html), en
el alt de cada logo. Cada landing es un segmento de tarjeta y se divide en tres
pestañas por ubicación; las pestañas no referencian su panel por id, el contenedor tiene
la barra y después una <section> por pestaña, en el mismo orden.
OCA sirve desde Contentstack con un token de lectura que viene en su propio
JavaScript. Hay que consultar el tipo benefits directo: la página que los referencia
lista 57 de los 72 publicados.
Santander renderiza el listado en el servidor (327 tarjetas con comercio y descuento) pero carga el descuento de cada ficha por AJAX, así que hace falta combinar las dos.
BBVA publica un listado paginado (descuentos.pag-N.html) con una ficha
server-rendered por comercio: rubro y localidad, vigencia, tramos por tier de tarjeta,
locales con link a Google Maps (acortadores goo.gl que se siguen con un HEAD para
llegar a las coordenadas) y legales. Devuelve 403 a cualquier cliente que no parezca un
navegador —no alcanza el User-Agent, hacen falta los headers completos (accept-language,
sec-fetch-*)—, aunque robots.txt no prohíbe nada. Como todas sus fichas siguen la misma
plantilla, no pasa por el modelo: bbva-parser.ts las convierte en tramos de forma
determinista y el runner usa ese normalizador propio (normalizar?) en vez de Claude.
Es el patrón para cualquier fuente con plantilla fija.
Scotiabank embebe su catálogo en el índice como objetos JS (pushBenefit({...}))
con porcentaje, tarjetas, días, departamento, vigencia y legales por beneficio. Tampoco
pasa por el modelo: el fetch le pasa esos campos a scotiabank-parser.ts en
Crudo.datos (que no se guarda ni entra en el hash) y el parser saca de los legales
solo lo que el catálogo no trae: topes, fechas concretas de la promo y "no acumulable".
Itaú tampoco pasa por el modelo: el feed y las landings pasan sus campos en
Crudo.datos a itau-parser.ts. Las landings son un encabezado fijo por tramo con sus
pestañas (Montevideo, Punta del Este, Interior); los items del feed se parten en
cláusulas, una por porcentaje, 2x1 o cuotas. La lectura de legales que comparten los dos
parsers (días, departamentos, fechas, topes) está en fuentes/legales.ts.
scrape ingerir <json> carga beneficios ya normalizados con las mismas guardas del
pipeline (Zod, ids deterministas, upsert idempotente) y deja la página marcada como
normalizada: sirve para un backfill hecho a mano o revisado. --limite llega también al
fetch por SCRAPER_LIMITE, así probar una fuente grande no baja todo su catálogo.
Guarda adicional contra bajas falsas: si el fetch trae menos de la mitad de las páginas conocidas (o ninguna), no se da de baja nada — es casi seguro que la fuente bloqueó al scraper, no que haya retirado medio catálogo.
Todo el sitio es server-rendered, así que no hace falta Playwright. El mismo
beneficio se publica bajo varias categorías (/gastronomia/expocafe y
/destacados/expocafe son la misma página), por eso la identidad es el slug
final. Ni el sitemap ni las páginas de categoría listan todo por separado, así
que se unen las dos fuentes de URLs. Una página suele contener varios beneficios
(distinto porcentaje según el tier de la tarjeta): cada uno es una fila.
Los bancos no publican direcciones: de 88 páginas de BROU, una sola las trae. Así que
las sucursales salen de OpenStreetMap para las cadenas que un beneficio nombra
(packages/scrapers/src/geo/cadenas.ts), y el geocodificador queda listo para cuando
una fuente sí publique la dirección.
pnpm --filter @tarjetazo/scrapers geo osm # sucursales de cadenas desde OSM
pnpm --filter @tarjetazo/scrapers geo pendientes # geocodifica las que tienen dirección sin punto
pnpm --filter @tarjetazo/scrapers geo localidades # localidades del país, para el buscador de zona
direcciones.ide.uy es el geocodificador principal: resuelve en dos pasos
(candidates interpreta el texto libre, find devuelve el punto) y su reverse
completa el departamento de los locales de OSM, que rara vez traen addr:state.
Nominatim queda de fallback, con su límite de 1 req/s. Todo pasa por geocode_cache,
que cachea también los fallos para no reintentarlos cada corrida.
Dos cosas aprendidas peleando con Overpass, por si hay que tocarlo:
area(3600287072) (Uruguay)
y nombres exactos tardan segundos. Por eso cadenas.ts lista nombres literales.bbox en vez de área entra ruido argentino: una prueba devolvió una estación de
tren de Buenos Aires llamada "Devoto".Tokens de marca en apps/web/src/app/globals.css: paleta cielo / menta / sol (más coral
para avisos), con variantes -s (fondo suave), -ln (línea) e -ink (texto); superficies
hueso/papel, texto tinta/humo, escala de radios y gradiente de marca. Tipografías:
Outfit (display), Inter (texto), Archivo tabular para números (.num).
Las vars semánticas de shadcn/ui están mapeadas encima, así que pnpm dlx shadcn@latest add <x>
sale con la marca puesta.
PostHog para eventos agregados (tarjetas_elegidas, filtro_aplicado, busqueda,
comercio_elegido, click_saliente) y Vercel Analytics para las visitas. Sin perfiles
de personas, sin grabación de sesión y respetando "Do Not Track". Si no hay
NEXT_PUBLIC_POSTHOG_KEY, no se inicializa nada.
Vercel, con Root Directory = apps/web y las variables de .env.example. El dominio
tarjetazo.uy se registra en nic.uy y se apunta a Vercel;
tarjetazo.com.uy redirige al canónico.
Los beneficios pertenecen a sus fuentes; cada ficha enlaza a la publicación oficial. Corregimos o damos de baja contenido a pedido. Nunca pedimos el número de tu tarjeta.