Skip to content

Commit 906fc94

Browse files
authored
Merge pull request #72 from firecrawl/nsc/agent-signups
(feat/auth) Agent Signups
2 parents 5c2ba51 + be2547b commit 906fc94

5 files changed

Lines changed: 315 additions & 2 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "firecrawl-cli",
3-
"version": "1.10.0",
3+
"version": "1.11.0",
44
"description": "Command-line interface for Firecrawl. Scrape, crawl, and extract data from any website directly from your terminal.",
55
"main": "dist/index.js",
66
"bin": {

skills/firecrawl-cli/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Must be installed and authenticated. Check with `firecrawl --status`.
2828
- **Concurrency**: Max parallel jobs. Run parallel operations up to this limit.
2929
- **Credits**: Remaining API credits. Each scrape/crawl consumes credits.
3030

31-
If not ready, see [rules/install.md](rules/install.md). For output handling guidelines, see [rules/security.md](rules/security.md).
31+
If not ready, see [rules/install.md](rules/install.md). For output handling guidelines, see [rules/security.md](rules/security.md). If the user has no account, use `firecrawl signup --email <email> --accept-terms` to create one with 50 free credits.
3232

3333
```bash
3434
firecrawl search "query" --scrape --limit 3

skills/firecrawl-cli/rules/install.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,27 @@ Ask the user how they'd like to authenticate:
5353

5454
1. **Login with browser (Recommended)** - Run `firecrawl login --browser`
5555
2. **Enter API key manually** - Run `firecrawl login --api-key "<key>"` with a key from firecrawl.dev
56+
3. **Create a new account (free credits)** - Run `firecrawl signup` to create a new account with 50 free credits
57+
58+
### Agent Signup (no account needed)
59+
60+
If the user doesn't have a Firecrawl account, an agent can create one with 50 free credits:
61+
62+
```bash
63+
# Non-interactive (recommended for agents)
64+
firecrawl signup --email user@example.com --accept-terms
65+
66+
# Interactive
67+
firecrawl signup
68+
```
69+
70+
The agent name is auto-detected from the environment (Cursor, VS Code, Claude Code, etc.). A verification email is sent so the user can confirm or revoke the key. After signup, the CLI is authenticated and ready to use immediately.
71+
72+
Use `firecrawl signup` when:
73+
- The user doesn't have a Firecrawl account
74+
- The user doesn't have an API key handy
75+
- Browser login isn't available or fails
76+
- You're setting up Firecrawl for the first time in a project
5677

5778
### Command not found
5879

src/commands/signup.ts

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/**
2+
* Agent signup command implementation
3+
* Allows AI agents to create a Firecrawl account on behalf of a user,
4+
* granting 50 free credits with a sandboxed API key.
5+
* The user receives a verification email to confirm or block the key.
6+
*/
7+
8+
import * as readline from 'readline';
9+
import { saveCredentials } from '../utils/credentials';
10+
import { updateConfig, getApiKey } from '../utils/config';
11+
import { isAuthenticated, printBanner } from '../utils/auth';
12+
13+
const DEFAULT_API_URL = 'https://api.firecrawl.dev';
14+
const TOS_URL = 'https://firecrawl.dev/terms-of-service';
15+
const CREDIT_LIMIT = 50;
16+
17+
export interface SignupOptions {
18+
email?: string;
19+
agentName?: string;
20+
acceptTerms?: boolean;
21+
apiUrl?: string;
22+
json?: boolean;
23+
}
24+
25+
interface AgentSignupResponse {
26+
success: boolean;
27+
api_key?: string;
28+
sponsor_status?: string;
29+
credit_limit?: number;
30+
credits_remaining?: number;
31+
verification_deadline_at?: string;
32+
tos_url?: string;
33+
error?: string;
34+
login_url?: string;
35+
}
36+
37+
function promptInput(question: string): Promise<string> {
38+
const rl = readline.createInterface({
39+
input: process.stdin,
40+
output: process.stdout,
41+
});
42+
43+
return new Promise((resolve) => {
44+
rl.question(question, (answer: string) => {
45+
rl.close();
46+
resolve(answer.trim());
47+
});
48+
});
49+
}
50+
51+
function detectAgentName(): string {
52+
const termProgram = process.env.TERM_PROGRAM?.toLowerCase();
53+
if (termProgram?.includes('cursor')) return 'Cursor';
54+
if (termProgram?.includes('windsurf')) return 'Windsurf';
55+
if (termProgram?.includes('vscode')) return 'VS Code';
56+
if (termProgram?.includes('zed')) return 'Zed';
57+
58+
if (process.env.CODEX_HOME) return 'Codex';
59+
if (
60+
process.env.AIDER_MODEL ||
61+
process.env.AIDER_WEAK_MODEL ||
62+
process.env.AIDER_EDITOR_MODEL
63+
)
64+
return 'Aider';
65+
if (
66+
process.env.OPENCODE_CONFIG ||
67+
process.env.OPENCODE_CONFIG_DIR ||
68+
process.env.OPENCODE_CONFIG_CONTENT
69+
)
70+
return 'OpenCode';
71+
if (
72+
process.env.GEMINI_CLI_SYSTEM_DEFAULTS_PATH ||
73+
process.env.GEMINI_CLI_SYSTEM_SETTINGS_PATH
74+
)
75+
return 'Gemini CLI';
76+
77+
return 'CLI Agent';
78+
}
79+
80+
/**
81+
* Main signup command handler
82+
*/
83+
export async function handleSignupCommand(
84+
options: SignupOptions = {}
85+
): Promise<void> {
86+
const apiUrl = options.apiUrl?.replace(/\/$/, '') || DEFAULT_API_URL;
87+
const orange = '\x1b[38;5;208m';
88+
const reset = '\x1b[0m';
89+
const dim = '\x1b[2m';
90+
const bold = '\x1b[1m';
91+
const green = '\x1b[32m';
92+
93+
if (isAuthenticated()) {
94+
const existingKey = getApiKey();
95+
console.log('You are already authenticated.');
96+
console.log(
97+
`\nAPI key: ${dim}${existingKey?.slice(0, 8)}...${existingKey?.slice(-4)}${reset}`
98+
);
99+
console.log('\nTo use a different account, run:');
100+
console.log(' firecrawl logout');
101+
console.log(' firecrawl signup');
102+
return;
103+
}
104+
105+
printBanner();
106+
107+
console.log(
108+
`${bold}Agent Signup${reset} — Create a Firecrawl account with ${orange}${CREDIT_LIMIT} free credits${reset}\n`
109+
);
110+
console.log(
111+
`${dim}A verification email will be sent so the account owner can confirm or revoke access.${reset}\n`
112+
);
113+
114+
// Get email
115+
let email = options.email;
116+
if (!email) {
117+
email = await promptInput('Email address: ');
118+
if (!email || email.length === 0) {
119+
console.error('Error: Email address is required.');
120+
process.exit(1);
121+
}
122+
}
123+
124+
// Basic email validation
125+
if (!email.includes('@') || !email.includes('.')) {
126+
console.error('Error: Invalid email address.');
127+
process.exit(1);
128+
}
129+
130+
// Get agent name
131+
let agentName = options.agentName;
132+
if (!agentName) {
133+
const detected = detectAgentName();
134+
agentName = detected;
135+
console.log(`${dim}Agent: ${detected}${reset}`);
136+
}
137+
138+
// Terms acceptance
139+
if (!options.acceptTerms) {
140+
console.log(`\n${dim}Terms of Service: ${TOS_URL}${reset}`);
141+
const acceptance = await promptInput(
142+
'Do you accept the Terms of Service? [Y/n]: '
143+
);
144+
if (acceptance.toLowerCase() === 'n' || acceptance.toLowerCase() === 'no') {
145+
console.log(
146+
'\nYou must accept the Terms of Service to create an account.'
147+
);
148+
process.exit(1);
149+
}
150+
}
151+
152+
// Call the API
153+
console.log(`\n${dim}Creating account...${reset}`);
154+
155+
try {
156+
const response = await fetch(`${apiUrl}/v2/agent-signup`, {
157+
method: 'POST',
158+
headers: {
159+
'Content-Type': 'application/json',
160+
},
161+
body: JSON.stringify({
162+
email: email.toLowerCase(),
163+
agent_name: agentName,
164+
accept_terms: true,
165+
}),
166+
});
167+
168+
const data = (await response.json()) as AgentSignupResponse;
169+
170+
if (options.json) {
171+
console.log(JSON.stringify(data, null, 2));
172+
if (data.success && data.api_key) {
173+
saveCredentials({ apiKey: data.api_key, apiUrl });
174+
updateConfig({ apiKey: data.api_key, apiUrl });
175+
}
176+
return;
177+
}
178+
179+
if (!response.ok || !data.success) {
180+
if (response.status === 409) {
181+
console.error(`\nA pending signup already exists for this email.`);
182+
console.log(
183+
`Check your inbox for the confirmation email, or log in at: ${data.login_url || 'https://firecrawl.dev/signin'}`
184+
);
185+
console.log(`\nAlternatively, log in with an existing API key:`);
186+
console.log(` firecrawl login`);
187+
process.exit(1);
188+
}
189+
if (response.status === 403) {
190+
console.error(
191+
`\n${data.error || 'This email has blocked agent signups.'}`
192+
);
193+
console.log(`\nLog in with an existing account instead:`);
194+
console.log(` firecrawl login`);
195+
process.exit(1);
196+
}
197+
if (response.status === 429) {
198+
console.error(
199+
`\n${data.error || 'Rate limit exceeded. Please try again later.'}`
200+
);
201+
process.exit(1);
202+
}
203+
console.error(`\nSignup failed: ${data.error || 'Unknown error'}`);
204+
process.exit(1);
205+
}
206+
207+
// Save credentials
208+
saveCredentials({ apiKey: data.api_key!, apiUrl });
209+
updateConfig({ apiKey: data.api_key!, apiUrl });
210+
211+
// Format deadline
212+
let deadlineStr = '';
213+
if (data.verification_deadline_at) {
214+
const deadline = new Date(data.verification_deadline_at);
215+
deadlineStr = deadline.toLocaleDateString('en-US', {
216+
weekday: 'long',
217+
year: 'numeric',
218+
month: 'long',
219+
day: 'numeric',
220+
});
221+
}
222+
223+
// Success output
224+
console.log(`\n${green}${reset} ${bold}Account created!${reset}\n`);
225+
console.log(
226+
` ${orange}${data.credits_remaining ?? CREDIT_LIMIT}${reset} free credits available`
227+
);
228+
console.log(
229+
` ${dim}API key: ${data.api_key?.slice(0, 8)}...${data.api_key?.slice(-4)}${reset}`
230+
);
231+
232+
console.log(`\n${bold}Next steps:${reset}`);
233+
console.log(
234+
` ${dim}1.${reset} A verification email was sent to ${bold}${email}${reset}`
235+
);
236+
console.log(
237+
` ${dim}2.${reset} Confirm the email to unlock your full plan`
238+
);
239+
if (deadlineStr) {
240+
console.log(
241+
` ${dim}3.${reset} Confirmation expires on ${bold}${deadlineStr}${reset}`
242+
);
243+
}
244+
245+
console.log(`\n${dim}You're ready to go! Try:${reset}`);
246+
console.log(` firecrawl scrape https://example.com`);
247+
console.log(` firecrawl search "your query"`);
248+
console.log('');
249+
} catch (error) {
250+
if (
251+
error instanceof TypeError &&
252+
(error as Error).message.includes('fetch')
253+
) {
254+
console.error(
255+
'\nError: Could not connect to Firecrawl API. Check your network connection.'
256+
);
257+
} else {
258+
console.error(
259+
'\nError:',
260+
error instanceof Error ? error.message : 'Unknown error'
261+
);
262+
}
263+
process.exit(1);
264+
}
265+
}

