driftless market comparten los mismos servicios in-process y siempre devuelven la proyección pública abstracta: operaciones semánticas tipadas, sin SQL, detalle masivo de proveedores ni coordenadas de contacto. El tool belt interno puede componer lecturas acotadas y redactadas para síntesis, sin convertirse en una superficie de extracción. Quien llama planea la secuencia y sintetiza la respuesta.
Cada ruta está acotada al workspace y requiere autenticación como el resto de la API — no hay ninguna ruta pública/sin autenticar en market-data.
market_capabilities es descubrimiento gratuito; las otras doce operaciones de market data son análisis medidos y atribuibles sobre un warehouse compartido.Ruta base
Las trece operaciones de market data
Cada ruta de abajo es relativa a la ruta base de arriba.example es un cuerpo de petición mínimo y válido, tomado directamente de la tabla de rutas que la propia API sirve en GET capabilities: el method/path/example de cada operación es descubrible en tiempo de ejecución, no solo documentado acá.
GET coverage y GET capabilities/compact se suman a estas como rutas de discovery propiedad de la plataforma (ver Discoverability más abajo).
Suppliers
search_suppliers necesita al menos una dimensión de acotamiento (query, state, scian_codes, source_slugs, o rfc) — un escaneo sin límite de todo el directorio se rechaza con query_too_broad en vez de servirse. Cada resultado es una observación publicada, nunca una empresa consolidada ni una demostración de capacidad; lleva la presencia de contacto y sus conteos, no los valores de contacto. Las respuestas HTTP, OAuth, MCP y CLI siempre usan la proyección abstracta y sólo exponen disponibilidad; las coordenadas canónicas permanecen dentro del servicio/auditoría.
count_suppliers comparte la misma regla de acotamiento y devuelve un entero exacto { "count": number }, calculado con SELECT count(*), nunca muestreado ni estimado. Si el conteo mismo no puede terminar dentro del statement timeout, falla con market_data_timeout en vez de sustituir una aproximación.
El belt interno de investigación puede abrir hasta 50 observaciones ya seleccionadas en una llamada in-process para síntesis acotada. Ese batch no es una operación HTTP, OAuth, MCP ni CLI y devuelve sólo observaciones ya redactadas. Las referencias inválidas, no disponibles y no licenciadas comparten intencionalmente un solo estado invalid_or_unavailable.
compare_segments compara hasta cinco segmentos definidos por texto en hasta diez estados mexicanos, con un límite duro de 50 celdas y un solo observed_kind obligatorio. Sus conteos son observaciones publicadas, nunca empresas únicas, TAM ni evidencia de demanda. contact_breakdown: true añade un conteo comparable de observaciones con un canal de contacto publicado.
Opportunities y awards
search_opportunities devuelve actionability y actionability_reason en cada fila. No hay campo de fecha límite: este corpus no publica ninguno, y la capa se rehúsa a inventar uno — is_open es un estado grabado al momento de la carga, no una verificación en vivo. get_opportunity devuelve sus awards como una child collection anidada awards más award_count, nunca un join plano (un procedimiento puede llevar cientos de awards).
Un award es una publicación — se registró que un contrato fue adjudicado — nunca un pago, una entrega o una ejecución. supplier_rfc es identidad fuerte; supplier_name es aproximado y devuelve candidatos. get_supplier_history requiere supplier_rfc, currency y amount_scope juntos (una historia indexada por nombre es la historia de un string), y su respuesta lista cada otro par (currency, amount_scope) en el que ese RFC tiene filas, para que el total devuelto nunca se lea como la historia completa. search_awards también acepta cog_partidas — de 1 a 20 códigos de cinco dígitos de “partida específica” SHCP (Clasificador por Objeto del Gasto) que el comprador asignó al momento de la adjudicación, comparados por overlap de arreglo, de modo que un contrato que lleva varios códigos hace match con cualquiera de ellos; un código que no tiene forma de cinco dígitos se rehúsa como invalid_field_value antes de leer el corpus. Cada fila de award lleva cogPartidas ([] cuando el publicador no registró ninguno).
aggregate_awards requiere currency, amount_scope, y al menos un group_by en la lista permitida (supplier_rfc, buyer_name, buyer_acronym, procedure_type, contracting_type, supplier_size, award_month, award_year, cog_partida, cog_capitulo — como máximo tres). supplier_name deliberadamente no es una dimensión de agrupación: agrupar por identidad aproximada fusionaría organizaciones distintas. Los montos nunca se suman entre currencies ni entre amount scopes (supplier_contract y award_group_published_total son cantidades distintas).
compare_period
Pasa compare_period: { from_date, to_date } junto con el from_date/to_date propio de la petición para correr una comparación periodo contra periodo dentro del mismo statement agrupado, nunca una segunda consulta:
results no se traduce a snake_case):
delta_pct se calcula en Postgres en numeric y se castea a float8 solo después de que la división exacta ya decidió si el denominador era cero — nunca es Infinity ni un número inventado cuando el periodo A totalizó cero. El orden de ranking y de paginación siempre es el del periodo B: compare_period compara contra el ranking, nunca cambia qué se está rankeando. Un cursor emitido sin compare_period se invalida (invalid_cursor) si se reanuda con uno, y viceversa.
group_by: cog_partida / cog_capitulo
Estas dos dimensiones agrupan por el/los código(s) de objeto del gasto SHCP que lleva un contrato mixto — cog_partida sobre el código de cinco dígitos, cog_capitulo sobre su primer dígito — desanidando cog_partidas antes de agrupar. Un contrato que lleva varios códigos no se reparte entre ellos: cuenta íntegro bajo cada código que lleva, así que el awardCount/totalAmount de un grupo puede duplicar respecto al corpus, y la suma entre grupos puede exceder el total del corpus. Esto nunca se resuelve prorrateando — el publicador nunca declaró cómo dividir un contrato entre sus códigos. Agrupar por cualquiera de las dos dimensiones siempre adjunta la advertencia semántica cog_partida_totals_may_exceed_corpus, y ambas componen con compare_period igual que cualquier otro group_by. No existe un filtro cog_capitulo en search_awards — un escaneo sin índice del primer dígito sobre las ~500K filas que sirve esta relación se consideró demasiado lento para ofrecerlo; aggregate_awards es la forma de hacer una pregunta con forma de capítulo.
Marcas de riesgo
search_risks encuentra marcas adversas publicadas — un listado de supplier vetado (efos) o una sancion/inhabilitación. rfc es identidad fuerte; entity_name es aproximado y devuelve candidatos con sus RFCs. Una marca es un listado publicado, nunca una condena judicial, y una parte puede llevar varias, incluyendo exoneraciones publicadas.
screen_risks agrupa de 1 a 50 RFCs en un solo escaneo acotado rfc = ANY($1) en vez de una llamada a search_risks por RFC:
results lleva una entrada por cada RFC pedido, en el orden pedido, incluyendo un RFC con cero marcas. Ese cero sólo está respaldado cuando el envelope declara coverage efectiva de riesgos: en ese caso la entrada lleva warnings: ["zero_results_with_coverage"], nunca como evidencia sobre otro RFC del batch. El envelope repite el warning únicamente si todo el batch quedó en cero. Sin coverage licenciada y visible, ninguno de los dos niveles lo emite y la declaración de coverage sin fuentes expresa la limitación. Más de 50 RFCs se rechaza con batch_too_large en vez de truncarse en silencio.
Permits
search_permits encuentra permits, concesiones y compromisos de capex publicados. holder_rfc es identidad fuerte donde se publica (raro en este corpus); holder_name es aproximado y es el join sobre el que se construyó esta relación. Un permit es un derecho otorgado grabado al momento de la carga, nunca una verificación operacional en vivo — trata is_active_risk, status_text, is_expansion, y los campos de capex/capacidad (investment_mdd, capacity_mw, estimated_generation) como lo que el publisher imprimió, no como gasto auditado o desembolsado.
El envelope
Cada respuesta exitosa — search, get, aggregate, count, screen por igual — es un solo envelope, y cada key en él es camelCase, deliberadamente distinta del snake_case que usa el resto de la plataforma (Knowledge y Collections). Esto no es una inconsistencia por corregir: es el dialecto que ya habla el contrato de market-data en producción, el resultado de la tool de MCP, y el plugin de ChatGPT.interpretedRequestes lo que la capa realmente corrió después de normalizar la entrada de quien llama (un nombre de state resuelto a su código, un municipio impreciso resuelto a su ortografía canónica) —normalizationsdice qué cambió y por qué.resultses el payload propio de la operación: un array de filas para un search, un objeto para una lecturaget_*/count_*, un array de grupos para un aggregate.pageesnullcuando la operación no pagina (get_supplier,get_opportunity,market_capabilities,count_suppliers,compare_segments).coverageycorpusBasisson lo que hace honesta una respuesta: qué fuentes publicadas contribuyeron, y qué snapshot del corpus vio esta lectura. Ninguno de los dos puede ser autorado por un modelo o por quien llama.semanticWarningsnombra una mala lectura que este dominio invita activamente (un award no es un pago, una marca de riesgo no es una condena, un nombre es identidad aproximada) — siempre adjuntado por la capa, nunca decoración.provenancedice dónde se leyó cada fila, explícitamente no que esté vigente o verificada.
Lo que ve un modelo: categorías de evidencia, no publicadores
El servicio conserva una proyección canónica para auditoría y soporte, pero la API HTTP de arriba siempre devuelve la proyección abstracta:source_slug,
source_record_id, identidad del publicador, internals de recuperación y
coordenadas de contacto nunca llegan a un caller HTTP autenticado.
Una superficie que lee un modelo — Chat, Research y cada tool de MCP — lee el
mismo envelope a través de una proyección abstracta. La identidad del publicador
se reemplaza por el tipo de evidencia que devolvió la operación, derivado de la
relación del warehouse y no de una tabla de publicadores:
Bajo esa proyección cada fila lleva
evidence_category,
evidence_category_label, un record_ref opaco y un
record_fingerprint; coverage agrupa por categoría y cuenta solo las filas
licenciadas para mostrarse; y el cursor de página va sellado. Cada advertencia
semántica, conteo de cobertura, valor de frescura, fuerza de identidad y alcance
de monto queda intacto.
record_ref es lo que toma get_supplier: cópialo textual de la fila de
búsqueda cuyo detalle quieres. Es un ciphertext nuevo por respuesta, así que es
un handle para seguir y nunca un valor para comparar entre llamadas: dentro de UN
mismo envelope una fila lleva el mismo record_ref en todas partes donde aparece
(la fila de results[] y su entrada en provenance[] coinciden), y la siguiente
respuesta acuña uno distinto para la misma fila. Para reconocer una fila entre
respuestas, únelas por record_fingerprint — nunca por record_ref. Una
referencia que esta capa no emitió se rehúsa con invalid_record_ref.
record_fingerprint es el valor que comparten dos observaciones de la misma
fila — 128 bits, con llave y de una sola vía. Revela deliberadamente
igualdad: quien lo tiene puede ligar el mismo registro publicado entre
páginas, llamadas y corridas, que es lo que la deduplicación necesita. No revela
identidad: no hay operación que lo convierta de vuelta en un source_slug,
un source_record_id ni nada más sobre la fila, y se rehúsa donde se espera un
record_ref. Sobrevive a una rotación de la llave primaria mientras una
referencia emitida antes de esa rotación siga resolviendo, para que una fila ya
vista siga siendo reconocible.
No hay filtro de fuente en las superficies model-facing. source_slugs se
acepta en la ruta canónica y no se anuncia en ninguna otra parte. No tiene
reemplazo abstracto, deliberadamente: un filtro por “tipo de registro”
necesitaría metadata por fuente que esta capa no tiene, y derivarlo de una lista
de slugs congelada dentro de la API inventaría un valor dependiente del corpus
fuera del corpus. Una pregunta de membresía (“¿está X en ese padrón
específico?”) se responde entonces buscando, y la respuesta dice qué encontró la
búsqueda en vez de de qué padrón vino. Ver docs/market-data/tool-contract.md
para la metadata del warehouse que devolvería el filtro.
La proyección la elige el entrypoint, nunca el payload de una petición: no es
un parámetro, y no hay valor que quien llama pueda enviar para ampliar lo que ve.
Paginación — keyset, nunca offset
Cada operación que devuelve más de una fila (search_*, aggregate_awards, get_supplier_history) pagina con un cursor keyset opaco, nunca OFFSET:
page.nextCursor lleva la versión del contrato (para que un cursor de una forma de respuesta más vieja no se pueda reanudar contra una más nueva), el snapshot del corpus contra el que se emitió (para que la paginación nunca pueda entrelazar dos snapshots publicados), un digest de los filtros — y, para aggregate_awards, de compare_period — para que continuar con un conjunto de filtros distinto se rechace en vez de servirse, y la posición de la última fila en el orden total.
page.hasMore/page.returned se calculan con una lectura honesta de limit + 1, nunca se infieren de returned === limit. De un cursor roto salen dos refusals distintos porque la recuperación correcta difiere: un cambio de filtro a mitad de paginación es invalid_cursor; un cambio de corpus es cursor_stale — no es error de nadie, los datos publicados se movieron. Ambos se recuperan igual: reiniciar sin cursor.
El tamaño de página por defecto es 20 (50 para aggregate_awards); limit topa en 50 para toda operación con forma de search y en 200 para aggregate_awards.
Refusals
Un refusal nunca es un 400 desnudo. Todo rechazo específico del dominio lleva un bloquemarket_data junto al code/message/request_id propio de la plataforma:
whyexplica el malentendido, no solo la regla — la mayoría de los refusals en este dominio son una pregunta bien formada sobre un campo o una forma que no significa lo que quien llama asumió, no un error de tipeo.suggested_correctiones prosa dirigida a un LLM: la siguiente llamada concreta que funcionaría.recovery.actiones el mismo hecho, legible por máquina, tomado de un vocabulario cerrado para que quien llama pueda ramificar sin parsear prosa. Se deriva uno a uno desemantic_code— nunca se define de forma independiente.retryableesfalsepara todo refusal de forma/valor/vocabulario (la misma llamada reproduce el mismo refusal), ytruesolo para el puñado operacional — timeout, no disponible, proyección stale — donde se espera que el mundo cambie, no la petición.
suppliers/count reutiliza query_too_broad; risks/screen reutiliza invalid_field_value/batch_too_large — ninguna operación inventa su propio vocabulario de error de un solo uso.
Una violación estructural del DTO (un enum inválido, un
limit fuera de rango, forbidNonWhitelisted rechazando una propiedad desconocida) conserva el envelope de error de la plataforma tal cual — VALIDATION_FAILED, mismo message, mismo status. En las rutas de market-data ahora TAMBIÉN lleva un bloque market_data con allowed_values (el conjunto cerrado completo que el decorador aplica) y recovery.action: fix_arguments, porque el servidor sabe exactamente qué habría aceptado y un refusal que se lo guarda no enseña nada. El semantic_code es invalid_field_value. En el resto de la plataforma una violación de DTO no cambia y no lleva ese bloque.Discoverability
Hay dos cosas que un cliente ciego (un agente que solo tienecurl y las descripciones propias de esta API) necesita y que un 404 desnudo no puede darle: qué verbo sí habría funcionado, y qué rutas existen en absoluto.
- Verbo equivocado, ruta real →
405 Method Not Allowedcon un headerAllowcompatible con RFC 9110 que nombra los métodos que sí funcionan, más el mismo hecho en el cuerpo JSON (allowed_methods). - Ruta desconocida bajo
market-data/*→404condocumentation_url(la propia rutacapabilitiesdel workspace) yoperations— la lista completa de método/ruta — en vez de dejar que quien llama adivine desde el silencio:
operations lista las trece, recortado arriba por espacio.)
GET capabilities es el contrato completo para integradores: el question, requires, refuses, method, path, y un example ejecutable de cada operación, más las listas de valores de filtro observadas en el corpus (nombres de state, códigos SCIAN, procedure types, …), cada una estampada con el corpusBasis contra el que se leyó — nunca presentada como una constante atemporal. Un query param opcional ?facets=a,b acota tanto el cómputo como la respuesta a las dimensiones del corpus nombradas. GET capabilities/compact es la proyección acotada y consciente de la fuente que se inyecta a los runtimes de agente (chat, MCP) en vez de gastar una llamada a tool en el catálogo completo. Ambas llevan openapiUrl, que apunta al documento OpenAPI legible por máquina — servido root-absolute en /openapi.json, a diferencia de cualquier otra ruta de market-data, que es relativa al workspace.
GET coverage devuelve el mismo bloque coverage que se adjunta a cada otra respuesta, de forma independiente: qué fuentes publicadas contribuyeron a cada relación, qué tan fresca está, y si está licenciada para mostrarse. Es lo que separa “sin resultados” de “no existe ese mercado”.
Análisis frente a activación de contactos
Las preguntas a gran escala usan conteos, comparaciones, historiales y agregados de adjudicaciones calculados en el servidor. Una respuesta agregada cuesta un crédito de insight aunque contenga muchos grupos; las filas subyacentes de empresas o contactos no se exportan. La investigación de contactos es una capacidad separada y opera únicamente sobre registros CRM seleccionados por el usuario:driftless_contact_quotelee la selección exacta y el saldo. Es gratuita, no llama a proveedores externos y no devuelve coordenadas de contacto.- Después de aceptar explícitamente la cotización,
driftless_contact_unlockenvía los mismos ids,confirm: true, elmax_creditsaprobado, elquote_tokenopaco emitido por el servidor (válido durante 10 minutos) y elidempotency_keyemitido por esa misma cotización. El servidor recalcula el precio y se niega antes de gastar si aumentó o si cambió el saldo o el permiso.
Autenticación y scopes
Las rutas de market-data requieren la misma autenticación que el resto de la API (API key o token bearer de OAuth) — no hay ninguna ruta pública. Sobre OAuth, el análisis usa el scope dedicadomarket_data:read, en OR-list con context:read por compatibilidad hacia atrás. La cotización gratuita de Contact Path sobre registros CRM seleccionados usa commercial:read; el desbloqueo pagado usa commercial:activate:
MCP: tools tipadas de market data
MCP expone trece tools tipadasdriftless_market_* para análisis más las tools de dos pasos de Contact Path. No expone driftless_market_data, driftless_market_get_supplier_batch, selectores de fuente física ni coordenadas de contacto. Reconecta el cliente después de este cambio de schema porque los conectores pueden guardar definiciones de tools en caché. Ver Referencia MCP para conectar un cliente.
Las trece tools de market data llevan readOnlyHint: true: no modifican datos de negocio ni producen efectos externos. Esa anotación no significa que una llamada sea gratuita. driftless_market_capabilities cuesta cero llamadas y cero créditos; cada una de las otras doce tools de market data se mide conforme al contrato de uso comercial.
driftless_contact_quote; sólo después de mostrar el costo exacto se permite driftless_contact_unlock con confirm: true, max_credits, el quote_token opaco emitido por el servidor (válido 10 minutos) y el idempotency_key emitido por esa misma cotización. Registros ya desbloqueados cuestan cero y los resultados parciales se reportan por registro.
El resultado de la tool agrega la paginación estándar de la casa al nivel superior (shown, has_more) junto al propio page.returned/page.hasMore del envelope — aditivo, nunca un reemplazo — más una pista next_action que le dice a quien llama qué hacer después (continuar con un cursor, inspeccionar un candidato con get_supplier, leer coverage antes de concluir “sin marcas”, etcétera).
En operaciones textuales de proveedores, oportunidades y adjudicaciones (incluyendo aggregate_awards), query_mode es opcional: auto preserva el comportamiento existente; exact_phrase, all_terms y any_terms son interpretaciones acotadas del texto tokenizado. Un modo explícito en oportunidades usa recuperación léxica y no puede combinarse con strategy=hybrid. interpretedRequest.query_interpretation, escrito por la plataforma, declara el modo pedido y aplicado, términos normalizados, aliases de estado y filtros reconocidos, y cualquier advertencia. Nunca incluye SQL, schema ni una consulta compilada.
Los clientes MCP (claude.ai, ChatGPT) cachean los schemas de herramientas por sesión de conector. Cuando cambian las tools o sus parámetros, reconecta el conector (o arranca una sesión nueva) para ver la superficie tipada nueva — ver MCP y OAuth para la nota general sobre este caching.
CLI: driftless market
--json para el envelope semántico completo; sin él, la CLI imprime un resumen humano compacto (cantidad de filas, etiqueta por fila, warnings, coverage, próximo cursor). Repite un flag de array o pasa valores separados por coma. --compare-from-date/--compare-to-date son flags planos de la CLI que se anidan en un solo objeto compare_period sobre el wire — pasa ambos juntos o ninguno.
Chat y el método de due-diligence
El chat de investigación de mercado del dashboard planea y llama estas mismas operaciones directamente (sin una API separada de cara al modelo) y puede correr un método de due-diligence screening que componesearch_risks y search_permits:
- Resolver identidad por RFC primero — de un resultado previo de
search_suppliers/search_awardso dado directamente — y llamarsearch_risks { rfc }con él. No usar ese RFC de awards como puente hacia permits: las fuentes actuales de permits no publican RFC del holder. Llamarsearch_permits { holder_name }sólo después de obtener un nombre publicado y verificado; la resolución por nombre es aproximada y debe declarar la advertencia del match.holder_rfcqueda como filtro fuerte si una fuente futura lo publica. - Nunca concluir “sin marcas de riesgo” o “no autorizado” solo a partir de una página vacía — se lee
coverageprimero y se declara la búsqueda contra él. - Un permit y una marca de riesgo responden preguntas distintas y se reportan por separado — nunca se fusionan en un veredicto de “limpio” o “autorizado”. Una marca de riesgo es un listado publicado, no una condena; un permit es un derecho otorgado grabado al momento de la carga, no un estado operacional vigente.
Relacionado
- MCP y OAuth - conectar un cliente y la nota sobre el caching de schemas.
- Errores - el envelope de error de toda la plataforma y el catálogo de códigos.
- Seguridad - autenticación y API keys.
