Build a Mailchimp alternative
Build a Mailchimp alternative as one list, a markdown campaign, and a send. Reputation, bounces, and unused visual journeys are the other product.
You can build a Mailchimp alternative as one list, a markdown campaign, and a send. The catalog calls Mailchimp that loop, not a marketing OS.
You pay $20–$350 a month. The verdict is “If your list is simple”.
Last time I wrote about vibe coding your own FreshBooks. That was a regular backlog post. This one is a teardown.
Build a Mailchimp alternative as a list
The Mailchimp page splits the work into three rows.
The first row is 12 hours. A subscriber list and a broadcast. That is a table of emails, a draft, and a job that hands each row to a transactional provider. You can vibe-code this.
The second row is 4 days. Double opt-in, tags, scheduling, open tracking. Still your list. A confirm link, a send-at time, a count of opens you treat as approximate.
The third row is 3 months and up. Visual journeys, a template builder, a full CRM. Skip that row.
If you tell an agent to “clone Mailchimp”, it aims at the third row. The useful v1 is the first one.
Here is the shape of the data. Nothing else has to exist at 12 hours.
const subscriber = {
id: 'sub-ada',
email: '[email protected]',
status: 'subscribed',
source: 'footer-form',
confirmedAt: '2026-10-12T09:14:00.000Z',
}
const campaign = {
id: 'camp-oct-28',
subject: 'What shipped this week',
fromName: 'Bootcamp.dev',
replyTo: '[email protected]',
body: 'Four posts went out. The invoice one is live.',
status: 'draft',
}
A subscriber, a campaign, later a delivery row per send. Tags wait until you have two kinds of reader. Scheduling waits until you write on Sunday and send on Tuesday.
The audience line on the page is a small company sending a weekly newsletter to one audience. If that is you, one list is enough.
The same v1 also replaces MailerLite, Brevo, ActiveCampaign, and Constant Contact if what you wanted was “people who asked, then one email”. Kit is the creator cousin at $29–$79 a month. Ghost Pro is the public archive cousin at $9–$35. A signup that should stay a line, not a list, is GetWaitlist at $15–$50.
How does Mailchimp work
Mailchimp publishes the Marketing API, the Transactional docs, and a few posts about sending. I am summarizing those pages, not reconstructing today’s cluster.
The Marketing API is REST on https://api.mailchimp.com/3.0/. Resources are nouns. subscribers. campaigns. Actions that are not GET or POST live under actions/. Sending is POST /campaigns/{campaign_id}/actions/send.
A list member is an email plus a status. The add-member endpoint allows subscribed, unsubscribed, cleaned, pending, and transactional. The member id is the MD5 of the lowercase email. That hash is a lookup key, not a secret.
pending is the double-opt-in path. Their import guide is blunt. On a double-opt-in audience, subscribed skips the confirmation mail. You end up with a contact marked subscribed who never confirmed. Send pending and let Mailchimp mail the confirm link.
A 12-hour v1 copies the public statuses, not the endpoints. Store pending until the confirm token is used. Then flip to subscribed. Never mail pending or unsubscribed.
function confirmSubscriber(row, token) {
if (row.confirmToken !== token) return row
if (row.status !== 'pending') return row
return {
...row,
status: 'subscribed',
confirmToken: null,
confirmedAt: '2026-10-12T09:14:00.000Z',
}
}
Campaigns are a second resource. The create-campaign docs name the types: regular, plaintext, absplit, rss, variate. Content is a later PUT to /campaigns/{campaign_id}/content. You send HTML, or plain text, or both. If you omit plain text, Mailchimp generates it.
A v1 is regular plus markdown you render once. No RSS campaign. No multivariate test.
Rate limits are 10 simultaneous connections per user, not per key. Calls time out at 120 seconds. The batch endpoint exists because a 10,000-row import will not finish in that window. Mailchimp runs the operations on its own workers. You poll. A v1 imports a CSV in one request and writes rows in your database. No batch job.
How Mailchimp campaigns send
The skip row starts when you click Send on a list you do not host.
A 2015 sending post is the public picture of bulk mail. At the time they said about 600 million emails a day. Clicking Send splits the campaign into smaller payloads, about 10,000 recipients each on large sends, and each payload goes to a mail transfer agent as soon as it is ready. Engaged contacts go first. They mark the campaign sent once the payloads are queued, then watch SMTP replies for delivery and bounces. Campaigns under 100,000 recipients, they said, reached most inboxes in under 15 minutes.
Treat “the 2026 cluster still cuts at 10,000” as an educated guess. The product rule is public: slice the audience, queue early, record bounces after the fact.
A later Transactional engineering note gives a newer volume number. Mailchimp says the infrastructure sends up to 157 billion emails a month, with 99.99% uptime and a median delivery under one second on that product. Your weekly letter is one job in that queue.
A v1 does not need an MTA farm. Render the campaign once. Select status = 'subscribed'. Hand each address to a transactional provider. Write a delivery row before you call the provider, so a retry does not mail twice.
async function queueBroadcast(campaign, subscribers) {
const ready = subscribers.filter((row) => row.status === 'subscribed')
return ready.map((row) => ({
id: `del-${campaign.id}-${row.id}`,
campaignId: campaign.id,
email: row.email,
status: 'queued',
}))
}
Keep id stable. If del-camp-oct-28-sub-ada already exists, skip Ada.
I already wrote the Ghost teardown. Ghost’s first row is the public site. Mailchimp’s first row is the list. If you want both, start on Ghost, then add the send.
How Mailchimp reputation works
Inbox placement is the other product.
Domain authentication is required on Transactional. DKIM plus a DMARC TXT record. They accept v=DMARC1; p=none as the minimum. Classic Mailchimp asks for the same pair in the domain setup guide: two DKIM CNAMEs and _dmarc. A v1 that sends from bootcamp.dev needs those records on day one, even if the list has twelve people. The catalog guardrail already says authenticate the sending domain.
Reputation is account-level on Transactional. Poor, Good, or Excellent. It moves with spam complaints, bounces, unsubscribes, failed content reviews, and denylist edits. The hourly quota follows the reputation. Shared IPs are the reason. One bad sender burns the neighbors.
A bounce, an unsubscribe, or a spam complaint adds the address to a Rejection Denylist. Later sends to that address become rejections. Rejections are not sent, so they do not hit reputation again. The webhook docs POST those events to you: hard-bounce, soft-bounce, spam, unsub, reject, plus opens and clicks if you turned tracking on. The body is a mandrill_events array, up to 1,000 events.
A v1 should ingest the same events from whatever provider you use. Flip subscribed to cleaned on a hard bounce. Flip to unsubscribed on unsub or spam. Do not mail that row again.
function applyEvent(row, event) {
if (event.type === 'hard_bounce') {
return { ...row, status: 'cleaned' }
}
if (event.type === 'unsub' || event.type === 'spam') {
return { ...row, status: 'unsubscribed', unsubscribedAt: event.at }
}
return row
}
Mailchimp’s own unsubscribe help says every marketing mail needs an unsubscribe method. Templates inject *|UNSUB|*. The in-body link is two clicks, a page then a confirm, because inbox bots click links. One-click still lives in the List-Unsubscribe header, which Gmail and Yahoo want. A v1 can do both: a signed /unsubscribe?token= link in the footer, and the header on the provider call. Honor it in the same request. Do not hide the address in a follow-up job that might fail.
Visual journeys are the rest of the skip row. The Customer Journeys trigger is one POST of an email into a flow you drew in the app. The builder, the wait steps, the branch on “opened last campaign”, that is a rules engine plus a send graph. The catalog non-goal already names visual automations, the template marketplace, multivariate tests, and a full CRM. HubSpot is the CRM if that is the rent.
A Mailchimp alternative for one newsletter
I would rebuild Mailchimp for the one letter I already write on Monday. Footer form. Confirm link. Markdown in, HTML out. A Worker cron that sends what is status = 'queued'. DKIM on the domain I already own.
Tags come after I have two audiences that should not get the same subject. Open tracking comes after I stop guessing whether anyone read it. I would not draw a journey. I would not import a purchased list.
Host it next to the product, on Node.js and Astro, with D1 for the rows and a Worker cron for the send, because that is the stack on the card. Use TanStack Start if React is your weekday language. Keep consent evidence on the subscriber row. Honor unsubscribe before the next campaign.
If the rent is a public archive that also emails, that is Ghost Pro, 12 hours for posts and RSS. If the rent is a creator letter with an archive, that is Kit, 10 hours for subscribers and markdown broadcasts. If the rent is a launch line, that is GetWaitlist. Do not grow that line into Postbird.
Start with the first row
Go to Build your own Mailchimp. Copy the Astro prompt, or TanStack Start if that is what you write.
Tell the agent the first estimate only: subscriber list, send a broadcast. Twelve hours.
If the company already lives in journeys, a template library, and a shared audience of tens of thousands, keep paying.