Two canonical roles
assets.icon is for compact, square placements. assets.logo is the wider wordmark for headers and cards.
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.
assets.icon is for compact, square placements. assets.logo is the wider wordmark for headers and cards.
Every returned candidate includes its source, dimensions, role scores, variant metadata, and the reasons behind its score.
Ask for light or dark surfaces, color treatment, and transparent or opaque backgrounds. Matching is best-effort.
Quickstart
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.
$ npm install github:Hendrikc4/logo-yoink
import { yoink } from 'logo-yoink';
const { icon, logo } = await yoink('stripe.com');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.
First request
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"}'
const response = await fetch('http://127.0.0.1:4310/api/extract', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ website: 'stripe.com' }),
});
if (!response.ok) throw new Error(`Extraction failed: ${response.status}`);
const { assets, assetVariants } = await response.json();
console.log(assets.icon?.resolvedUrl);
console.log(assets.logo?.resolvedUrl);
/api/extractDiscovers, validates, deduplicates, and ranks logo assets for one public HTTP or HTTPS website.
websitestring · requiredA hostname or public HTTP(S) URL, up to 2,048 characters. Bare domains such as stripe.com are accepted.
preferencesobject · optionalRole-specific appearance preferences. Only icon and logo keys are accepted.
scrapersarray · optionalChoose "browser", "jina", both, or []. The HTTP default is ["browser"]; Jina is always opt-in.
wikimediaFallbackboolean · optionalDefaults to true. Set to false to disable the external Wikidata/Wikimedia identity source. Scraper selection is independent.
[]static onlyFastest and most private. Reads first-party HTML and declared assets without launching a browser.
["browser"]default everywhereUses local Playwright only when a useful role is missing. Chromium installs during setup.
["jina"]opt-inUses Jina Reader for blocked pages. The server needs JINA_API_KEY and PUBLIC_DEMO_ALLOW_JINA=1. Common bot protection may still deny access.
Request options
Icon and logo preferences use the same fields. Omitted values normalize to any.
themeany | light | darkThe surface the asset will sit on. Use dark to prefer light artwork for a dark UI.
colorany | color | white | blackThe desired artwork treatment.
backgroundany | transparent | opaqueWhether the returned file should carry transparency.
{
"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.
Response
icon / logoThe simplest top-level fields. Each is the canonical asset or null.
assetsThe same picks grouped as assets.icon and assets.logo.
assetVariantsOrdered, semantically distinct treatments. The selected asset is first.
assetFamiliesRelated delivery-size copies grouped into artwork families.
candidatesEvery validated candidate with scores and evidence.
diagnosticsDiscovery counts, scraper enabled/used state, network work, and duration.
selectedByRoleCompatibility aliases: icon, wide, and legacy favicon.
{
"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 }
}
Errors & limits
400Invalid JSON fields, preference values, URL, or an extraction that could not complete.
403A cross-site browser request was rejected by the demo origin policy.
413The JSON request body is larger than the configured limit (2 KB by default).
415The request is not application/json.
429The per-client or global rate window is exhausted. Respect Retry-After.
503All 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." }.
CLI
$ 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 | darkPreference for the logo destination surface.
--backgroundany | transparent | opaquePreference for the logo file background.
--roleicon | logoChoose which canonical winner to download. Icon-first is the default.
--downloaddirectoryWrite the selected validated asset to disk.
--no-browseropt-outDisable the default Playwright fallback and stay on the static path.
--jinaopt-inEnable Jina recovery. Requires JINA_API_KEY.
--all-fallbacksopt-inEnable browser, Jina, and deep first-party recovery together.
--deep-wideflagWhen needed, follow up to two strong first-party brand, press, or media links and inspect official ZIP kits.
--spa-bundlesflagWith deep discovery, inspect one same-origin entry bundle (up to 2.2 MB) for strong logo asset literals.
--no-wikimedia-fallbackflagDisable the default exact-domain Wikidata/Commons missing-role fallback.
Configuration
Static discoveryalways onFirst-party HTML and declared asset discovery is the base path everywhere.
BrowseronAll interfaces render missing roles by default. Use an empty scraper list or --no-browser to opt out.
JinaoffAlways request-level opt-in, even when the server has a key.
WikimediaonExact-domain missing-role recovery; it can be disabled per request.
BIMIoffExperimental and explicitly enabled.
HOST127.0.0.1Local server bind address.
PORT4310Local server port.
BROWSER_DISCOVERY1Set to 0 to disable local Chromium discovery.
JINA_API_KEYunsetMakes request-level Jina recovery available; setting it does not enable Jina.
BESTICON_URLunsetOptional URL for a self-hosted Besticon service.
PUBLIC_DEMO_ALLOW_JINA0Set to 1 to permit explicitly requested Jina calls after configuring a budget.
PUBLIC_DEMO_BROWSER1Set to 0 to disable Chromium for web/API requests.
PUBLIC_DEMO_WIKIMEDIA1Set to 0 to disable exact-domain Wikidata/Commons recovery.
DEMO_RATE_LIMIT20 / 10 minPer-client in-process request window.
DEMO_GLOBAL_RATE_LIMIT60 / minPer-instance global request window.
DEMO_MAX_CONCURRENT2Simultaneous extraction ceiling.
Logo Yoink loads .env.local automatically. Keep secrets there; it is gitignored. The checked-in .env.example contains safe local defaults.
Deployment
The repository includes a Vercel function adapter and static configuration. The same app can run as a long-lived Node process with npm start.
Set HOST=0.0.0.0 in a container or VM, expose PORT, install Chromium, and run npm start.
Import the repository. vercel.json serves the static site, maps the API function, includes serverless Chromium, and applies security headers.
Under the hood
HTML images, picture sources, safe inline SVGs, schema data, metadata, manifests, touch icons, mask icons, tiles, and favicons.
Fetch under byte and time budgets, verify actual image bytes, measure dimensions and visible content, then reject unsafe or unusable files.
Deduplicate by URL and byte hash, then group delivery-size variations of the same artwork into asset families.
Score source quality, shape, size, placement, home-link evidence, company agreement, visual treatment, and negative context separately for icon and wordmark.
Bounded browser, deep first-party, SPA-bundle, cached favicon, Besticon, optional Jina, or exact-domain Wikidata/Commons recovery can fill a missing role.
Ranking
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.
Minimum role score for an additional semantic variant.
Threshold for the high confidence band.
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.
Security
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.
Limitations
Use the canonical picks for the common path, but preserve the candidate evidence and variants when human review matters.
Ready to ride?