Check disposable domains during sign-up

Ask about a domain without sending a full address, and keep unavailable results explicitly unknown.

Local mock Version 1.0.0 · Node.js 22.23.2 / TypeScript · .md

Local fixtures, not a delivery claim.Local fixtures on Node.js 22.23.2 and contract review; authenticated sandbox and delivery not executed.

The public site does not accept credentials or send email. Authenticated use happens only in your own backend.

Before you start

  • Node.js 22.23.2; no API key or account is required.
  • Use an ASCII domain or its punycode form. The example deliberately sends only the domain, not a personal email address.

Run the local example

  1. Run npm run mock to inspect a synthetic not_listed result without any network request.
  2. Run npm test to verify domain validation and unknown handling for outages, rate limits and malformed responses.
  3. If one public API query is wanted, run npm run lookup. It queries the synthetic domain example.com and does not include an Authorization header.
  4. Use result as a list verdict. Let your application define an explicit policy for unknown; do not silently turn it into a pass or a hard block.

Expected result: The result is disposable, not_listed or unknown. not_listed says only that the exact domain is absent from the current list.

The complete files

These exact files are included in the starter. Test fixtures use synthetic addresses and mock identifiers.

README.md
README.md
# Check disposable domains during sign-up

Maintained by Oveyon. MIT licensed. Version 1.0.0.
Canonical: https://1loc.dev/recipes/disposable-domain/

Ask about a domain without sending a full address, and keep unavailable results explicitly unknown.

## Run locally

Node.js 22.23.2. No install is needed; there are no external dependencies.

```sh
npm run mock
npm test
```

These commands do not send email or make API requests. SQLite, when used, is experimental in Node 22.

## Prerequisites

- Node.js 22.23.2; no API key or account is required.
- Use an ASCII domain or its punycode form. The example deliberately sends only the domain, not a personal email address.

## Procedure

1. Run npm run mock to inspect a synthetic not_listed result without any network request.
2. Run npm test to verify domain validation and unknown handling for outages, rate limits and malformed responses.
3. If one public API query is wanted, run npm run lookup. It queries the synthetic domain example.com and does not include an Authorization header.
4. Use result as a list verdict. Let your application define an explicit policy for unknown; do not silently turn it into a pass or a hard block.

## Expected result

The result is disposable, not_listed or unknown. not_listed says only that the exact domain is absent from the current list.

## Permissions and effects

Mock mode is local. The optional lookup makes one public GET request without credentials and consumes the per-IP lookup allowance.

Optional backend command(s): npm run lookup.
Keep credentials in .env on your own backend or in a secrets manager. Never commit .env. The public site does not accept credentials.

## Failures

- 400 or an invalid domain: correct the domain format.
- 429: the public endpoint has a per-IP limit. Honor Retry-After rather than retrying a registration loop.
- 503, network timeout or malformed response: preserve unknown; do not claim the address was checked.

## Limits and verification

- Only fixtures were executed; the public lookup command has not been run as a verification claim.
- The lookup does not establish mailbox existence, deliverability, trustworthiness or inbox placement.
- Matching is for the exact domain, not every subdomain. The documented cap is 60 lookups per IP per hour; use the public list directly for bulk processing.

Local fixture tests executed on Node.js 22.23.2 on 2026-10-28. Contract snapshot reviewed; authenticated API/sandbox and real delivery were not executed.

## Sources

- https://api.oveyon.com/
- https://api.oveyon.com/v1/openapi.json
package.json
package.json
{
  "name": "1loc-disposable-domain",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "license": "MIT",
  "engines": {
    "node": ">=22.23.2 <23"
  },
  "scripts": {
    "mock": "OVEYON_MODE=mock node --env-file-if-exists=.env --experimental-strip-types disposable-domain/main.ts",
    "test": "node --experimental-strip-types --test disposable-domain/recipe.test.ts",
    "lookup": "OVEYON_MODE=public-lookup node --env-file-if-exists=.env --experimental-strip-types disposable-domain/main.ts"
  }
}
package-lock.json
package-lock.json
{
  "name": "1loc-disposable-domain",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "1loc-disposable-domain",
      "version": "1.0.0",
      "license": "MIT",
      "engines": {
        "node": ">=22.23.2 <23"
      }
    }
  }
}
.env.example
.env.example
# No credential is required. Mock is the default.
.gitignore
.gitignore
.env
.1loc-data/
*.sqlite
*.sqlite-shm
*.sqlite-wal
node_modules/
LICENSE
LICENSE
MIT License

