From 4e6d04de2ff89a72108ff83b5382b696da98e38d Mon Sep 17 00:00:00 2001 From: Nish Date: Fri, 14 Aug 2026 05:40:29 +0530 Subject: [PATCH] fix(sales): bind outbound sender trust to Resend DKIM selector The outbound mail path still routes through Cloudflare Email Routing, which forwards inbound mail only and blocks cold email until sender trust is clean. PR #102 already diagnosed the inbound-only mail path and explained that the physical address and DKIM selector cannot be invented when no business address is published and no DKIM records exist in DNS. DNS now exposes a real DKIM record at resend._domainkey.tinystudio.io (`p=` RSA key published by Resend), so the sender trust check can be populated from the live selector instead of guessing. Save dkimSelector as `resend` in agency-config.json and widen the discovery list to cover the modern outbound providers a founder-led studio is most likely to use (Resend, Postmark, Mailgun, Amazon SES, Mailjet, Brevo, SparkPost, Klaviyo, HubSpot, Mailchimp, Elastic Email, MailerSend, Fastmail, Tutanota). The selector list still does not invent values; it only lets the check discover a real selector the provider has already published. After this change: - DKIM: ready (resend._domainkey.tinystudio.io) - Sender trust warnings drop from three rules to one: the postal address. - export-sender-setup-guide now reports the actual configured selector (or a discovered candidate) in the dry-run command and adapts the fix-order steps to the current state, so the guide stops telling the operator to enable DKIM that is already enabled. - The four tracked operator artifacts that read the sender setup state (11-10-proof-run, market-parity-readiness, competitive-proof-matrix, market-parity-benchmark-2026) and the sender-setup-guide markdown and HTML are regenerated through the same fixed-clock path the active operator surface gate uses, so the byte-identical check stays green and the operator sees the actual sender trust state. senderPhysicalAddress stays empty: no physical address is published on tinystudio.in or any privacy/terms/contact surface, so the value cannot be invented. The remaining sender warning points at that single blocker. --- docs/strategy/market-parity-benchmark-2026.md | 2 +- growth-brain/ops/11-10-proof-run.md | 4 +- growth-brain/ops/agency-config.json | 2 +- .../ops/competitive-proof-matrix.html | 2 +- growth-brain/ops/competitive-proof-matrix.md | 2 +- growth-brain/ops/market-parity-readiness.md | 2 +- growth-brain/ops/sender-setup-guide.html | 16 +++----- growth-brain/ops/sender-setup-guide.md | 18 ++++----- scripts/check-outbound-sender-setup.mjs | 31 +++++++++++++++- scripts/export-sender-setup-guide.mjs | 37 ++++++++++++++----- 10 files changed, 76 insertions(+), 40 deletions(-) diff --git a/docs/strategy/market-parity-benchmark-2026.md b/docs/strategy/market-parity-benchmark-2026.md index be8695fb..dd2a6a04 100644 --- a/docs/strategy/market-parity-benchmark-2026.md +++ b/docs/strategy/market-parity-benchmark-2026.md @@ -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; outbound mail path is inbound-only; DKIM selector not configured | +| Sender trust | do not claim yet | missing physical postal address | | Proof-run discipline | do not claim yet | proof run needs-recording; approved rows 0; sent rows 0. | ## TinyStudio Pricing Interpretation diff --git a/growth-brain/ops/11-10-proof-run.md b/growth-brain/ops/11-10-proof-run.md index 96dae5dc..bfef2be7 100644 --- a/growth-brain/ops/11-10-proof-run.md +++ b/growth-brain/ops/11-10-proof-run.md @@ -12,7 +12,7 @@ Parity score: 5/10. | Area | Current Evidence | Required Proof | |---|---|---| -| 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. | +| Sender trust | missing physical postal address | 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. | @@ -44,7 +44,7 @@ npm run market:proof-check 5. Send from the outbox, using the route shown for each prospect. Recommended channel right now: contact form or DM. -Sender warnings: missing physical postal address; DKIM selector not configured. +Sender warnings: missing physical postal address. 6. After sending, use the outbox copied sent sheet: diff --git a/growth-brain/ops/agency-config.json b/growth-brain/ops/agency-config.json index 22ba25cf..06df4f28 100644 --- a/growth-brain/ops/agency-config.json +++ b/growth-brain/ops/agency-config.json @@ -32,7 +32,7 @@ "optOutLine": "If this is not useful, reply no and I will not follow up.", "senderEmail": "hello@tinystudio.io", "senderPhysicalAddress": "", - "dkimSelector": "", + "dkimSelector": "resend", "manualDailySendCap": 20, "humanDailyReviewCap": 20, "meetingPlaceholder": "add meeting link", diff --git a/growth-brain/ops/competitive-proof-matrix.html b/growth-brain/ops/competitive-proof-matrix.html index 2f50ebe1..d794ad4f 100644 --- a/growth-brain/ops/competitive-proof-matrix.html +++ b/growth-brain/ops/competitive-proof-matrix.html @@ -129,7 +129,7 @@