src/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
import { handleVersionCommand } from './commands/version';
2929
import { handleLoginCommand } from './commands/login';
3030
import { handleLogoutCommand } from './commands/logout';
31+
import { handleSignupCommand } from './commands/signup';
3132
import {
3233
handleInitCommand,
3334
scaffoldTemplate,
@@ -1108,6 +1109,32 @@ program
11081109
await handleLogoutCommand();
11091110
});
11101111

1112+
program
1113+
.command('signup')
1114+
.description(
1115+
'Create a Firecrawl account with free credits (for AI agents setting up on behalf of users)'
1116+
)
1117+
.option(
1118+
'-e, --email <email>',
1119+
'Email address for the account (skips interactive prompt)'
1120+
)
1121+
.option(
1122+
'--agent-name <name>',
1123+
'Name of the AI agent creating the account (auto-detected if not set)'
1124+
)
1125+
.option('--accept-terms', 'Accept the Terms of Service without prompting')
1126+
.option('--api-url <url>', 'API URL (default: https://api.firecrawl.dev)')
1127+
.option('--json', 'Output as JSON format', false)
1128+
.action(async (options) => {
1129+
await handleSignupCommand({
1130+
email: options.email,
1131+
agentName: options.agentName,
1132+
acceptTerms: options.acceptTerms,
1133+
apiUrl: options.apiUrl,
1134+
json: options.json,
1135+
});
1136+
});
1137+
11111138
program
11121139
.command('init')
11131140
.description(

0 commit comments

Comments
 (0)