Domain Search Agent
A Slack-triggered agent that takes a business idea, brainstorms domain names, checks availability using RDAP lookups, and replies in-thread with formatted recommendations. Built with custom tools and zero sandbox access — it only needs the network and Slack.
This cookbook assumes you've already installed the CLI and set up an API key.
1. What This Demonstrates
- Slack triggers — the agent activates on
app_mentionandmessageevents in a specific channel - Custom tools — two TypeScript tools that call the public RDAP API, with retry logic and rate-limit handling
- Minimal tool surface — all built-in sandbox tools disabled; the agent only has its custom tools and
slack_reply - Prompt templates — injecting Slack event context into the agent's prompt
2. Scaffold the Agent
bash
swarmlord init domain-search \
-m "anthropic/claude-haiku-4.5" \
-p "Find available domains for this idea."3. Configure
Replace swarmlord.jsonc:
jsonc
{
"$schema": "https://swarmlord.ai/config.json",
"name": "domain-search",
"description": "Domain name exploration and availability checking agent — triggered via Slack",
"model": "anthropic/claude-haiku-4.5",
"trigger": {
"provider": "slack",
"events": ["app_mention", "message"],
"channels": ["domain-search-agent"],
},
"promptTemplate": "A Slack message was received:\n\nFrom: {{event.user}}\nChannel: {{event.channel}}\nThread: {{event.ts}}\nText: {{event.text}}\n\nFind available domains for this idea. When done, deliver your results by calling slack_reply with your formatted message.",
"expectedTools": ["slack_reply"],
"tools": {
"bash": false,
"read": false,
"write": false,
"skill": false,
"websearch": false,
"webfetch": false,
"todowrite": false,
"todoread": false,
},
"agent": {
"build": {
"description": "Brainstorms domain names, checks availability via RDAP, returns concise Slack-formatted results",
"steps": 50,
},
},
"permission": { "*": "allow" },
}Every built-in tool is explicitly disabled — this agent doesn't need filesystem access, web browsing, or bash. It only uses its two custom tools plus the slack_reply integration tool provided by the trigger.
4. Write the Instructions
Edit SOUL.md:
markdown
# Domain Search Agent
You are the Head of Marketing & Communications recommending domain names for a
business idea. Your reader is a founder, not a developer — write for them.
You have two tools: `check_domain` (single lookup) and `search_tlds` (batch-check
a name across extensions). Never guess availability — always verify with tools.
## Voice & Framing
**STRICT**: Never use the word "TLD" anywhere in your output. Just write full
domain names naturally (e.g., "bouquetbay.com").
- `.com` is king — it carries instant trust. Always lead with one if available.
- Frame every recommendation around brand strength: memorable, easy to spell
on a podcast, hard to confuse.
- Never list a domain just because it's available. Only recommend names you'd
pitch in a branding meeting.
## Process
1. Read the idea. Identify brand feeling, target audience, and core keywords.
2. Brainstorm 15-20 candidate names.
3. Use `search_tlds` on your top 8-10 names to filter.
4. Use `check_domain` to verify the best 3-5 hits.
## Delivery
Deliver results via `slack_reply` using Slack mrkdwn formatting. Max 3-5
recommendations, each with a one-line brand rationale. Total output under
400 words.
## Pricing Reference
.com $9 · .io $34 · .dev $12 · .app $14 · .ai $20 · .co $12 · .net $11 · .org $10 · .xyz $15. Add Custom Tools
Create tools/check_domain.ts — a single-domain RDAP lookup:
typescript
import { tool } from "swarmlord/tool";
import { z } from "zod";
interface RdapEvent {
eventAction: string;
eventDate: string;
}
interface RdapVcard {
vcardArray?: [string, Array<[string, Record<string, string>, string, string]>];
}
interface RdapResponse {
ldhName?: string;
entities?: RdapVcard[];
events?: RdapEvent[];
}
export default tool({
description:
"Check if a specific domain name is currently registered using RDAP lookup. " +
"Returns availability status and registrar info if taken.",
input: z.object({
domain: z.string().describe("Full domain name to check, e.g. coolstartup.com"),
}),
timeout: 30_000,
retries: 1,
handler: async ({ domain }, ctx) => {
const attempt = async (retries = 2): Promise<Response> => {
const res = await fetch(`https://rdap.org/domain/${domain}`);
if (res.status === 429 && retries > 0) {
ctx.log(`Rate limited on ${domain}, waiting 2s...`);
await new Promise(r => setTimeout(r, 2000));
return attempt(retries - 1);
}
return res;
};
const res = await attempt();
if (res.status === 404) {
return JSON.stringify({ domain, available: true });
}
if (!res.ok) {
return JSON.stringify({ domain, available: "unknown", error: `RDAP returned ${res.status}` });
}
const data: RdapResponse = await res.json();
const registrarEntity = data.entities?.[0];
const registrar = registrarEntity?.vcardArray?.[1]?.[1]?.[3] ?? "unknown";
const expiration = data.events?.find(e => e.eventAction === "expiration")?.eventDate;
return JSON.stringify({
domain,
available: false,
registrar,
...(expiration ? { expires: expiration } : {}),
});
},
});Create tools/search_tlds.ts — batch-checks a base name across extensions:
typescript
import { tool } from "swarmlord/tool";
import { z } from "zod";
const DEFAULT_TLDS = ["com", "io", "dev", "app", "co", "ai", "xyz", "net", "org", "so"];
interface TldResult {
domain: string;
tld: string;
available: boolean | "unknown";
}
export default tool({
description:
"Check availability of a base domain name across multiple TLDs at once. " +
"Returns which TLD variants are available. Handles RDAP rate limiting internally.",
input: z.object({
name: z.string().describe("Base name without TLD, e.g. 'coolstartup'"),
tlds: z
.array(z.string())
.optional()
.describe("TLDs to check. Defaults to: com, io, dev, app, co, ai, xyz, net, org, so"),
}),
timeout: 45_000,
retries: 1,
handler: async ({ name, tlds }, ctx) => {
const checkTlds = tlds ?? DEFAULT_TLDS;
const results: TldResult[] = [];
const CHUNK_SIZE = 8;
const chunks: string[][] = [];
for (let i = 0; i < checkTlds.length; i += CHUNK_SIZE) {
chunks.push(checkTlds.slice(i, i + CHUNK_SIZE));
}
for (let ci = 0; ci < chunks.length; ci++) {
const batch = await Promise.all(
chunks[ci].map(async (tld): Promise<TldResult> => {
const domain = `${name}.${tld}`;
try {
const res = await fetch(`https://rdap.org/domain/${domain}`);
if (res.status === 404) return { domain, tld, available: true };
if (res.status === 429) return { domain, tld, available: "unknown" };
return { domain, tld, available: false };
} catch {
return { domain, tld, available: "unknown" };
}
})
);
results.push(...batch);
if (ci < chunks.length - 1) {
ctx.log(`Checked ${results.length}/${checkTlds.length} TLDs, pausing for rate limit...`);
await new Promise(r => setTimeout(r, 1500));
}
}
return JSON.stringify(results);
},
});6. Add Dependencies
Create tools/package.json:
json
{
"name": "domain-search-agent",
"private": true,
"type": "module",
"devDependencies": {
"swarmlord": "latest",
"zod": "^4.3.6"
}
}7. Test Locally
bash
cd domain-search
swarmlord run "Find available domains for an AI-powered recipe recommendation app"The agent will brainstorm names, batch-check them across extensions with search_tlds, verify top picks with check_domain, and print its formatted recommendations. Since there's no Slack connection in local mode, the slack_reply call will be logged but not delivered.
8. Deploy
bash
swarmlord deploy✓ Bundled domain-search (2 custom tools)
✓ domain-search deployed
Trigger: slack → #domain-search-agent9. Connect Slack
The agent listens for app_mention and message events in the #domain-search-agent channel. See the Slack trigger setup guide for creating a Slack app, configuring event subscriptions, and connecting it to your swarmlord deployment.
Once connected, mention the bot or post a message in the channel:
@domain-search An AI tool that helps indie game developers playtest and balance their games
The agent spins up a session, brainstorms names, checks availability, and replies in-thread with its recommendations.
10. Use from the SDK
You can also trigger the agent programmatically, bypassing Slack:
typescript
import { createClient } from "swarmlord";
const idea =
process.argv[2] ||
"An AI-powered recipe recommendation app that learns your taste preferences and dietary restrictions";
const client = createClient();
const session = await client.agent({ name: "domain-search", model: process.env.MODEL }).createSession({
title: `domain-search: ${idea.slice(0, 40)}`,
});
console.log(`\nSearching domains for: ${idea}\n`);
const result = await session.send(idea, {
onText: delta => process.stdout.write(delta),
});
console.log(`\n\ncost: $${result.cost?.toFixed(4) ?? "?"}`);
if (result.error) console.error(`error: ${result.error}`);
await session.end();bash
npx tsx domain-search.ts "A sustainable fashion marketplace for Gen Z"