Why do ad tools ignore the mailbox?
Clicks arrive with an ID attached. Postcards arrive as a phone call, a QR scan or a code typed into a form, so the mailbox never gets a column next to search.
Google Ads has a meter
Spend, clicks, conversions and cost per lead are read from the account. channel_cac can pull live Google Ads spend for a campaign, so the paid-search number is not typed in by hand. selftest: mail.cac
A postcard has no click
Nobody puts a postcard's cost per lead next to Google's because the response signal is a call, a scan or a read-out code. Without a unique code the lead is unattributable. Without a clean list, the print and postage spend is guesswork before it leaves the building.
One ruler, five channels
channel_cac reports cost per lead and per client across Google Ads, ChatGPT Ads, direct mail, lead vendors and referrals. Attribution order: tracking code, then UTM, then what the lead said, then referrer. selftest: mail.cac
ChatGPT Ads (OpenAI Ads API) is supported since AdRails 0.5.0. Today the live spend pull is Google Ads; ChatGPT Ads still appears as a channel in channel_cac. Ten direct-mail tools are listed under /docs/tools/.
What six mailing mistakes does the toolkit exist to stop?
The rails were generalised from a live postcard program a Tampa property manager ran in 2026. That program hit all six: labels cut off ZIP+4 and unit numbers, foreign addresses with domestic stamps, institutional owners, a stale suppression list, test scans counted as responses, and link-preview bots counted as people.
Truncated labels
mail_list_clean normalises USPS abbreviations and ZIP+4. mail_labels wraps a long line and refuses a label that will not fit; it does not cut the unit number off. selftest: mail.hygiene, mail.labels
Foreign addresses
A domestic stamp on an overseas address is wasted postage. mail_list_clean drops foreign and incomplete addresses before the print file is built. selftest: mail.hygiene
Institutional owners
Banks, REITs and housing authorities come off the list. Plain LLCs and trusts are kept, because the rule is ownership type, not the word LLC. selftest: mail.hygiene
A stale suppression list
Clients, leads and do-not-mail entries are suppressed. mail_list_clean warns when the client suppression list is empty, which is the state that produces an angry call. selftest: mail.hygiene
Your own test scans
The first scan is usually yours. mail_scans drops your own IPs and CIDRs, IPv4 and IPv6, and any hit before that piece was mailed. selftest: mail.scans
Link-preview bots
Slack, Facebook, WhatsApp and similar fetch a URL to build a preview. mail_scans drops bots and link previewers, so the count is unique human scans per code. selftest: mail.scans
Can a tracking code survive being read over the phone?
A code that only works when scanned is not a tracking code for a local service business. Most of the response arrives as a spoken code on a call.
The check character
mail_codes_assign uses an alphabet without 0, 1, I, L, O or U, then adds a check character: a position-weighted checksum modulo the prime 31. It is measured to catch every single wrong character and every swapped adjacent pair, with 0 misses in 4,060,000 substitutions. selftest: mail.codes
Why 47 is the number
On the same substitution test, standard Luhn mod 30 missed 47 adjacent swaps. A caller reading two characters in the wrong order would have produced a valid-looking code and a misattributed lead. selftest: mail.codes
What it will not do
Lookalike characters are never guessed. If a caller reads a character the alphabet excludes, mail_codes_assign does not silently repair it into a different code. selftest: mail.codes
Each code also gets a tracking URL with UTM tags and a CSV ready for the label run. selftest: mail.codes
Do the QR codes actually scan?
A QR symbol that looks right on screen and fails on a phone is a silent hole in the response count.
What is generated
mail_qr is a pure-Python encoder for versions 1-10, error-correction levels L/M/Q/H and all eight masks. It returns SVG text and writes a PNG in the mail workdir. selftest: mail.qr
The decoder result
An independent decoder (OpenCV) read 557 of 557 test symbols back to the exact input. The harness control read 40 of 40 codes from an established encoder, so the test itself was not the weak link. selftest: mail.qr
Will the labels print exactly, or cut the unit number?
The address is the delivery mechanism. A wrapped line is recoverable; a truncated unit number is a returned piece and a wasted stamp.
Avery sheets as HTML
mail_labels builds print-exact Avery 5160/8160, 5163, 5164 and 5167 sheets as HTML that prints to PDF in any browser. An optional QR can sit on each label, and a vendor CSV is included with a CSV-injection guard. selftest: mail.labels
SAMPLE by default
The SAMPLE watermark is ON by default. A real print run uses sample=false and is written to the tamper-evident audit chain, so there is a record of which sheet was actually released to print. selftest: mail.labels
Too long is refused
A line that cannot fit is refused, not cut. mail_list_clean normalises the address first; mail_labels wraps and refuses overflow. selftest: mail.hygiene, mail.labels
How do you count scans without storing IP addresses?
A scan log can answer "did anyone respond?" without becoming a second customer database of raw IP addresses.
From your own logs
mail_scans reads your own web-server logs and counts unique human scans per code. It drops bots and link previewers such as Slack, Facebook and WhatsApp, your own IPs and CIDRs (IPv4 and IPv6), unknown codes, and any hit before that piece was mailed. selftest: mail.scans
Hashed ids, not raw IPs
Visitor ids are hashed; raw IPs are not stored. Re-running the same log is idempotent, so a weekly scan job does not inflate the numbers. First-scan delay and scan rate by batch or segment are returned too. selftest: mail.scans
A scan is not a lead
mail_scans counts scans. mail_response turns scans, leads and clients into response rates with intervals. A high scan count on a small batch still gets called "too early". selftest: mail.response
What is an honest response rate for a postcard?
A single percentage with no interval invites a decision the sample cannot support. The tool says what the data can and cannot show.
Rates with intervals
mail_response reports response rates by any column — batch, segment or variant — with 95% Wilson intervals and a verdict. Small samples are labelled "too early" or "no signal yet: could still be up to X%" rather than dressed up as a point estimate. selftest: mail.response
A/B, not vibes
mail_compare runs a two-proportion test with a difference interval: two postcard designs, or mailed versus a no-mail holdout. The difference gets an interval, not just a winner. selftest: mail.response
The worked example
Base rate 1%, target lift +50%. Do not compute the counts yourself; mail_holdout_plan returns the counts for a no-mail holdout at 95% confidence and 80% power. Its result lands within 3% of the textbook formula. selftest: mail.response
A no-mail holdout is a clean control: some pieces are not mailed and the response difference is measured. mail_holdout_plan sizes that holdout before you print.
How do you compare a postcard with a Google Ads click?
The channel with the cheapest visible cost is not always the channel with the cheapest client. The comparison has to use the same unit.
Cost per lead, cost per client
channel_cac puts Google Ads, ChatGPT Ads, direct mail, lead vendors and referrals on one ruler. Attribution order: tracking code, then UTM, then what the lead said, then referrer. It can pull live Google Ads spend for a campaign, and labels small numbers "too few to judge". selftest: mail.cac
Break-even before you print
mail_breakeven takes cost per piece (print, postage, other), lead-to-client rate and client value, and returns the response rate you need to break even. That is the number to beat, not last year's postcard anecdote. selftest: mail.cac
No postage, no contact
Nothing in the direct-mail tools sends mail, buys postage or contacts anyone. The tools build the list, codes, QR symbols and label sheets; your printer and mail house do the rest.
What does a month-one direct-mail routine look like?
The agent runs the same sequence every campaign: clean, code, label, mail, scan, response, CAC. Each step has a tool and an output you can inspect.
Clean the list
mail_list_clean normalises USPS abbreviations and ZIP+4, drops foreign and incomplete addresses, institutional owners, your own properties, clients and do-not-mail entries, and duplicates. selftest: mail.hygiene
Assign codes
mail_codes_assign gives every piece a unique code with a check character and a tracking URL with UTM tags, then writes a CSV ready for the label run. selftest: mail.codes
Print labels
Run mail_labels with the SAMPLE watermark first. When the sheet is right, run sample=false; the real run is written to the audit chain. Add a QR per label with mail_qr. selftest: mail.labels, mail.qr
Your printer or mail house sends the pieces. AdRails does not send mail, buy postage or contact anyone; it prepares the list, codes, symbols and labels.
Read scans weekly
mail_scans reads the web-server log, drops bots, link previewers, your own IPs, unknown codes and pre-mail hits, and returns unique human scans with first-scan delay. selftest: mail.scans
Response at six weeks
mail_response reports the rate by batch or segment with a 95% Wilson interval and an honest verdict. mail_compare tests variant against variant or mailed against holdout. selftest: mail.response
CAC across channels
channel_cac puts direct mail next to Google Ads, ChatGPT Ads, lead vendors and referrals as cost per lead and per client, with small numbers flagged "too few to judge". selftest: mail.cac
Can the mailbox join the same report as paid search?
Ten direct-mail tools run locally with your list, your printer and your CRM. The measurement is built to survive a phone call, a bot and a small sample.
For the answer-engine side of the same problem, see the AEO toolkit: crawler access for 18 AI and search crawlers, page answer-readiness, llms.txt and share-of-answer tracking. selftest: aeo.crawl_audit
Straight answers.
Can AdRails mail postcards for me?
No. The ten direct-mail tools do not send mail, buy postage or contact anyone. They clean the list, assign tracking codes, build print-exact label sheets and QR codes, and measure scans and response from logs you already have. You keep your printer, postage vendor and CRM; AdRails keeps the measurement honest.
How do I know a postcard actually caused a lead?
Each piece gets a unique tracking code and a URL with UTM tags. channel_cac attributes in a fixed order: tracking code first, then UTM, then what the lead said, then referrer. Scans count only after the mail date and only from unique humans, with bots, link previewers, your own IPs and unknown codes dropped (mail.scans, mail.cac).
What sample size do I need before I trust a response rate?
Use mail_holdout_plan. Give it the base response rate and the lift you want to detect; at 95% confidence and 80% power it returns the pieces and holdout counts, landing within 3% of the textbook formula. At a 1% base rate and a target of +50%, do not hand-calculate: mail_holdout_plan returns the counts. Until then mail_response says 'too early'.
Can the tracking codes be read over the phone?
Yes. Codes exclude 0, 1, I, L, O and U and carry a check character: a position-weighted checksum modulo the prime 31. It is measured to catch every single wrong character and every swapped adjacent pair, with 0 misses in 4,060,000 substitutions. Standard Luhn mod 30 missed 47 adjacent swaps on the same test (mail.codes).
Will the labels cut off ZIP+4 or apartment numbers?
No. mail_list_clean normalises USPS abbreviations and ZIP+4, and mail_labels refuses a label that is too long instead of truncating it. Sheets are print-exact Avery 5160/8160, 5163, 5164 and 5167 as HTML that prints to PDF from a browser. SAMPLE is on by default; a real run is written to the audit chain (mail.labels).
Do the QR codes work with normal phone scanners?
They are generated by a pure-Python encoder for versions 1-10, error-correction levels L/M/Q/H and all eight masks, returned as SVG and written as PNG. An independent decoder (OpenCV) read 557 of 557 test symbols back to the exact input, and the harness control read 40 of 40 codes from an established encoder (mail.qr).