lunartools-js
v0.2.0
Published
SDK for the LunarTools remote APIs: captcha solving, inbox OTP retrieval, and Discord webhook forwarding.
Maintainers
Readme
lunartools-js
TypeScript/JavaScript SDK for the LunarTools remote APIs: captcha solving, inbox OTP retrieval, and Discord webhook forwarding.
Requests are routed to a specific user's running LunarTools toolbox. The toolbox must be open and connected.
npm install lunartools-jsNode 18+ (uses the global fetch). On older runtimes pass your own via options.fetch.
Quick start
Create a client, then pass an API key per call. The key is created in the toolbox and identifies both the key and the toolbox it belongs to, so no Client ID is needed.
import { LunarTools } from "lunartools-js";
const client = new LunarTools();
const otp = await client.otp("lt_ik_...", {
email: "[email protected]",
site: "nike",
});
console.log(otp.otpCode, "from", otp.imapEmail);Inbox API
otp() waits for a one-time code to arrive and returns it. Only unread mail is searched, newest first, and the matched message is marked read.
const otp = await client.otp(apiKey, {
email: "[email protected]", // required - indexes the inbox
imapEmail: "[email protected]", // optional - omit to search every connected account
site: "nike", // optional - use a built-in parser
regex: undefined, // optional - your own pattern, first capture group wins
from: undefined, // optional - filter by sender
timeoutMs: 60_000, // optional - default 60s, max 120s
});Omit imapEmail and the toolbox searches every connected account, then reports which one served the code in otp.imapEmail.
With no site and no regex, the toolbox auto-detects the code (4, 6, or 8 characters), falling back to AI if the heuristic finds nothing.
Built-in sites: bestbuy, crunchyroll, disney, eql, funko, goat, nike, privacy, samsclub, target, topps, walmart, zumiez.
Counting by subject
Pass a subject to count matching mail instead of extracting a code. Useful for win trackers. Returns immediately and marks nothing read.
const { count } = await client.count(apiKey, {
email: "[email protected]",
subject: "WINNER",
});Solving API
const { token, solveMs } = await client.solve(apiKey, {
captchaType: "hcaptcha",
pageUrl: "https://example.com/checkout",
siteKey: "e94865c2-4231-4c25-9c6e-2b797b2b56cf",
proxyUrl: undefined, // optional - omit to use the harvester's own proxy
});Webhooks
Posts a Discord-shaped payload to your forwarder token, which fans out to every Discord URL configured for it. The payload mirrors Discord's schema exactly, so existing embeds can be passed through unchanged. Embeds without a timestamp get one automatically.
const res = await client.webhook("your-webhook-token", {
username: "LunarTools",
embeds: [{
title: "Checkout Success",
color: 0x5865f2,
fields: [{ name: "Site", value: "Nike", inline: true }],
}],
});
console.log(`${res.delivered} of ${res.count} delivered`);The webhook token comes from the toolbox and is not the same as an API key.
Errors
Every failure throws a LunarToolsError carrying the API's code.
import { LunarToolsError } from "lunartools-js";
try {
await client.otp(apiKey, { email: "[email protected]" });
} catch (error) {
if (error instanceof LunarToolsError) {
if (error.retryable) {
// client_offline, client_disconnected, auth_unavailable, too_many_inflight
}
console.error(error.code, error.statusCode);
}
}| Code | Meaning |
|---|---|
| invalid_key | Unknown API key for this client |
| key_disabled | The key exists but is disabled |
| client_offline | The toolbox is not connected |
| imap_not_connected | No matching IMAP account is connected |
| unsupported_site | site is not a built-in parser |
| invalid_regex | regex failed to compile |
| otp_timeout | No code arrived before the deadline |
| no_harvester | No accepting harvester of that captcha type is open |
| solve_timeout | The solve did not finish before the deadline |
| too_many_inflight | Too many concurrent requests for this client |
Options
const client = new LunarTools({
baseUrl: "https://remote.lunaraio.com",
timeoutMs: 150_000,
fetch: myFetch,
});Requests are long-polled and can take up to 120s, so keep timeoutMs above your per-request timeoutMs.
