ThreatMetrix (TMX) is one of the most sophisticated anti-bot and fraud detection systems used by major e-commerce platforms, financial institutions, and ticketing sites. Building a TMX solver requires understanding how the system collects device fingerprints, generates session IDs, and communicates with backend servers.
A TMX solver works by replicating the legitimate profiling behavior that ThreatMetrix expects from real browsers. It generates valid session payloads that pass the risk assessment without triggering fraud detection flags. This guide covers the complete architecture, from deobfuscating TMX scripts to building a production-ready solver.
What is ThreatMetrix and How Does It Work?
ThreatMetrix is owned by LexisNexis Risk Solutions. It processes over 35 billion annual transactions across thousands of websites globally.
The system creates persistent device identifiers called SmartIDs. These identifiers survive cookie deletion, private browsing modes, and even some hardware changes.
When you visit a website protected by TMX, JavaScript tags execute in your browser. These tags collect over 250 device attributes and send them to ThreatMetrix servers.
The backend then performs a risk assessment. It compares your device profile against their global fraud network database and returns a review status: Pass, Review, Challenge, or Reject.
The Three-Phase TMX Flow
Phase 1 - Profiling: JavaScript tags collect device attributes and browser fingerprints.
Phase 2 - Session Query: The website's backend sends collected data to TMX's API with a unique session ID.
Phase 3 - Risk Assessment: TMX returns a score and recommendation based on the device's history and current attributes.
Your solver needs to replicate Phase 1 convincingly. The goal is generating payloads that produce a "Pass" status when the backend performs Phase 2.
Understanding the TMX Detection Mechanisms
ThreatMetrix employs four primary detection layers. Understanding each one is essential for building an effective solver.
SmartID Device Fingerprinting
SmartID creates persistent device identifiers by combining multiple browser attributes. It doesn't rely on cookies or local storage alone.
The system collects canvas fingerprinting data through both 2D and 3D rendering operations. It also captures WebGL parameters and GPU-specific rendering patterns.
AudioContext fingerprinting analyzes signal processing variations unique to each device's audio hardware. Font enumeration and installed plugin detection round out the fingerprint.
IP and Network Intelligence
TMX maintains a massive database of IP addresses associated with fraud. Residential IPs score differently than datacenter IPs.
The system detects VPNs, proxies, and Tor exit nodes through multiple methods. It compares the browser's detected IP against the server-visible IP to identify masking attempts.
When building your solver, consider using residential proxies from providers like Roundproxies.com to maintain clean IP reputation.
Behavioral Analysis
TMX tracks mouse movements, scroll patterns, and keyboard timing. Automated tools often exhibit inhuman precision or consistency.
The system also monitors page interaction sequences. Real users don't immediately submit forms or click buttons the instant a page loads.
Browser Integrity Checks
TMX validates that browser properties are internally consistent. A Chrome browser shouldn't report Firefox-specific navigator properties.
The system checks for automation signatures like the webdriver property set by Selenium. It also looks for missing browser APIs or anomalous property values.
Step 1: Analyze the ThreatMetrix JavaScript
Before building a solver, you need to understand what the TMX script collects. The JavaScript is heavily obfuscated, so deobfuscation comes first.
Locating the TMX Script
TMX scripts load from domains like h.online-metrix.net or customer-specific subdomains. Look for script tags containing profiling or tags.js in the URL.
The Org ID and session ID typically appear in the script URL or within inline JavaScript. These values are essential for your solver.
// Example TMX tag structure you might find
var tmx_session_id = "abc123-unique-session-id";
var tmx_org_id = "customer_org_id";
The Org ID identifies the customer's ThreatMetrix account. The session ID links the browser profiling to the backend API query.
Deobfuscating the TMX Script
TMX uses control flow flattening, string encryption, and dead code injection to obscure their logic. You'll need AST-based tools to unravel it.
Create a basic deobfuscator using JavaScript parsing libraries. The following approach works for initial analysis:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
function deobfuscate(code) {
const ast = parser.parse(code);
// Evaluate constant expressions
traverse(ast, {
BinaryExpression(path) {
const { left, right, operator } = path.node;
if (t.isNumericLiteral(left) && t.isNumericLiteral(right)) {
const result = eval(`${left.value} ${operator} ${right.value}`);
path.replaceWith(t.numericLiteral(result));
}
}
});
return generate(ast).code;
}
This snippet evaluates simple binary expressions. A complete deobfuscator needs handlers for string concatenation, array element lookups, and function inlining.
Extracting Collection Logic
After deobfuscation, identify the functions that collect fingerprint data. Look for patterns accessing navigator, screen, and document properties.
The critical collection points include canvas rendering, WebGL context creation, and audio context initialization. Map each collection function to understand the expected output format.
// Typical fingerprint collection patterns to look for:
// Canvas fingerprinting
function getCanvasFingerprint() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillText('TMX fingerprint test', 2, 2);
return canvas.toDataURL();
}
// WebGL fingerprinting
function getWebGLInfo() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
return {
vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
};
}
Document each property the script accesses. Your solver must generate consistent values for all of them.
Step 2: Build the Fingerprint Harvester
A fingerprint harvester collects real device data to populate your solver's database. This gives you authentic fingerprint combinations.
Setting Up the Harvester Architecture
The harvester runs in actual browsers to capture genuine fingerprints. Use headless browser automation with stealth plugins.
Store harvested fingerprints in MongoDB or PostgreSQL. Each fingerprint record should include all TMX-relevant properties plus metadata about the device.
from pymongo import MongoClient
from datetime import datetime
class FingerprintHarvester:
def __init__(self, mongo_uri):
self.client = MongoClient(mongo_uri)
self.db = self.client['tmx_solver']
self.fingerprints = self.db['fingerprints']
def store_fingerprint(self, fingerprint_data):
record = {
'timestamp': datetime.utcnow(),
'canvas_hash': fingerprint_data['canvas'],
'webgl_vendor': fingerprint_data['webgl_vendor'],
'webgl_renderer': fingerprint_data['webgl_renderer'],
'screen_resolution': fingerprint_data['screen'],
'timezone_offset': fingerprint_data['timezone'],
'language': fingerprint_data['language'],
'platform': fingerprint_data['platform'],
'user_agent': fingerprint_data['user_agent'],
'plugins': fingerprint_data['plugins'],
'fonts': fingerprint_data['fonts']
}
return self.fingerprints.insert_one(record)
This Python class handles fingerprint storage. The structure mirrors what TMX collects from browsers.
Browser Automation for Harvesting
Configure Playwright or Puppeteer with stealth patches. The browser should appear as a normal user device.
const { chromium } = require('playwright-extra');
const stealth = require('puppeteer-extra-plugin-stealth');
chromium.use(stealth());
async function harvestFingerprint() {
const browser = await chromium.launch({
headless: false, // Some fingerprints differ in headless mode
args: ['--disable-blink-features=AutomationControlled']
});
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
locale: 'en-US',
timezoneId: 'America/New_York'
});
const page = await context.newPage();
// Inject fingerprint collection script
const fingerprint = await page.evaluate(() => {
return {
canvas: getCanvasFingerprint(),
webgl: getWebGLInfo(),
screen: `${screen.width}x${screen.height}`,
timezone: new Date().getTimezoneOffset(),
language: navigator.language,
platform: navigator.platform,
userAgent: navigator.userAgent
};
});
await browser.close();
return fingerprint;
}
Run this harvester on different machines and browsers. Diversity in your fingerprint database improves solver success rates.
Validating Harvested Data
Not all fingerprint combinations are valid. TMX knows which attribute combinations occur in real browsers.
A Windows machine won't have iOS-specific properties. A Chrome browser won't report Firefox plugin arrays. Cross-validate your harvested data for consistency.
def validate_fingerprint(fp):
# Platform/UA consistency check
if 'Windows' in fp['platform'] and 'iPhone' in fp['user_agent']:
return False
# Screen resolution sanity check
width, height = map(int, fp['screen_resolution'].split('x'))
if width < 320 or height < 240:
return False
# WebGL renderer should match platform
if 'Windows' in fp['platform'] and 'Apple' in fp['webgl_renderer']:
return False
return True
Filter invalid combinations before using them in your solver. TMX flags inconsistent fingerprints as high-risk.
Step 3: Implement the Session ID Generator
The session ID links browser profiling to backend queries. It must be unique per request and follow TMX's expected format.
Session ID Format Requirements
TMX session IDs can be up to 128 bytes. They only accept alphanumeric characters, underscores, and hyphens.
Never reuse session IDs across different profiling attempts. TMX tracks session ID patterns and flags repeated usage.
import uuid
import hashlib
import time
class SessionIDGenerator:
def __init__(self, prefix='tmx'):
self.prefix = prefix
def generate(self):
# Combine timestamp and random UUID
timestamp = str(int(time.time() * 1000))
random_part = uuid.uuid4().hex
# Create hash-based session ID
combined = f"{timestamp}{random_part}"
session_hash = hashlib.sha256(combined.encode()).hexdigest()[:32]
return f"{self.prefix}_{session_hash}"
def generate_with_entropy(self, additional_data=''):
# Add extra entropy from request context
entropy = f"{time.time()}{uuid.uuid4()}{additional_data}"
return hashlib.sha256(entropy.encode()).hexdigest()[:64]
The generator produces unique session IDs for each solve attempt. The hash-based approach ensures unpredictability.
Integrating Session IDs with Org IDs
Each target website has its own Org ID. Your solver must pair the correct Org ID with generated session IDs.
SITE_CONFIGS = {
'example_retailer': {
'org_id': 'abc123org',
'fp_server': 'h.online-metrix.net',
'profiling_domain': 'example.com'
},
'ticketing_site': {
'org_id': 'def456org',
'fp_server': 'custom.threatmetrix.net',
'profiling_domain': 'tickets.example.com'
}
}
def get_session_config(site_name):
config = SITE_CONFIGS.get(site_name)
if not config:
raise ValueError(f"Unknown site: {site_name}")
return {
**config,
'session_id': SessionIDGenerator().generate()
}
Store site configurations in a database for easy updates. TMX configurations change periodically.
Step 4: Create the Payload Encoder
TMX payloads use custom encoding schemes. Your solver must encode fingerprint data exactly as the real script does.
Understanding the Payload Structure
The payload consists of key-value pairs encoded in a specific format. Keys are typically single characters or short codes.
Values undergo base64 encoding with additional transformations. Some implementations use custom character substitution.
import base64
import json
class TMXPayloadEncoder:
def __init__(self):
self.key_map = {
'session_id': 'a',
'org_id': 'b',
'canvas_hash': 'c',
'webgl_vendor': 'd',
'webgl_renderer': 'e',
'screen_res': 'f',
'timezone': 'g',
'language': 'h',
'platform': 'i',
'user_agent': 'j'
}
def encode_value(self, value):
if isinstance(value, (dict, list)):
value = json.dumps(value)
encoded = base64.b64encode(str(value).encode()).decode()
return encoded
def build_payload(self, fingerprint_data):
payload_parts = []
for key, code in self.key_map.items():
if key in fingerprint_data:
encoded_value = self.encode_value(fingerprint_data[key])
payload_parts.append(f"{code}={encoded_value}")
return '&'.join(payload_parts)
This encoder maps fingerprint properties to short codes. The actual TMX encoding is more complex and varies by implementation.
Handling Encryption Layers
Some TMX implementations encrypt payloads before transmission. You'll need to reverse-engineer the encryption scheme.
Common approaches include XOR with dynamic keys or AES encryption. The key often derives from other page elements.
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import os
class PayloadEncryptor:
def __init__(self, key_source):
self.key = self._derive_key(key_source)
def _derive_key(self, source):
# Key derivation from page elements
import hashlib
return hashlib.sha256(source.encode()).digest()
def encrypt_payload(self, payload):
iv = os.urandom(16)
cipher = Cipher(
algorithms.AES(self.key),
modes.CBC(iv),
backend=default_backend()
)
encryptor = cipher.encryptor()
# Pad payload to block size
padding_length = 16 - (len(payload) % 16)
padded_payload = payload + chr(padding_length) * padding_length
encrypted = encryptor.update(padded_payload.encode()) + encryptor.finalize()
return base64.b64encode(iv + encrypted).decode()
Extract the key derivation logic from the deobfuscated TMX script. Keys may change with each page load.
Generating the Final Request
Combine encoded fingerprints with session data to create the complete profiling request.
import requests
class TMXProfiler:
def __init__(self, config):
self.org_id = config['org_id']
self.fp_server = config['fp_server']
self.session_id = config['session_id']
self.encoder = TMXPayloadEncoder()
def profile(self, fingerprint):
payload_data = {
'session_id': self.session_id,
'org_id': self.org_id,
**fingerprint
}
encoded_payload = self.encoder.build_payload(payload_data)
url = f"https://{self.fp_server}/fp/tags"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': fingerprint['user_agent'],
'Origin': f"https://{fingerprint.get('origin', 'example.com')}"
}
response = requests.post(url, data=encoded_payload, headers=headers)
return response.status_code == 200
The profiler sends fingerprint data to TMX servers. Success doesn't guarantee the backend query will pass—that depends on fingerprint quality.
Step 5: Build the Solver API
Wrap your solver in an API for easy integration. Support multiple delivery methods based on client needs.
API Architecture Options
Single Request Mode: Client sends one request, waits for the solved session ID.
Polling Mode: Client sends initial request, receives a task ID, polls until completion.
WebSocket Mode: Real-time bidirectional communication for fastest response times.
from flask import Flask, request, jsonify
from threading import Thread
import queue
app = Flask(__name__)
task_queue = queue.Queue()
results = {}
@app.route('/solve', methods=['POST'])
def solve_single():
data = request.json
site = data.get('site')
if not site:
return jsonify({'error': 'Site required'}), 400
# Get config and solve synchronously
config = get_session_config(site)
fingerprint = get_random_fingerprint()
profiler = TMXProfiler(config)
success = profiler.profile(fingerprint)
if success:
return jsonify({
'session_id': config['session_id'],
'success': True
})
return jsonify({'success': False, 'error': 'Profiling failed'}), 500
@app.route('/solve/async', methods=['POST'])
def solve_async():
data = request.json
task_id = str(uuid.uuid4())
# Queue the task
task_queue.put({
'id': task_id,
'site': data.get('site'),
'callback': data.get('callback')
})
return jsonify({'task_id': task_id, 'status': 'queued'})
@app.route('/status/<task_id>', methods=['GET'])
def check_status(task_id):
if task_id in results:
return jsonify(results[task_id])
return jsonify({'status': 'pending'})
This Flask API provides both synchronous and asynchronous solving. Choose based on your latency requirements.
Implementing WebSocket Support
WebSocket connections enable real-time solve notifications. This eliminates polling overhead.
from flask_socketio import SocketIO, emit
socketio = SocketIO(app, cors_allowed_origins="*")
@socketio.on('solve')
def handle_solve(data):
site = data.get('site')
client_id = request.sid
def solve_and_notify():
config = get_session_config(site)
fingerprint = get_random_fingerprint()
profiler = TMXProfiler(config)
success = profiler.profile(fingerprint)
result = {
'success': success,
'session_id': config['session_id'] if success else None
}
socketio.emit('solve_result', result, room=client_id)
Thread(target=solve_and_notify).start()
emit('solve_started', {'message': 'Processing'})
WebSocket mode is ideal for high-volume clients who need immediate results.
API Authentication and Rate Limiting
Protect your solver API with key-based authentication and usage limits.
from functools import wraps
from flask import g
import time
API_KEYS = {
'key_abc123': {'uses': -1, 'expires': None}, # Unlimited
'key_def456': {'uses': 1000, 'expires': 1735689600} # Limited
}
def require_api_key(f):
@wraps(f)
def decorated(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if not api_key or api_key not in API_KEYS:
return jsonify({'error': 'Invalid API key'}), 401
key_data = API_KEYS[api_key]
# Check expiration
if key_data['expires'] and time.time() > key_data['expires']:
return jsonify({'error': 'API key expired'}), 401
# Check usage limit
if key_data['uses'] != -1 and key_data['uses'] <= 0:
return jsonify({'error': 'Usage limit exceeded'}), 429
# Decrement uses
if key_data['uses'] != -1:
API_KEYS[api_key]['uses'] -= 1
g.api_key = api_key
return f(*args, **kwargs)
return decorated
Apply the decorator to protected endpoints. Track usage for billing and abuse prevention.
Step 6: Handle Dynamic Keys and URLs
TMX implementations frequently change keys, endpoints, and encoding schemes. Your solver needs automatic adaptation.
Extracting Keys from Live Sites
Monitor target sites for configuration changes. Automated extraction keeps your solver current.
import re
import requests
class TMXConfigExtractor:
def __init__(self):
self.key_patterns = [
r'org_id["\']?\s*[:=]\s*["\']([^"\']+)',
r'fp_server["\']?\s*[:=]\s*["\']([^"\']+)',
r'session_id["\']?\s*[:=]\s*["\']([^"\']+)'
]
def extract_from_page(self, url):
response = requests.get(url, headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0'
})
html = response.text
config = {}
for pattern in self.key_patterns:
match = re.search(pattern, html, re.IGNORECASE)
if match:
key_name = pattern.split('[')[0]
config[key_name] = match.group(1)
# Also extract from inline scripts
scripts = re.findall(r'<script[^>]*>(.*?)</script>', html, re.DOTALL)
for script in scripts:
for pattern in self.key_patterns:
match = re.search(pattern, script, re.IGNORECASE)
if match:
key_name = pattern.split('[')[0]
config[key_name] = match.group(1)
return config
Run extraction periodically or trigger it when solve failures increase. Fresh configurations improve success rates.
Using AST Parsing for Complex Extractions
Regex alone can't handle heavily obfuscated configurations. AST parsing provides more reliable extraction.
import subprocess
import json
def extract_with_ast(script_content):
# Use Node.js for JavaScript AST parsing
node_script = '''
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const ast = parser.parse(process.argv[2]);
const configs = [];
traverse(ast, {
AssignmentExpression(path) {
const left = path.node.left;
const right = path.node.right;
if (left.type === 'MemberExpression') {
const propName = left.property.name || left.property.value;
if (['org_id', 'session_id', 'fp_server'].includes(propName)) {
configs.push({
key: propName,
value: right.value
});
}
}
}
});
console.log(JSON.stringify(configs));
'''
result = subprocess.run(
['node', '-e', node_script, script_content],
capture_output=True,
text=True
)
return json.loads(result.stdout)
AST parsing finds configurations even when variable names are mangled or values are computed dynamically.
Caching and Invalidation Strategy
Cache extracted configurations but implement smart invalidation. Stale configs cause solve failures.
from datetime import datetime, timedelta
class ConfigCache:
def __init__(self, ttl_minutes=30):
self.cache = {}
self.ttl = timedelta(minutes=ttl_minutes)
def get(self, site):
if site not in self.cache:
return None
entry = self.cache[site]
if datetime.utcnow() > entry['expires']:
del self.cache[site]
return None
return entry['config']
def set(self, site, config):
self.cache[site] = {
'config': config,
'expires': datetime.utcnow() + self.ttl
}
def invalidate(self, site):
if site in self.cache:
del self.cache[site]
def invalidate_all(self):
self.cache = {}
When solve failures spike, invalidate the cache and re-extract configurations.
Step 7: Implement Proxy Rotation
IP reputation significantly impacts TMX risk scores. Rotate through clean residential IPs for best results.
Proxy Pool Management
Maintain a pool of tested proxies. Remove proxies that get flagged or show poor performance.
import random
from collections import defaultdict
class ProxyPool:
def __init__(self):
self.proxies = []
self.proxy_stats = defaultdict(lambda: {'success': 0, 'fail': 0})
self.blocked_proxies = set()
def add_proxy(self, proxy_url):
if proxy_url not in self.blocked_proxies:
self.proxies.append(proxy_url)
def get_proxy(self):
available = [p for p in self.proxies if p not in self.blocked_proxies]
if not available:
raise Exception("No available proxies")
# Weight by success rate
weighted = []
for proxy in available:
stats = self.proxy_stats[proxy]
total = stats['success'] + stats['fail']
if total == 0:
weight = 1.0
else:
weight = stats['success'] / total
weighted.append((proxy, weight))
# Random selection weighted by success rate
total_weight = sum(w for _, w in weighted)
r = random.uniform(0, total_weight)
cumulative = 0
for proxy, weight in weighted:
cumulative += weight
if r <= cumulative:
return proxy
return weighted[-1][0]
def report_result(self, proxy, success):
if success:
self.proxy_stats[proxy]['success'] += 1
else:
self.proxy_stats[proxy]['fail'] += 1
# Block proxies with high failure rates
stats = self.proxy_stats[proxy]
total = stats['success'] + stats['fail']
if total >= 10 and stats['fail'] / total > 0.7:
self.blocked_proxies.add(proxy)
The pool automatically deprioritizes poorly performing proxies. Blocked proxies stay out of rotation.
Integrating Proxies with the Solver
Pass proxy configuration to your HTTP requests. Match proxy location with fingerprint data.
def solve_with_proxy(site, proxy_pool):
proxy = proxy_pool.get_proxy()
config = get_session_config(site)
fingerprint = get_random_fingerprint()
# Ensure timezone matches proxy location
proxy_location = get_proxy_location(proxy)
fingerprint['timezone'] = get_timezone_for_location(proxy_location)
profiler = TMXProfiler(config)
profiler.set_proxy(proxy)
try:
success = profiler.profile(fingerprint)
proxy_pool.report_result(proxy, success)
return {
'success': success,
'session_id': config['session_id'] if success else None,
'proxy_used': proxy
}
except Exception as e:
proxy_pool.report_result(proxy, False)
raise
Geographic consistency between IP location and fingerprint properties improves pass rates.
Handling Proxy Failures Gracefully
Network errors shouldn't crash your solver. Implement retry logic with backoff.
import time
def solve_with_retry(site, proxy_pool, max_retries=3):
last_error = None
for attempt in range(max_retries):
try:
result = solve_with_proxy(site, proxy_pool)
if result['success']:
return result
except requests.exceptions.ProxyError as e:
last_error = e
# Exponential backoff
time.sleep(2 ** attempt)
continue
except requests.exceptions.Timeout:
time.sleep(1)
continue
return {
'success': False,
'error': str(last_error) if last_error else 'Max retries exceeded'
}
Retries with different proxies often succeed where single attempts fail.
Common Pitfalls and Debugging Tips
Building a TMX solver involves many moving parts. Here are the issues you'll encounter most often.
TLS Fingerprinting Issues
TMX checks TLS handshake characteristics. Python's default SSL library produces different fingerprints than browsers.
Use libraries like curl_cffi or tls-client that mimic browser TLS signatures.
from curl_cffi import requests as curl_requests
def make_browser_like_request(url, data, headers):
response = curl_requests.post(
url,
data=data,
headers=headers,
impersonate="chrome110" # Mimic Chrome's TLS fingerprint
)
return response
TLS mismatch is a common reason for unexplained solve failures.
Fingerprint Consistency Errors
Every property in your fingerprint must be internally consistent. Common mistakes include:
- Windows platform with Mac-specific WebGL renderers
- Chrome user agent with Firefox navigator properties
- Mobile screen resolution with desktop user agent
Validate fingerprint combinations before using them in production.
Session ID Reuse
Never reuse session IDs. TMX flags duplicate session IDs as suspicious.
Generate fresh session IDs for every solve attempt. Even retry attempts need new IDs.
Payload Encoding Mismatches
If your encoded payload doesn't match what TMX expects, the request silently fails. The server returns 200 but doesn't register the session.
Compare your encoded output against captured traffic from real browsers. Use browser developer tools to capture legitimate TMX requests.
Rate Limiting Detection
TMX tracks request patterns. Sending too many requests from the same IP triggers rate limits.
Space out requests and rotate IPs. Mimic human timing patterns—real users don't submit requests in perfect 1-second intervals.
Final Thoughts
Building a TMX solver requires understanding multiple technical domains: JavaScript reverse engineering, browser fingerprinting, network protocols, and API design.
Start with the basics. Harvest real fingerprints, understand the payload format, and test against live sites.
The landscape changes constantly. TMX updates their detection methods, sites update their integrations. Plan for ongoing maintenance from the start.
A working solver opens doors for security research, automated testing, and understanding how fraud detection systems operate at scale.
FAQ
What programming language is best for building a TMX solver?
Python works well for rapid prototyping with its extensive HTTP and parsing libraries. Golang offers better performance for production solvers handling high request volumes. Both ZacharyHampton's and post04's reference implementations demonstrate these approaches.
How long do TMX session IDs remain valid?
Session IDs typically expire within 5-15 minutes of generation. The exact timeout depends on the customer's TMX configuration. Always generate fresh session IDs immediately before you need them rather than pre-generating batches.
Can TMX detect headless browsers?
Yes, TMX actively detects headless browser signatures. Automation properties like navigator.webdriver, missing browser plugins, and anomalous canvas fingerprints all trigger detection. Use stealth plugins and harvest fingerprints from real browser sessions.
Why do solves fail even with correct payloads?
Multiple factors beyond payload correctness affect pass rates. IP reputation, behavioral signals from the broader session, historical device patterns, and customer-specific policies all influence the risk score. A technically correct solve can still receive a "Review" or "Reject" status.
How often should I update my fingerprint database?
Refresh fingerprints monthly at minimum. Browser updates change fingerprint characteristics. Operating system patches modify system properties. Stale fingerprints become increasingly recognizable as outdated.