Logo YoinkDocs
Documentationv0.2.0

One URL in.
The right logos out.

Logo Yoink is a self-hosted API and CLI that discovers the real logo files a website exposes, validates them, and ranks the best icon and wordmark—without generating or inventing artwork.

Deterministic runtime Node.js 22+ No database MIT licensed
01

Two canonical roles

assets.icon is for compact, square placements. assets.logo is the wider wordmark for headers and cards.

02

Evidence, not guesses

Every returned candidate includes its source, dimensions, role scores, variant metadata, and the reasons behind its score.

03

Preference-aware

Ask for light or dark surfaces, color treatment, and transparent or opaque backgrounds. Matching is best-effort.

01

Quickstart

One install. One function.

You need Node.js 22 or newer. No database, account, environment file, or API key is required. Chromium installs automatically for the default browser-assisted path.

Install from GitHub
$ npm install github:Hendrikc4/logo-yoink
Use it
import { yoink } from 'logo-yoink';

const { icon, logo } = await yoink('stripe.com');

Verify your setup

npm run smoke starts an isolated local server, verifies the homepage and API validation, then shuts it down. It does not contact a third-party site. For the full suite, run npm run check.

02

First request

Extract from any public website.

In JavaScript, call yoink('stripe.com'). Over HTTP, send one JSON object to the instance you control. The hosted endpoint is intentionally rate-limited.

$ curl -sS http://127.0.0.1:4310/api/extract \
  -H 'content-type: application/json' \
  -d '{"website":"stripe.com"}'
POST/api/extract

Discovers, validates, deduplicates, and ranks logo assets for one public HTTP or HTTPS website.

Request body

websitestring · required

A hostname or public HTTP(S) URL, up to 2,048 characters. Bare domains such as stripe.com are accepted.

preferencesobject · optional

Role-specific appearance preferences. Only icon and logo keys are accepted.

scrapersarray · optional

Choose "browser", "jina", both, or []. The HTTP default is ["browser"]; Jina is always opt-in.

wikimediaFallbackboolean · optional

Defaults to true. Set to false to disable the external Wikidata/Wikimedia identity source. Scraper selection is independent.

Scraper choices

[]static only

Fastest and most private. Reads first-party HTML and declared assets without launching a browser.

["browser"]default everywhere

Uses local Playwright only when a useful role is missing. Chromium installs during setup.

["jina"]opt-in

Uses Jina Reader for blocked pages. The server needs JINA_API_KEY and PUBLIC_DEMO_ALLOW_JINA=1. Common bot protection may still deny access.

03

Request options

Choose the right treatment.

Icon and logo preferences use the same fields. Omitted values normalize to any.

themeany | light | dark

The surface the asset will sit on. Use dark to prefer light artwork for a dark UI.

colorany | color | white | black

The desired artwork treatment.

backgroundany | transparent | opaque

Whether the returned file should carry transparency.

Request with role-specific preferences
{
  "website": "stripe.com",
  "preferences": {
    "icon": { "color": "color" },
    "logo": {
      "theme": "dark",
      "color": "white",
      "background": "transparent"
    }
  }
}

Preferences are best-effort. A matching eligible asset ranks first when available; otherwise Logo Yoink returns the strongest eligible fallback.

04

Response

Canonical picks, variants, and proof.

icon / logo

The simplest top-level fields. Each is the canonical asset or null.

assets

The same picks grouped as assets.icon and assets.logo.

assetVariants

Ordered, semantically distinct treatments. The selected asset is first.

assetFamilies

Related delivery-size copies grouped into artwork families.

candidates

Every validated candidate with scores and evidence.

diagnostics

Discovery counts, scraper enabled/used state, network work, and duration.

selectedByRole

Compatibility aliases: icon, wide, and legacy favicon.

Representative response

200 OK · application/json
{
  "input": "stripe.com",
  "domain": "stripe.com",
  "homepage": "https://stripe.com/",
  "icon": null,
  "logo": { "resolvedUrl": "https://…/logo.svg", "format": "svg" },
  "preferences": {
    "icon": { "theme": "any", "color": "any", "background": "any" },
    "logo": { "theme": "dark", "color": "white", "background": "transparent" }
  },
  "assets": {
    "icon": null,
    "logo": {
      "resolvedUrl": "https://…/logo.svg",
      "dataUrl": "data:image/svg+xml;base64,…",
      "format": "svg",
      "width": 512,
      "height": 170,
      "source": "dom-img",
      "variant": { "theme": "dark", "color": "white", "background": "transparent" },
      "role_scores": { "icon": 28, "wide": 84, "favicon": 12 },
      "predicted_roles": ["wide"],
      "score_reasons": ["wide shape +30", "home linked +12"]
    }
  },
  "assetVariants": {
    "icon": [],
    "logo": [{
      "resolvedUrl": "https://…/logo.svg",
      "certainty": { "score": 84, "band": "high" }
    }]
  },
  "variantPolicy": { "minimumRoleScore": 45 },
  "assetFamilies": [],
  "candidates": [],
  "diagnostics": { "discovered": 19, "validated": 8, "browserUsed": false, "durationMs": 1240 }
}
05

Errors & limits

Predictable failure modes.

400

Invalid JSON fields, preference values, URL, or an extraction that could not complete.

403

A cross-site browser request was rejected by the demo origin policy.

413

The JSON request body is larger than the configured limit (2 KB by default).

415

The request is not application/json.

429

The per-client or global rate window is exhausted. Respect Retry-After.

503

All extraction slots are busy. Retry after the provided delay.

Successful responses expose RateLimit-Limit and RateLimit-Remaining. Errors use the stable shape { "error": "Human-readable message." }.