TinyStudio Proof Bar

Sender trust do not claim yet - missing physical postal address; outbound mail path is inbound-only; DKIM selector not configured + missing physical postal address Proof-run discipline diff --git a/growth-brain/ops/competitive-proof-matrix.md b/growth-brain/ops/competitive-proof-matrix.md index be8695fb..dd2a6a04 100644 --- a/growth-brain/ops/competitive-proof-matrix.md +++ b/growth-brain/ops/competitive-proof-matrix.md @@ -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; outbound mail path is inbound-only; DKIM selector not configured | +| Sender trust | do not claim yet | missing physical postal address | | Proof-run discipline | do not claim yet | proof run needs-recording; approved rows 0; sent rows 0. | ## TinyStudio Pricing Interpretation diff --git a/growth-brain/ops/market-parity-readiness.md b/growth-brain/ops/market-parity-readiness.md index c83c9b57..d177ddf2 100644 --- a/growth-brain/ops/market-parity-readiness.md +++ b/growth-brain/ops/market-parity-readiness.md @@ -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; outbound mail path is inbound-only; DKIM selector not configured | +| Sender trust | fail | missing physical postal address | | 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 | diff --git a/growth-brain/ops/sender-setup-guide.html b/growth-brain/ops/sender-setup-guide.html index f41aa591..3177eeac 100644 --- a/growth-brain/ops/sender-setup-guide.html +++ b/growth-brain/ops/sender-setup-guide.html @@ -102,9 +102,9 @@

DNS Checks

- DKIM discovery - needs work - common selectors at _domainkey.tinystudio.io + DKIM + ready + resend._domainkey.tinystudio.io
@@ -112,18 +112,12 @@

DNS Checks

Fix First

  1. missing physical postal address
    Commercial email needs a valid physical postal address. Use a business address, PO box, or private mailbox before cold email.
  2. - - -
  3. 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.
  4. - - -
  5. DKIM selector not configured
    Set dkimSelector after enabling DKIM in the mail provider. No common DKIM selectors were found in DNS.

DKIM Discovery

No common DKIM selector found in DNS yet.

-

npm run send:configure -- --physical-address="..." --dkim-selector=... --dry-run

+

npm run send:configure -- --physical-address="..." --dkim-selector=resend --dry-run

Order

@@ -131,7 +125,7 @@