Copyright (c) 2026 Oveyon

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
disposable-domain/main.ts
disposable-domain/main.ts
import { pathToFileURL } from 'node:url';
import { lookup } from './lookup.ts';
export async function run() {
  const mode = process.env.OVEYON_MODE ?? 'mock';
  if (!['mock', 'public-lookup'].includes(mode)) throw new Error('Use mock, or public-lookup for one explicit public API query.');
  const transport = mode === 'mock' ? async () => Response.json({ domain: 'example.com', result: 'not_listed', disposable: false }) : fetch;
  const result = await lookup('example.com', transport);
  console.log(JSON.stringify({ mode, synthetic: mode === 'mock', ...result }, null, 2));
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await run();
disposable-domain/lookup.ts
disposable-domain/lookup.ts
export type Verdict = 'disposable' | 'not_listed' | 'unknown';
export function lookupUrl(domain: string) {
  const normalized = domain.trim().toLowerCase().replace(/\.$/, '');
  if (normalized.length > 253 || !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(normalized)) throw new TypeError('Use an ASCII domain, or its punycode form.');
  const url = new URL('https://api.oveyon.com/v1/disposable');
  url.searchParams.set('domain', normalized);
  return url.href;
}
export async function lookup(domain: string, transport: typeof fetch = fetch): Promise<{ result: Verdict; httpStatus: number; reason: string }> {
  const url = lookupUrl(domain);
  let response: Response;
  try { response = await transport(url, { method: 'GET', redirect: 'error', signal: AbortSignal.timeout(5000) }); }
  catch { return { result: 'unknown', httpStatus: 0, reason: 'Lookup unavailable; do not turn an outage into a positive verdict.' }; }
  const body = await response.json().catch(() => null);
  if (response.status !== 200 || !body || !['disposable', 'not_listed', 'unknown'].includes(body.result)) return { result: 'unknown', httpStatus: response.status, reason: 'The service did not provide a usable verdict.' };
  return { result: body.result, httpStatus: response.status, reason: body.result === 'not_listed' ? 'Not on the current list; mailbox existence and deliverability were not checked.' : body.result === 'disposable' ? 'This exact domain is on the disposable-domain list.' : 'The service cannot determine the result.' };
}
disposable-domain/recipe.test.ts
disposable-domain/recipe.test.ts
import test from 'node:test';
import assert from 'node:assert/strict';
import { lookup, lookupUrl } from './lookup.ts';
test('domain query is encoded and needs no authentication header', async () => {
  assert.equal(lookupUrl(' Example.COM. '), 'https://api.oveyon.com/v1/disposable?domain=example.com');
  assert.throws(() => lookupUrl('[email protected]'));
  await lookup('example.com', async (url, init) => { assert.equal(new Headers(init?.headers).has('Authorization'), false); assert.equal(new URL(String(url)).searchParams.has('email'), false); return Response.json({ result: 'not_listed' }); });
});
test('unavailable, rate limited and malformed responses stay unknown', async () => {
  for (const status of [429, 503]) assert.equal((await lookup('example.com', async () => Response.json({ result: 'unknown', disposable: null }, { status }))).result, 'unknown');
  assert.equal((await lookup('example.com', async () => { throw new Error('offline'); })).result, 'unknown');
  assert.equal((await lookup('example.com', async () => Response.json({ result: 'trustworthy' }))).result, 'unknown');
  assert.match((await lookup('example.com', async () => Response.json({ result: 'not_listed' }))).reason, /existence and deliverability were not checked/);
});

Failures to handle

  • 400 or an invalid domain: correct the domain format.
  • 429: the public endpoint has a per-IP limit. Honor Retry-After rather than retrying a registration loop.
  • 503, network timeout or malformed response: preserve unknown; do not claim the address was checked.

Limits & operational decisions

  • Only fixtures were executed; the public lookup command has not been run as a verification claim.
  • The lookup does not establish mailbox existence, deliverability, trustworthiness or inbox placement.
  • Matching is for the exact domain, not every subdomain. The documented cap is 60 lookups per IP per hour; use the public list directly for bulk processing.

Copy for your agent

The context package contains this recipe’s complete files, prerequisites, execution effects, tests, limitations and sources. It is task information, with no hidden instructions.

Read context as Markdown

Sources & verification

Maintained by Oveyon, the provider used in these examples. This relationship is commercial and explicit. Code is MIT licensed; no independent endorsement is implied.

Updated 2026-10-28. Local fixtures on Node.js 22.23.2 and contract review; authenticated sandbox and delivery not executed.

Next step

Run the mock tests first. For an authenticated integration, create a key with the permissions stated in the recipe and verify the current contract in the official reference. An API acceptance response is not proof of inbox placement.

Type two or more characters. Try “duplicate”, “webhook” or “retry”.