Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2b7879e
fix(sales): diagnose inbound-only mail path in sender-trust check
nish3451 Aug 11, 2026
61dd573
docs(sales): regenerate sender/market docs with inbound-only mail pat…
nish3451 Aug 11, 2026
36ee83a
docs(sales): sync tracked artifacts to fixed-clock regeneration
nish3451 Aug 11, 2026
f03b9a4
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 12, 2026
4894f85
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 12, 2026
a337028
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 12, 2026
2d7df77
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 12, 2026
69385bd
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 12, 2026
dde56c8
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 12, 2026
c86a291
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 12, 2026
ab14b71
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 12, 2026
1e746b8
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 12, 2026
5bff0d0
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 12, 2026
b6fc899
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 13, 2026
9955591
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 13, 2026
c7bf1f1
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 13, 2026
089761f
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 13, 2026
f9935d0
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 13, 2026
b1e1b42
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 13, 2026
4dcd78e
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 13, 2026
416601a
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 13, 2026
5e20697
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 13, 2026
ce8ff6b
Merge branch 'main' into fix/sender-trust-inbound-only-diagnosis
nish3451 Aug 13, 2026
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 docs/strategy/market-parity-benchmark-2026.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Current public market signals:
| Automated workflow depth | can claim | Automation prepares intake, research, drafts, QA, evidence, handoff, and routing; humans decide fit, claims, delivery, acceptance, and renewal. |
| Tangible improvement cadence | do not claim yet | 0 canonical paid client folder(s); 0 client-ready folder(s). |
| Market traction | do not claim yet | 0/5 Looms, 0/5 sends, 0 replies. |
| Sender trust | do not claim yet | missing physical postal address; DKIM selector not configured |
| Sender trust | do not claim yet | missing physical postal address; outbound mail path is inbound-only; DKIM selector not configured |
| Proof-run discipline | do not claim yet | proof run needs-recording; approved rows 0; sent rows 0. |

## TinyStudio Pricing Interpretation
Expand Down
2 changes: 1 addition & 1 deletion growth-brain/ops/11-10-proof-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Parity score: 5/10.

| Area | Current Evidence | Required Proof |
|---|---|---|
| Sender trust | missing physical postal address; DKIM selector not configured | Run `npm run send:configure -- --physical-address="..." --dkim-selector=... --dry-run` with the real values, then apply it without `--dry-run`. Until then, use contact forms or DMs. |
| Sender trust | missing physical postal address; outbound mail path is inbound-only; DKIM selector not configured | Run `npm run send:configure -- --physical-address="..." --dkim-selector=... --dry-run` with the real values, then apply it without `--dry-run`. Until then, use contact forms or DMs. |
| Market proof | 0/5 Looms, 0/5 sends, 0 replies | Record and send 5 approved Looms with fault, impact, fix, and ask notes. |
| Sales proof | 0 external client(s) with a validated application, human fit approval, and paid Day 0 | Capture at least 1 external consented application, human fit approval, and validated paid Day 0 record. |
| Delivery proof | 0 external paid client(s) ready with approved delivery; 0 with approved claims | Complete hash-bound human approval, implementation acceptance, approved claims, scorecard, and client readiness for that paid client. |
Expand Down
2 changes: 1 addition & 1 deletion growth-brain/ops/competitive-proof-matrix.html
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ <h2>TinyStudio Proof Bar</h2>
<tr>
<td>Sender trust</td>
<td><span class="pill bad">do not claim yet</span></td>
<td>missing physical postal address; DKIM selector not configured</td>
<td>missing physical postal address; outbound mail path is inbound-only; DKIM selector not configured</td>
</tr>
<tr>
<td>Proof-run discipline</td>
Expand Down
2 changes: 1 addition & 1 deletion growth-brain/ops/competitive-proof-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Current public market signals:
| Automated workflow depth | can claim | Automation prepares intake, research, drafts, QA, evidence, handoff, and routing; humans decide fit, claims, delivery, acceptance, and renewal. |
| Tangible improvement cadence | do not claim yet | 0 canonical paid client folder(s); 0 client-ready folder(s). |
| Market traction | do not claim yet | 0/5 Looms, 0/5 sends, 0 replies. |
| Sender trust | do not claim yet | missing physical postal address; DKIM selector not configured |
| Sender trust | do not claim yet | missing physical postal address; outbound mail path is inbound-only; DKIM selector not configured |
| Proof-run discipline | do not claim yet | proof run needs-recording; approved rows 0; sent rows 0. |

## TinyStudio Pricing Interpretation
Expand Down
2 changes: 1 addition & 1 deletion growth-brain/ops/market-parity-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Not 11/10 yet: current evidence proves the internal system, but sender trust, ma
| Automation coverage | pass | recording, send, follow-up, sales, delivery, proof, and metrics surfaces are generated |
| Stress-tested internals | pass | kit, claim, and send gates pass on current repo state |
| Comparable price/value | pass | The Website Correction; $1,000 founder pilot; scope one highest-leverage page |
| Sender trust | fail | missing physical postal address; DKIM selector not configured |
| Sender trust | fail | missing physical postal address; outbound mail path is inbound-only; DKIM selector not configured |
| Market proof | fail | 0/5 Looms, 0/5 sends, 0 replies |
| Sales proof | fail | 0 external client(s) with a validated application, human fit approval, and paid Day 0 |
| Delivery proof | fail | 0 external paid client(s) ready with approved delivery; 0 with approved claims |
Expand Down
17 changes: 14 additions & 3 deletions growth-brain/ops/sender-setup-guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ <h2>DNS Checks</h2>
</div>


<div class="card">
<span>Outbound mail path</span>
<strong>needs work</strong>
<small>route3.mx.cloudflare.net, route1.mx.cloudflare.net, route2.mx.cloudflare.net</small>
</div>


<div class="card">
<span>DKIM discovery</span>
<strong>needs work</strong>
Expand All @@ -107,6 +114,9 @@ <h2>Fix First</h2>
<li><strong>missing physical postal address</strong><br>Commercial email needs a valid physical postal address. Use a business address, PO box, or private mailbox before cold email.</li>


<li><strong>outbound mail path is inbound-only</strong><br>MX records point at Cloudflare Email Routing (route3.mx.cloudflare.net, route1.mx.cloudflare.net, route2.mx.cloudflare.net), which forwards inbound mail only. Connect a sending provider (Google Workspace, Zoho Mail, Outlook, Resend, Postmark, or SendGrid), enable DKIM there, then save its exact selector as dkimSelector.</li>


<li><strong>DKIM selector not configured</strong><br>Set dkimSelector after enabling DKIM in the mail provider. No common DKIM selectors were found in DNS.</li>
</ol>
</section>
Expand All @@ -118,10 +128,11 @@ <h2>DKIM Discovery</h2>
<section>
<h2>Order</h2>
<ol>
<li>Add the sender postal address in <code>agency-config.json</code>.</li>
<li>Enable DKIM in the mail provider and copy the selector.</li>
<li>Connect an outbound sending provider (Cloudflare Email Routing forwards inbound mail only and cannot send).</li>
<li>Add the sender postal address in <code>agency-config.json</code> (business address, PO box, or private mailbox).</li>
<li>Enable DKIM in the outbound provider and copy the selector.</li>
<li>Add the DKIM TXT record in Cloudflare DNS at <code>&lt;selector&gt;._domainkey.tinystudio.io</code>.</li>
<li>Save the selector as <code>dkimSelector</code>.</li>
<li>Add the DKIM TXT record in Cloudflare if the provider gives one.</li>
<li>Run <code>npm run send:setup</code>.</li>
</ol>
</section>
Expand Down
15 changes: 9 additions & 6 deletions growth-brain/ops/sender-setup-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ Use this before cold email. Until this guide is clean, use contact forms or DMs
|---|---|---|
| SPF | ready | tinystudio.io |
| DMARC | ready | _dmarc.tinystudio.io |
| Outbound mail path | needs work | route3.mx.cloudflare.net, route1.mx.cloudflare.net, route2.mx.cloudflare.net |
| DKIM discovery | needs work | common selectors at _domainkey.tinystudio.io |

## Warnings To Fix

| Warning | What It Means |
|---|---|
| missing physical postal address | Commercial email needs a valid physical postal address. Use a business address, PO box, or private mailbox before cold email. |
| outbound mail path is inbound-only | MX records point at Cloudflare Email Routing (route3.mx.cloudflare.net, route1.mx.cloudflare.net, route2.mx.cloudflare.net), which forwards inbound mail only. Connect a sending provider (Google Workspace, Zoho Mail, Outlook, Resend, Postmark, or SendGrid), enable DKIM there, then save its exact selector as dkimSelector. |
| DKIM selector not configured | Set dkimSelector after enabling DKIM in the mail provider. No common DKIM selectors were found in DNS. |

## DKIM Discovery
Expand All @@ -40,12 +42,13 @@ npm run send:configure -- --physical-address="..." --dkim-selector=... --dry-run

## Fix Order

1. Add a real sender postal address to `senderPhysicalAddress` in `growth-brain/ops/agency-config.json`.
2. In the mail provider for `tinystudio.io`, enable DKIM and copy the selector.
3. Add the selector to `dkimSelector` in `growth-brain/ops/agency-config.json`.
4. If the mail provider gives a DKIM TXT record, add it in Cloudflare DNS at `<selector>._domainkey.tinystudio.io`.
5. Run `npm run send:setup`.
6. If it is clean, email can join contact forms and DMs as an outbound route.
1. Connect an outbound sending provider for `tinystudio.io` (Cloudflare Email Routing forwards inbound mail only and cannot send).
2. Add a real sender postal address to `senderPhysicalAddress` in `growth-brain/ops/agency-config.json` (business address, PO box, or private mailbox).
3. In the outbound provider, enable DKIM and copy the selector.
4. Add the provider's DKIM TXT record in Cloudflare DNS at `<selector>._domainkey.tinystudio.io`.
5. Save the selector as `dkimSelector` in `growth-brain/ops/agency-config.json`.
6. Run `npm run send:setup`.
7. If it is clean, email can join contact forms and DMs as an outbound route.

## Notes

Expand Down
28 changes: 27 additions & 1 deletion scripts/check-outbound-sender-setup.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
import { resolveTxt } from "node:dns/promises";
import { resolveMx, resolveTxt } from "node:dns/promises";
import { agencyConfig } from "./lib/agency-config.mjs";

const strict = process.argv.includes("--strict");
Expand Down Expand Up @@ -47,6 +47,17 @@ async function txtRecords(domain) {
}
}

async function mxRecords(domain) {
try {
const records = await resolveMx(domain);
return records
.sort((a, b) => a.priority - b.priority)
.map((record) => record.exchange.toLowerCase());
} catch {
return [];
Comment on lines +56 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish failed MX lookups from absent records

When DNS is temporarily unavailable or returns SERVFAIL/ETIMEOUT, this catch converts the resolver failure into an empty result, and the caller consequently states that no MX records exist and replies will bounce. Running send:setup or regenerating tracked operator artifacts during a transient resolver failure therefore produces a false configuration diagnosis and potentially instructs the operator to change valid DNS; preserve the error so lookup failure can be reported separately or retried.

Useful? React with 👍 / 👎.

}
}

function warn(rule, detail) {
warnings.push({ rule, detail });
}
Expand Down Expand Up @@ -91,6 +102,21 @@ if (!domain) {
checks.push({ name: "DMARC", status: dmarc ? "found" : "missing", domain: dmarcDomain });
if (!dmarc) warn("missing DMARC", `No DMARC TXT record found for ${dmarcDomain}.`);

const mx = await mxRecords(domain);
if (mx.length) {
const inboundOnly = mx.every((host) => /^route\d+\.mx\.cloudflare\.net\.?$/.test(host));
checks.push({ name: "Outbound mail path", status: inboundOnly ? "missing" : "found", domain: mx.join(", ") });
Comment on lines +107 to +108

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Base outbound readiness on sender configuration, not MX

MX records describe where inbound mail is received, not how the domain sends mail. A valid setup using Cloudflare Email Routing for inbound mail and Resend/Postmark/SendGrid for outbound will continue to show this check as missing, while a domain with non-Cloudflare MX records but no outbound provider is reported as found. This makes the new diagnosis unreliable in both directions; derive outbound readiness from an explicitly configured sending provider and its authentication instead.

Useful? React with 👍 / 👎.

Comment on lines +107 to +108

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject null MX records as a reply path

When a domain publishes a null MX record (0 .) to declare that it accepts no mail, mx.length is nonzero and the root exchange does not match the Cloudflare pattern, so this records the path as found. Such a sender cannot receive replies and may let send:setup pass once the other checks are satisfied; detect the null-MX sentinel and report it as unavailable.

Useful? React with 👍 / 👎.

if (inboundOnly && !config.dkimSelector) {
warn(
"outbound mail path is inbound-only",
`MX records point at Cloudflare Email Routing (${mx.join(", ")}), which forwards inbound mail only. Connect a sending provider (Google Workspace, Zoho Mail, Outlook, Resend, Postmark, or SendGrid), enable DKIM there, then save its exact selector as dkimSelector.`
);
}
} else {
checks.push({ name: "Outbound mail path", status: "missing", domain: `no MX records for ${domain}` });
warn("no MX records for sender domain", "No MX records exist, so replies to the sender address would bounce. Add MX records or use a sender domain that can receive replies before cold email.");
Comment on lines +116 to +117

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Feed the no-MX failure into the email send gate

When the configured sender has a postal address and DKIM selector but genuinely has no MX records, this branch makes send:setup warn that replies will bounce, but sendChannelGuidance() still computes emailReady solely from those local config values. Consequently, checked consumers such as update-prospect-pipeline.mjs, complete-prospect-batch-send.mjs, and the prospect outbox permit and recommend email despite this new failure; propagate the setup failure into the shared email-readiness gate so operators cannot proceed with a reply address known to reject mail.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor implicit MX fallback before declaring replies broken

When a sender domain has no MX record but its A/AAAA address accepts SMTP, standard mail routing treats that address as an implicit MX target. This branch instead states that replies will bounce and marks setup as failed, so a valid receiving configuration is misdiagnosed; verify the implicit host before emitting the no-reply-path warning.

Useful? React with 👍 / 👎.

}

if (config.dkimSelector) {
const dkimDomain = `${config.dkimSelector}._domainkey.${domain}`;
const dkimTxt = await txtRecords(dkimDomain);
Expand Down
20 changes: 11 additions & 9 deletions scripts/export-sender-setup-guide.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,13 @@ ${configureCommand}

## Fix Order

1. Add a real sender postal address to \`senderPhysicalAddress\` in \`growth-brain/ops/agency-config.json\`.
2. In the mail provider for \`${setup.senderDomain || "the sender domain"}\`, enable DKIM and copy the selector.
3. Add the selector to \`dkimSelector\` in \`growth-brain/ops/agency-config.json\`.
4. If the mail provider gives a DKIM TXT record, add it in Cloudflare DNS at \`${dkimHost}\`.
5. Run \`npm run send:setup\`.
6. If it is clean, email can join contact forms and DMs as an outbound route.
1. Connect an outbound sending provider for \`${setup.senderDomain || "the sender domain"}\` (Cloudflare Email Routing forwards inbound mail only and cannot send).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Only prescribe Cloudflare remediation when detected

When send:configure -- --sender-email=... switches to a domain that uses another mail host—or when an outbound provider is already configured—send:guide still claims Cloudflare Email Routing is in use and instructs the operator to connect a provider, even if the setup checks pass. Derive this fix-order step from the actual inbound-only warning/check instead of emitting it unconditionally; the HTML instructions have the same issue.

Useful? React with 👍 / 👎.

2. Add a real sender postal address to \`senderPhysicalAddress\` in \`growth-brain/ops/agency-config.json\` (business address, PO box, or private mailbox).
3. In the outbound provider, enable DKIM and copy the selector.
4. Add the provider's DKIM TXT record in Cloudflare DNS at \`${dkimHost}\`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the provider-supplied DKIM record type

When the operator chooses Outlook/Microsoft 365—one of the providers recommended by this change—the provider supplies selector1 and selector2 CNAME records rather than a DKIM TXT record. This unconditional instruction can therefore make the operator create the wrong DNS record type, preventing provider verification and leaving DKIM blocked; instruct them to publish the exact record type and values supplied by the provider, as the previous conditional wording did.

Useful? React with 👍 / 👎.

5. Save the selector as \`dkimSelector\` in \`growth-brain/ops/agency-config.json\`.
6. Run \`npm run send:setup\`.
7. If it is clean, email can join contact forms and DMs as an outbound route.

## Notes

Expand Down Expand Up @@ -233,10 +234,11 @@ const html = `<!doctype html>
<section>
<h2>Order</h2>
<ol>
<li>Add the sender postal address in <code>agency-config.json</code>.</li>
<li>Enable DKIM in the mail provider and copy the selector.</li>
<li>Connect an outbound sending provider (Cloudflare Email Routing forwards inbound mail only and cannot send).</li>
<li>Add the sender postal address in <code>agency-config.json</code> (business address, PO box, or private mailbox).</li>
<li>Enable DKIM in the outbound provider and copy the selector.</li>
<li>Add the DKIM TXT record in Cloudflare DNS at <code>${escapeHtml(dkimHost)}</code>.</li>
<li>Save the selector as <code>dkimSelector</code>.</li>
<li>Add the DKIM TXT record in Cloudflare if the provider gives one.</li>
<li>Run <code>npm run send:setup</code>.</li>
</ol>
</section>
Expand Down
Loading