Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/(site)/art/site--spot-launch-directories-cover.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
<body>
<div class="frame">
<div class="copy">
<img alt="stack127" class="wordmark" src="../../../public/logos/stack127-wordmark-on-dark.png">
<img alt="stack127" class="wordmark" src="../../../public/logos/stack127-wordmark-on-dark.svg">
<div style="background: transparent">
<h1>Find the right place to <mark>launch your product</mark></h1>
<p class="lede">Compare cost, review time, link type, and who can submit.</p>
Expand Down
12 changes: 6 additions & 6 deletions app/ui/site/brand-wordmark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ export function BrandWordmark({ className }: BrandWordmarkProps) {
className={`${styles.image} ${styles.onDark}`}
decoding="async"
fetchPriority="high"
height="282"
height="790"
loading="eager"
src="/logos/stack127-wordmark-on-dark.png"
width="1451"
src="/logos/stack127-wordmark-on-dark.svg"
width="3438"
/>
<img
alt=""
className={`${styles.image} ${styles.onLight}`}
decoding="async"
height="282"
height="790"
loading="lazy"
src="/logos/stack127-wordmark-on-light.png"
width="1451"
src="/logos/stack127-wordmark-on-light.svg"
width="3438"
/>
</span>
);
Expand Down
11 changes: 10 additions & 1 deletion lib/data-api/ad-networks/core.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { brandKit } from '../brand.ts';
import { createHash } from 'node:crypto';
import { domainToASCII } from 'node:url';
import { adNetworkFacts } from '../../../app/(site)/(directories)/ad-networks/network-facts.ts';
Expand Down Expand Up @@ -133,9 +134,10 @@ export function boundResults(data: Record<string, any>,cap: number) {
sets.forEach((set,i)=>{if(set.parent[set.key].length>counts[i]){set.parent[set.key]=set.parent[set.key].slice(0,counts[i]);set.parent.truncated=true;truncatedSets.push(set.path);}});
return {row_count:cap-left,truncated_sets:truncatedSets};
}
export type DataAnswer = { as_of: unknown; queried_at: string; definitions: typeof definitions; caveats: string[]; filters: Record<string, any>; url: string; data: Record<string, any>; limits: { max_rows: number; row_count:number; truncated: boolean; truncated_sets:string[] } };
export type DataAnswer = { as_of: unknown; queried_at: string; definitions: Record<string, string>; caveats: string[]; filters: Record<string, any>; url: string; data: Record<string, any>; limits: { max_rows: number; row_count:number; truncated: boolean; truncated_sets:string[] } };
export async function queryTool(db: Reader,name: ToolName,f: Record<string, any>,cap: number): Promise<Record<string, any>> {
switch(name) {
case 'get_brand_kit': return brandKit;
case 'ad_networks_overview': {
const stats=(await db.query('select * from dataset_summary'))[0];
const totals=(await db.query(`select count(distinct publisher_id)::int as listed,count(distinct publisher_id) filter(where relationship_type='primary_or_exclusive_manager')::int as managed from ad_network_publisher_relationships r where ${listed}`))[0];
Expand Down Expand Up @@ -198,6 +200,13 @@ export async function executeTool(name: ToolName,input: unknown,transaction?: ty
if (!toolNames.includes(name)) throw new ApiError(404,'not_found','Unknown tool.');
if (name==='run_readonly_sql' && !sqlEnabled()) throw new ApiError(404,'not_found','Not found.');
const f=validateInput(name,input),cap=rowLimit();
if(name==='get_brand_kit') return {
as_of:null,queried_at:new Date().toISOString(),
definitions:{as_of:'Brand assets have no dataset freshness date. Use as_of from the data answer in the source line.'},
caveats:['Use the data tool answer for its citation URL, as_of date, definitions and caveats.'],
filters:f,url:sourceUrl,data:brandKit,
limits:{max_rows:cap,row_count:0,truncated:false,truncated_sets:[]},
};
return (transaction ?? (name==='run_readonly_sql' ? sqlReadTransaction : readTransaction))(async db=>{
const freshness=(await db.query('select * from dataset_summary'))[0];
const data=await queryTool(db,name,f,cap);
Expand Down
1 change: 1 addition & 0 deletions lib/data-api/ad-networks/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const filtersShape = {
status: z.enum(['confirmed','stale']).optional().describe('Further restrict listed status. Ended relationships are available only in lookup_site and movements.'),
};
export const toolSchemas = {
get_brand_kit: z.object({}).strict(),
ad_networks_overview: z.object({}).strict(),
list_ad_networks: z.object({
max_minimum_traffic: z.number().int().nonnegative().optional().describe('Maximum published monthly minimum. Unknown minimums are excluded; units differ, use traffic_unit. Example: 50000.'),
Expand Down
91 changes: 91 additions & 0 deletions lib/data-api/brand-assets.generated.ts

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions lib/data-api/brand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { brandAssets } from './brand-assets.generated.ts';

export const brandKit = {
name: 'stack127',
credit: 'Ad network data from stack127',
source_line: 'Source: stack127.com/ad-networks · data as of <as_of date>',
url: 'https://stack127.com/ad-networks',
default_theme: 'dark',
...brandAssets,
chart_rules: [
'Highlight the main series with the accent; use neutral steps for other series.',
'Use the lightest neutral step for unknown or missing values.',
'Use no gradients, shadows or 3D.',
'Label units and the data answer\'s as_of date. Use its more specific page URL in the source line when available.',
'Place a small logo in a corner. Never stretch or recolor it; keep the #bafc3c dot.',
],
tone: 'Plain, direct, no hype.',
};
3 changes: 2 additions & 1 deletion lib/data-api/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { toolNames, toolSchemas } from './ad-networks/schemas.ts';
import type { ToolName } from './ad-networks/schemas.ts';
import { sqlSchema } from './ad-networks/sql.ts';
const descriptions: Record<ToolName,string> = {
get_brand_kit:'For charts, images, tables and shared visuals: call once per visual task for stack127 colors, fonts, inline SVG logos and credit rules. Not needed for data questions. Input {}.',
ad_networks_overview:'Start here before researching stack127 ad network data. Returns coverage totals, freshness, all definitions/caveats, allowed filters and a tool guide. Counts observed sites, not the whole market.',
list_ad_networks:'Use to choose networks by joining rules or compare listed/managed counts and payment terms. Returns sourced public facts with checkedAt, median RDAP domain age and measurement bases. Traffic units and alternative joining rules are not comparable without reading the facts.',
get_ad_network:'Use for a full profile of one network slug. Returns sourced joining/payment facts, confirmed/stale/listed/managed counts, age/year/niche/size/platform distributions, co-listed networks and bounded newest-first daily snapshots. Missing days are unknown; snapshots are listed counts.',
Expand All @@ -34,7 +35,7 @@ export async function handleMcp(request: Request,secret?: string): Promise<Respo
// MCP batching is not accepted: each HTTP request has one rate-limit unit.
if(!body||Array.isArray(body)||typeof body!=='object')throw new ApiError(400,'invalid_request','One JSON-RPC request per HTTP request.');
server=new McpServer({name:'stack127-private-ad-networks',version:'1.0.0'},
{instructions:'Start with ad_networks_overview. Cite the returned stack127 URL. Carry definitions, as_of, filters and caveats into research. Listed sites may overlap between networks. Movements are not reliable growth/churn. All tools read data; SQL is private and optional.'});
{instructions:'Start with ad_networks_overview. Cite the returned stack127 URL. Carry definitions, as_of, filters and caveats into research. Listed sites may overlap between networks. Movements are not reliable growth/churn. All tools read data; SQL is private and optional. When making a chart, image or table for sharing, call get_brand_kit first. Title the visual by what it shows and add "Source: stack127.com/ad-networks · data as of <as_of date>", using the data answer’s as_of date and its more specific page URL when available. Use the brand colors and dark theme by default.'});
for(const name of toolNames.filter(n=>n!=='run_readonly_sql'||sqlEnabled())) {
server.registerTool(name,{
title:name.split('_').map(w=>w[0].toUpperCase()+w.slice(1)).join(' '),description:descriptions[name],
Expand Down
12 changes: 12 additions & 0 deletions public/brand/originals/stack127-wordmark-hybrid-on-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading