Resolve IP addresses to mobile advertising IDs using Rosetta Stone for cross-device targeting and mobile campaign activation
A company has server logs or web analytics data containing IP addresses, but needs mobile advertising IDs (MAIDs) to activate audiences on mobile channels. By joining their IP dataset against Rosetta Stone, they can resolve IPs to MAIDs observed on the same network, rank them by observation frequency, and build targetable mobile audiences—all without querying individual data providers.
Upload your IP address file to Narrative. This dataset serves as the seed list you want to enrich with MAIDs.Your dataset should include at minimum:
Column
Type
Description
ip
string
IPv4 or IPv6 address
observed_at
timestamp
When the IP was observed in your system
You may also include additional context columns (session ID, page URL, etc.) that you want to carry through to the enriched output.
Upload your dataset through the Narrative UI or SDK. The ip column does not need a Rosetta Stone mapping—you will join it directly against the "ip_address" attribute in Rosetta Stone.
Before running the full enrichment, query Rosetta Stone to understand how much MAID coverage is available by identifier type:
Copy
Ask AI
SELECT rs."mobile_id_unique_identifier"."type" AS maid_type, APPROX_COUNT_DISTINCT(rs."ip_address") AS unique_ips, APPROX_COUNT_DISTINCT(rs."mobile_id_unique_identifier"."value") AS unique_maidsFROM narrative.rosetta_stone rsWHERE rs."mobile_id_unique_identifier"."value" IS NOT NULL AND rs."ip_address" IS NOT NULL AND rs."event_timestamp" >= CURRENT_TIMESTAMP - INTERVAL '45' DAYGROUP BY rs."mobile_id_unique_identifier"."type"
This shows you the relative volume of IDFA vs GAID data and helps set expectations for match rates.
GAID (Android) typically has higher volume than IDFA (iOS) due to Apple’s App Tracking Transparency framework. See Mobile Ad IDs for details on availability trends.
Join your IP dataset against Rosetta Stone to find MAIDs observed on the same IP addresses. This is the core enrichment query:
Copy
Ask AI
SELECT cd."ip", rs."mobile_id_unique_identifier"."value" AS maid, rs."mobile_id_unique_identifier"."type" AS maid_type, rs."iso_3166_1_country" AS country_code, rs."event_timestamp" AS observed_atFROM narrative.rosetta_stone rsINNER JOIN company_data."your_ip_dataset" cd ON rs."ip_address" = cd."ip"WHERE rs."mobile_id_unique_identifier"."value" IS NOT NULL AND rs."ip_address" IS NOT NULL AND rs."event_timestamp" >= CURRENT_TIMESTAMP - INTERVAL '45' DAY
Key patterns used:
Pattern
Purpose
INNER JOIN company_data
Matches only IPs present in your seed dataset
mobile_id_unique_identifier.value / .type
Extracts MAID value and type (IDFA or GAID)
ip_address
Rosetta Stone’s normalized IP address attribute
INTERVAL '45' DAY
Limits to recent observations for freshness
A single IP address may resolve to many MAIDs—especially shared or commercial IPs. Step 4 shows how to rank and limit results to the most reliable matches.
A single IP can map to many devices, and not all associations are equally reliable. Use QUALIFY with ROW_NUMBER and a nested APPROX_COUNT_DISTINCT window function to keep only the top N most-frequently-observed MAIDs per IP:
Copy
Ask AI
SELECT cd."ip", rs."mobile_id_unique_identifier"."value" AS maid, rs."mobile_id_unique_identifier"."type" AS maid_type, rs."iso_3166_1_country" AS country_codeFROM narrative.rosetta_stone rsINNER JOIN company_data."your_ip_dataset" cd ON rs."ip_address" = cd."ip"WHERE rs."mobile_id_unique_identifier"."value" IS NOT NULL AND rs."ip_address" IS NOT NULL AND rs."event_timestamp" >= CURRENT_TIMESTAMP - INTERVAL '45' DAYQUALIFY ROW_NUMBER() OVER ( PARTITION BY rs."iso_3166_1_country", cd."ip" ORDER BY APPROX_COUNT_DISTINCT(rs."event_timestamp") OVER ( PARTITION BY rs."iso_3166_1_country", cd."ip", rs."mobile_id_unique_identifier"."value", rs."mobile_id_unique_identifier"."type" ) DESC) <= 3
How the nested window functions work:
The inner window (APPROX_COUNT_DISTINCT ... OVER (PARTITION BY country, ip, maid, maid_type)) counts distinct observation timestamps for each IP-MAID pair. MAIDs seen more often on an IP address get a higher count.
The outer window (ROW_NUMBER ... OVER (PARTITION BY country, ip ORDER BY ... DESC)) ranks MAIDs within each country + IP group by that observation count, highest first.
QUALIFY ... <= 3 keeps only the top 3 most-frequently-observed MAIDs per IP per country.
This approach prioritizes devices that consistently appear on an IP address, filtering out transient connections.
Adjust the <= 3 threshold based on your use case. For residential IPs, 1-3 MAIDs is typical. For commercial or CGNAT IPs shared across many users, consider filtering these out entirely or increasing the threshold.
Step 5: Add country filtering and enrichment stats
Extend the query with geographic filtering and a maid_count column that shows the total unique MAIDs found per IP before ranking. This helps you assess match quality:
Copy
Ask AI
SELECT cd."ip", rs."mobile_id_unique_identifier"."value" AS maid, rs."mobile_id_unique_identifier"."type" AS maid_type, rs."iso_3166_1_country" AS country_code, APPROX_COUNT_DISTINCT(rs."mobile_id_unique_identifier"."value") OVER ( PARTITION BY cd."ip" ) AS maid_countFROM narrative.rosetta_stone rsINNER JOIN company_data."your_ip_dataset" cd ON rs."ip_address" = cd."ip"WHERE rs."mobile_id_unique_identifier"."value" IS NOT NULL AND rs."ip_address" IS NOT NULL AND rs."event_timestamp" >= CURRENT_TIMESTAMP - INTERVAL '45' DAY AND rs."iso_3166_1_country" IN ('US', 'CA', 'GB', 'AU')QUALIFY ROW_NUMBER() OVER ( PARTITION BY rs."iso_3166_1_country", cd."ip" ORDER BY APPROX_COUNT_DISTINCT(rs."event_timestamp") OVER ( PARTITION BY rs."iso_3166_1_country", cd."ip", rs."mobile_id_unique_identifier"."value", rs."mobile_id_unique_identifier"."type" ) DESC) <= 3
The maid_count column reveals IP quality:
Low count (1-5): Likely residential IPs with a small number of devices—high-confidence matches
High count (50+): Likely commercial, VPN, or CGNAT IPs—consider excluding these from activation
To see the split between IDFA and GAID per IP, replace the maid_count window with a type-specific count:
Copy
Ask AI
SELECT cd."ip", rs."mobile_id_unique_identifier"."value" AS maid, rs."mobile_id_unique_identifier"."type" AS maid_type, rs."iso_3166_1_country" AS country_code, APPROX_COUNT_DISTINCT( CASE WHEN rs."mobile_id_unique_identifier"."type" = 'idfa' THEN rs."mobile_id_unique_identifier"."value" END ) OVER (PARTITION BY cd."ip") AS idfa_count, APPROX_COUNT_DISTINCT( CASE WHEN rs."mobile_id_unique_identifier"."type" = 'gaid' THEN rs."mobile_id_unique_identifier"."value" END ) OVER (PARTITION BY cd."ip") AS gaid_countFROM narrative.rosetta_stone rsINNER JOIN company_data."your_ip_dataset" cd ON rs."ip_address" = cd."ip"WHERE rs."mobile_id_unique_identifier"."value" IS NOT NULL AND rs."ip_address" IS NOT NULL AND rs."event_timestamp" >= CURRENT_TIMESTAMP - INTERVAL '45' DAY AND rs."iso_3166_1_country" IN ('US', 'CA', 'GB', 'AU')QUALIFY ROW_NUMBER() OVER ( PARTITION BY rs."iso_3166_1_country", cd."ip" ORDER BY APPROX_COUNT_DISTINCT(rs."event_timestamp") OVER ( PARTITION BY rs."iso_3166_1_country", cd."ip", rs."mobile_id_unique_identifier"."value", rs."mobile_id_unique_identifier"."type" ) DESC) <= 3
Wrap the final query in a materialized view that refreshes automatically:
Copy
Ask AI
CREATE MATERIALIZED VIEW "ip_to_maid_enriched"REFRESH_SCHEDULE = '@weekly'DISPLAY_NAME = 'IP to MAID Enrichment'DESCRIPTION = 'IP addresses enriched with top mobile ad IDs ranked by observation frequency'TAGS = ('enrichment', 'ip', 'maid', 'cross-device')WRITE_MODE = 'overwrite'ASSELECT cd."ip", rs."mobile_id_unique_identifier"."value" AS maid, rs."mobile_id_unique_identifier"."type" AS maid_type, rs."iso_3166_1_country" AS country_code, APPROX_COUNT_DISTINCT(rs."mobile_id_unique_identifier"."value") OVER ( PARTITION BY cd."ip" ) AS maid_countFROM narrative.rosetta_stone rsINNER JOIN company_data."your_ip_dataset" cd ON rs."ip_address" = cd."ip"WHERE rs."mobile_id_unique_identifier"."value" IS NOT NULL AND rs."ip_address" IS NOT NULL AND rs."event_timestamp" >= CURRENT_TIMESTAMP - INTERVAL '45' DAY AND rs."iso_3166_1_country" IN ('US', 'CA', 'GB', 'AU')QUALIFY ROW_NUMBER() OVER ( PARTITION BY rs."iso_3166_1_country", cd."ip" ORDER BY APPROX_COUNT_DISTINCT(rs."event_timestamp") OVER ( PARTITION BY rs."iso_3166_1_country", cd."ip", rs."mobile_id_unique_identifier"."value", rs."mobile_id_unique_identifier"."type" ) DESC) <= 3
Adjust the REFRESH_SCHEDULE based on how frequently your IP dataset changes. @weekly works well for server logs that update regularly. Use @monthly if your IP list is relatively static.
MAID availability: iOS App Tracking Transparency and Android privacy changes have reduced MAID availability. GAID volume is significantly higher than IDFA. Consider supplementing with hashed email or other identifier types for broader iOS coverage. See Mobile Ad IDs for details.
IP addresses as personal data: Under GDPR, IP addresses are considered personal data. Ensure your enrichment workflow complies with applicable privacy regulations, particularly when processing IPs from EU users. See GDPR compliance and CCPA compliance for details.