06

CLI

Use it without a server.

From your cloned repository: rank and print JSON
$ npm run cli -- stripe.com
$ npm run cli -- stripe.com --no-browser
$ npm run cli -- stripe.com --jina
$ npm run cli -- stripe.com --role logo --download ./downloads/stripe
--themeany | light | dark

Preference for the logo destination surface.

--backgroundany | transparent | opaque

Preference for the logo file background.

--roleicon | logo

Choose which canonical winner to download. Icon-first is the default.

--downloaddirectory

Write the selected validated asset to disk.

--no-browseropt-out

Disable the default Playwright fallback and stay on the static path.

--jinaopt-in

Enable Jina recovery. Requires JINA_API_KEY.

--all-fallbacksopt-in

Enable browser, Jina, and deep first-party recovery together.

--deep-wideflag

When needed, follow up to two strong first-party brand, press, or media links and inspect official ZIP kits.

--spa-bundlesflag

With deep discovery, inspect one same-origin entry bundle (up to 2.2 MB) for strong logo asset literals.

--no-wikimedia-fallbackflag

Disable the default exact-domain Wikidata/Commons missing-role fallback.

07

Configuration

Safe defaults, optional horsepower.

Defaults at a glance

Static discoveryalways on

First-party HTML and declared asset discovery is the base path everywhere.

Browseron

All interfaces render missing roles by default. Use an empty scraper list or --no-browser to opt out.

Jinaoff

Always request-level opt-in, even when the server has a key.

Wikimediaon

Exact-domain missing-role recovery; it can be disabled per request.

BIMIoff

Experimental and explicitly enabled.

Server environment

VariableDefaultPurpose
HOST127.0.0.1

Local server bind address.

PORT4310

Local server port.

BROWSER_DISCOVERY1

Set to 0 to disable local Chromium discovery.

JINA_API_KEYunset

Makes request-level Jina recovery available; setting it does not enable Jina.

BESTICON_URLunset

Optional URL for a self-hosted Besticon service.

PUBLIC_DEMO_ALLOW_JINA0

Set to 1 to permit explicitly requested Jina calls after configuring a budget.

PUBLIC_DEMO_BROWSER1

Set to 0 to disable Chromium for web/API requests.

PUBLIC_DEMO_WIKIMEDIA1

Set to 0 to disable exact-domain Wikidata/Commons recovery.

DEMO_RATE_LIMIT20 / 10 min

Per-client in-process request window.

DEMO_GLOBAL_RATE_LIMIT60 / min

Per-instance global request window.

DEMO_MAX_CONCURRENT2

Simultaneous extraction ceiling.

Logo Yoink loads .env.local automatically. Keep secrets there; it is gitignored. The checked-in .env.example contains safe local defaults.

08

Deployment

Host it where your app lives.

The repository includes a Vercel function adapter and static configuration. The same app can run as a long-lived Node process with npm start.

01

Node process

Set HOST=0.0.0.0 in a container or VM, expose PORT, install Chromium, and run npm start.

02

Vercel

Import the repository. vercel.json serves the static site, maps the API function, includes serverless Chromium, and applies security headers.

09

Under the hood

A bounded extraction pipeline.

1
Discover

Read the public signals.

HTML images, picture sources, safe inline SVGs, schema data, metadata, manifests, touch icons, mask icons, tiles, and favicons.

2
Validate

Prove every asset.

Fetch under byte and time budgets, verify actual image bytes, measure dimensions and visible content, then reject unsafe or unusable files.

3
Consolidate

Collapse the duplicates.

Deduplicate by URL and byte hash, then group delivery-size variations of the same artwork into asset families.

4
Rank

Select by role.

Score source quality, shape, size, placement, home-link evidence, company agreement, visual treatment, and negative context separately for icon and wordmark.

5
Recover

Escalate only when needed.

Bounded browser, deep first-party, SPA-bundle, cached favicon, Besticon, optional Jina, or exact-domain Wikidata/Commons recovery can fill a missing role.

10

Ranking

AI-labeled. Deterministic at runtime.

No model is called during extraction. AI-assisted review was used offline to label benchmark candidates and evaluate changes; the live ranker is an interpretable ruleset.

45Medium certainty

Minimum role score for an additional semantic variant.

70High certainty

Threshold for the high confidence band.

2,277Adjudicated labels

Frozen candidate judgments used to measure identity, role, and usability.

The ranker deliberately abstains when evidence is weak. A null canonical role is safer than silently returning a partner mark, product icon, UI glyph, or stale identity.

11

Security

Designed for hostile URLs.

  • ✓

    Public targets only. Private, loopback, and reserved network destinations are rejected, including after redirects.

  • ✓

    Bounded network work. HTML, images, bundles, redirects, time, and candidate counts have explicit ceilings.

  • ✓

    Validated content. A response claiming to be an image is not trusted until its bytes pass format and renderability checks.

  • ✓

    Hardened demo route. JSON-only input, origin checks, rate limits, coalescing, concurrency control, generic errors, and restrictive browser headers.

12

Limitations

Know where certainty ends.

  • Some websites block automated clients or hide assets behind interactions that a bounded pass will not perform.
  • A site may expose a product icon instead of its corporate identity.
  • Dimensions and transparent-pixel bounds cannot identify every padded or unusually composed wordmark.
  • Redirected, acquired, or rebranded domains can continue serving stale assets.

Use the canonical picks for the common path, but preserve the candidate evidence and variants when human review matters.

Pixel-art cowboy riding a horse

Ready to ride?

Self-host the whole thing.

Open source, inspectable, and yours to deploy.
Get the source