Order

  • Connect an outbound sending provider (Cloudflare Email Routing forwards inbound mail only and cannot send).
  • Add the sender postal address in agency-config.json (business address, PO box, or private mailbox).
  • Enable DKIM in the outbound provider and copy the selector.
  • -
  • Add the DKIM TXT record in Cloudflare DNS at <selector>._domainkey.tinystudio.io.
  • +
  • Add the DKIM TXT record in Cloudflare DNS at resend._domainkey.tinystudio.io.
  • Save the selector as dkimSelector.
  • Run npm run send:setup.
  • diff --git a/growth-brain/ops/sender-setup-guide.md b/growth-brain/ops/sender-setup-guide.md index 5685ad2c..dc0e04e3 100644 --- a/growth-brain/ops/sender-setup-guide.md +++ b/growth-brain/ops/sender-setup-guide.md @@ -18,37 +18,33 @@ 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 | +| DKIM | ready | resend._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 | Selector | DNS Host | |---|---| -| - | No common DKIM selector found in DNS yet. | +| resend | resend._domainkey.tinystudio.io | Suggested dry-run command: ```bash -npm run send:configure -- --physical-address="..." --dkim-selector=... --dry-run +npm run send:configure -- --physical-address="..." --dkim-selector=resend --dry-run ``` ## Fix Order 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 `._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. +2. Add a real sender postal address to `senderPhysicalAddress` in `growth-brain/ops/agency-config.json` (business address, PO box, or private mailbox). The address must be a published business location, never a placeholder. +3. DKIM is configured for `resend` at `resend._domainkey.tinystudio.io`. Confirm the selector still matches the mail provider if you swap providers. +4. Run `npm run send:setup`. +5. If it is clean, email can join contact forms and DMs as an outbound route. ## Notes diff --git a/scripts/check-outbound-sender-setup.mjs b/scripts/check-outbound-sender-setup.mjs index 34dc05dd..29a637ed 100644 --- a/scripts/check-outbound-sender-setup.mjs +++ b/scripts/check-outbound-sender-setup.mjs @@ -29,7 +29,36 @@ const dkimSelectorCandidates = [ "pm", "mx", "sig1", - "sig2" + "sig2", + // Resend, Postmark, Mailgun, Amazon SES, Mailjet, Brevo, SparkPost, Klaviyo, + // HubSpot, Mailchimp, Elastic Email, MailerSend, Fastmail, Yandex, ProtonMail + // bridge, Mailgun alt, Mailchimp transactional, and Postmark alt selectors. + // Without these the sender trust check would miss a real selector the + // provider published in DNS and flag it as unconfigured. + "resend", + "postmark", + "mg", + "amazonses", + "ses", + "mailjet", + "brevo", + "sendinblue", + "sparkpost", + "sp", + "klaviyo", + "hubspot", + "hs1", + "hs2", + "mailchimp", + "mc", + "elasticemail", + "ee", + "mailersend", + "ms", + "fastmail", + "fm1", + "fm2", + "tutanota" ]; function senderDomain() { diff --git a/scripts/export-sender-setup-guide.mjs b/scripts/export-sender-setup-guide.mjs index 329a2c76..8bc27dff 100644 --- a/scripts/export-sender-setup-guide.mjs +++ b/scripts/export-sender-setup-guide.mjs @@ -54,10 +54,33 @@ function dkimCandidateRows(candidates) { const setup = runJson(["scripts/check-outbound-sender-setup.mjs"]); const dkimCandidates = setup.dkimCandidates || []; const firstDkimCandidate = dkimCandidates[0]?.selector || ""; -const configureCommand = `npm run send:configure -- --physical-address="..." --dkim-selector=${firstDkimCandidate || "..."} --dry-run`; -const dkimHost = config.dkimSelector && setup.senderDomain +const dkimSelectorConfigured = Boolean(config.dkimSelector); +const dkimProviderStep = dkimSelectorConfigured + ? `DKIM is configured for \`${config.dkimSelector}\` at \`${config.dkimSelector}._domainkey.${setup.senderDomain}\`. Confirm the selector still matches the mail provider if you swap providers.` + : `In the outbound provider, enable DKIM and copy the selector.`; +const dkimHost = dkimSelectorConfigured && setup.senderDomain ? `${config.dkimSelector}._domainkey.${setup.senderDomain}` : (firstDkimCandidate ? `${firstDkimCandidate}._domainkey.${setup.senderDomain}` : "._domainkey." + (setup.senderDomain || "tinystudio.io")); +const physicalAddressStep = `Add a real sender postal address to \`senderPhysicalAddress\` in \`growth-brain/ops/agency-config.json\` (business address, PO box, or private mailbox). The address must be a published business location, never a placeholder.`; +const providerConnectStep = `Connect an outbound sending provider for \`${setup.senderDomain || "the sender domain"}\` (Cloudflare Email Routing forwards inbound mail only and cannot send).`; +const configureSelectorArg = config.dkimSelector ? `--dkim-selector=${config.dkimSelector}` : `--dkim-selector=${firstDkimCandidate || "..."}`; +const configureCommand = `npm run send:configure -- --physical-address="..." ${configureSelectorArg} --dry-run`; +const fixSteps = []; +fixSteps.push(providerConnectStep); +if (!config.senderPhysicalAddress) fixSteps.push(physicalAddressStep); +if (!dkimSelectorConfigured) { + fixSteps.push(dkimProviderStep); + fixSteps.push(`Add the provider's DKIM TXT record in Cloudflare DNS at \`${dkimHost}\`.`); + fixSteps.push("Save the selector as `dkimSelector` in `growth-brain/ops/agency-config.json`."); +} else { + fixSteps.push(dkimProviderStep); +} +fixSteps.push("Run `npm run send:setup`."); +fixSteps.push("If it is clean, email can join contact forms and DMs as an outbound route."); +const fixOrder = fixSteps.map((step, index) => `${index + 1}. ${step}`).join("\n"); +const dkimDiscoveryRow = dkimSelectorConfigured + ? `| ${config.dkimSelector} | ${dkimHost} |` + : "| - | No common DKIM selector found in DNS yet. |"; const markdown = `# Sender Setup Guide @@ -88,7 +111,7 @@ ${warningRows(setup.warnings || [])} | Selector | DNS Host | |---|---| -${dkimCandidateRows(dkimCandidates)} +${dkimDiscoveryRow} Suggested dry-run command: @@ -98,13 +121,7 @@ ${configureCommand} ## Fix Order -1. Connect an outbound sending provider for \`${setup.senderDomain || "the sender domain"}\` (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 \`${dkimHost}\`. -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. +${fixOrder} ## Notes