#!/usr/bin/env python3 """CreatorAPI Connect Local ยท connect your own OnlyFans or Fansly account to CreatorAPI (https://creator-api.com) by logging in on YOUR OWN machine, on YOUR OWN IP, instead of a hosted browser somewhere else. What this does, in plain terms: 1. Opens a real Chromium window on this computer, pointed at the real onlyfans.com or fansly.com login page. You log in yourself. Your password and your own two factor code go straight to the real platform, never to this script, never to CreatorAPI. 2. Once you are logged in, it reads a small number of session cookies and localStorage values from that browser tab (the same values any browser's own DevTools would show you), the minimum needed to make authenticated API calls on your own behalf afterward. 3. It sends exactly that, plus an account label you choose, to CreatorAPI's own POST https://api.creator-api.com/v1/connect/import endpoint, using YOUR CreatorAPI API key. That is the only place this script ever sends data to. This tool needs a real, full scope CreatorAPI key to do anything useful. Without one, it can open a browser and read cookies, but the connect call simply fails. Get a key at https://creator-api.com/signup. Only two platforms are handled here: onlyfans, fansly. 4based and Fanvue already have simpler connect paths (plain login / pasted access token) built directly into https://creator-api.com/connect, no browser automation needed for those. Usage: pip install -r requirements.txt playwright install chromium python creatorapi_connect.py --platform onlyfans --creator-id my_account (or just run it with no flags and answer the prompts) Your API key can also be set once via the CREATORAPI_KEY environment variable instead of typing it every time. This script never writes your key, your password, or your captured session to disk. Nothing is cached between runs. """ from __future__ import annotations import argparse import getpass import os import re import sys import time try: import requests except ImportError: print("Missing dependency 'requests'. Run: pip install -r requirements.txt") raise SystemExit(1) try: from playwright.sync_api import sync_playwright except ImportError: print("Missing dependency 'playwright'. Run: pip install -r requirements.txt") print("Then run once: playwright install chromium") raise SystemExit(1) # Hardcoded on purpose, not a --flag: this tool only ever talks to CreatorAPI's own # API. If you forked this to point somewhere else, that is your own build, not this # one โ€” and it still needs a real backend behind it to do anything with the session # this script captures. API_BASE = "https://api.creator-api.com" LOGIN_URLS = { "onlyfans": "https://onlyfans.com/", "fansly": "https://fansly.com/", } VIEWPORT = {"width": 460, "height": 780} POLL_SECONDS = 1.5 TIMEOUT_SECONDS = 600 # 10 minutes to finish logging in, then give up cleanly CREATOR_ID_RE = re.compile(r"^[A-Za-z0-9_-]+$") # Best effort cookie/age gate dismissal, ported from CreatorAPI's own hosted connect # flow. Selector-light and wrapped in try/except, so it never breaks anything if a # platform tweaks its markup โ€” worst case it just does nothing. _GATE_JS = r""" () => { const norm = s => (s || '').trim().toLowerCase(); const wants = ['accept all','accept cookies','accept','allow all','i agree','agree', 'enter','i am 18','i am over 18','yes, enter','continue']; const roots = [document]; document.querySelectorAll('*').forEach(el => { if (el.shadowRoot) roots.push(el.shadowRoot); }); const cands = []; for (const r of roots) r.querySelectorAll("button,[role='button'],a,input[type='button'],input[type='submit']") .forEach(e => cands.push(e)); for (const el of cands) { const t = norm(el.innerText || el.textContent || el.value || el.getAttribute('aria-label')); if (!t || t.length > 24) continue; if (wants.some(w => t === w || t.startsWith(w))) { const b = el.getBoundingClientRect(); if (b.width === 0 || b.height === 0) continue; try { el.click(); } catch (e) {} } } } """ # Fansly's token/deviceId live in localStorage under a few different possible keys # depending on app version; this scans the known spots in order. Ported as-is from # CreatorAPI's own hosted connect flow (app/services/of_connect_browser.py), which # is the same passive-read approach: no CDP input simulation, no device-id minting, # just reading what the real logged-in page already put there. _FANSLY_CAPTURE_JS = r""" () => { const digits = v => (typeof v === 'string' && /^[0-9]{16,20}$/.test(v)) ? v : null; const getCookie = name => { const parts = ('; ' + document.cookie).split('; ' + name + '='); return parts.length === 2 ? decodeURIComponent(parts.pop().split(';').shift()) : null; }; let sess = null; const rawSess = localStorage.getItem('session_active_session'); if (rawSess && rawSess !== 'null' && rawSess !== '""') { try { const o = JSON.parse(rawSess); if (o && typeof o === 'object') sess = o; } catch (e) {} } let token = null; if (sess && typeof sess.token === 'string' && sess.token.length > 20) { token = sess.token; } if (!token) { const keys = []; for (let i = 0; i < localStorage.length; i++) keys.push(localStorage.key(i)); keys.sort((a, b) => (/session/i.test(b) - /session/i.test(a))); for (const k of keys) { const v = localStorage.getItem(k); if (!v || v[0] !== '{') continue; try { const o = JSON.parse(v); if (o && typeof o.token === 'string' && o.token.length > 20) { token = o.token; break; } } catch (e) {} } } let deviceId = null; if (sess && digits(sess.deviceId)) { deviceId = sess.deviceId; } if (!deviceId) { const d = digits(localStorage.getItem('device_device_id')); if (d) deviceId = d; } if (!deviceId) { for (const k of ['fansly-client-id','deviceId','device_id','fansly-device-id']) { const d = digits(localStorage.getItem(k)); if (d) { deviceId = d; break; } } } if (!deviceId) { const d = digits(getCookie('f-d')) || digits(getCookie('fansly-d')); if (d) deviceId = d; } if (!deviceId) { for (let i = 0; i < localStorage.length; i++) { const d = digits(localStorage.getItem(localStorage.key(i))); if (d) { deviceId = d; break; } } } return { token, deviceId, ua: navigator.userAgent }; } """ def _page_for(ctx, host: str): for pg in ctx.pages: try: if host in (pg.url or ""): return pg except Exception: continue return ctx.pages[0] if ctx.pages else None def _extract_onlyfans(ctx) -> dict | None: cookies = {c["name"]: c["value"] for c in ctx.cookies() if "onlyfans.com" in (c.get("domain") or "")} auth_id, sess = cookies.get("auth_id"), cookies.get("sess") if not auth_id or not sess: return None page = _page_for(ctx, "onlyfans.com") if not page: return None try: x_bc = page.evaluate("() => window.localStorage.getItem('bcTokenSha')") ua = page.evaluate("() => navigator.userAgent") except Exception: return None if not x_bc or not ua: return None return {"auth_id": auth_id, "sess": sess, "x_bc": x_bc, "user_agent": ua} def _extract_fansly(ctx) -> dict | None: page = _page_for(ctx, "fansly.com") if not page: return None try: res = page.evaluate(_FANSLY_CAPTURE_JS) except Exception: return None if not res or not res.get("token") or not res.get("deviceId"): return None return {"token": str(res["token"]), "deviceId": str(res["deviceId"])} _EXTRACTORS = {"onlyfans": _extract_onlyfans, "fansly": _extract_fansly} def prompt_platform() -> str: while True: v = input("Platform [onlyfans/fansly]: ").strip().lower() if v in _EXTRACTORS: return v print("Please type 'onlyfans' or 'fansly'.") def prompt_creator_id() -> str: while True: v = input("Account label (letters, numbers, _ and - only, your own choice): ").strip() if CREATOR_ID_RE.match(v): return v print("Only letters, numbers, underscores and hyphens are allowed.") def resolve_key(cli_key: str | None) -> str: key = cli_key or os.environ.get("CREATORAPI_KEY") if key: return key.strip() key = getpass.getpass("CreatorAPI API key (input hidden, needs full scope): ").strip() if not key: print("A CreatorAPI key is required. Get one at https://creator-api.com/signup") raise SystemExit(1) return key def connect_account(key: str, platform: str, creator_id: str) -> None: print(f"\nOpening a local browser window for {platform}. Log in yourself, " f"including your own two factor code if asked. This window is entirely " f"on your own machine โ€” nothing you type here goes to this script or to " f"CreatorAPI, only to {platform}.\n") extractor = _EXTRACTORS[platform] with sync_playwright() as pw: browser = pw.chromium.launch(headless=False) ctx = browser.new_context(viewport=VIEWPORT) page = ctx.new_page() page.goto(LOGIN_URLS[platform]) try: page.evaluate(_GATE_JS) except Exception: pass print("Waiting for you to finish logging in...") deadline = time.time() + TIMEOUT_SECONDS auth = None while time.time() < deadline: try: page.evaluate(_GATE_JS) except Exception: pass auth = extractor(ctx) if auth: break time.sleep(POLL_SECONDS) browser.close() if not auth: print("\nTimed out waiting for login to complete. Nothing was sent to " "CreatorAPI. Run the script again and try once more.") raise SystemExit(1) print("Login captured. Registering the account with CreatorAPI...") r = requests.post( f"{API_BASE}/v1/connect/import", headers={"X-Api-Key": key, "Content-Type": "application/json"}, json={"platform": platform, "creator_id": creator_id, "auth": auth}, timeout=60, ) _report(r) def _report(r: "requests.Response") -> None: if r.status_code == 200: body = r.json() print(f"\nConnected. '{body.get('creator_id')}' is live on your CreatorAPI " f"key, active={body.get('active')}. Call it now with your API key at " f"{API_BASE}/v1/{{account}}/native/{{path}}.") return try: body = r.json() detail = body.get("detail") if isinstance(body, dict) else body except Exception: detail = r.text if r.status_code == 403: print("\nThat key does not have full scope. Connecting an account needs a " "full scope CreatorAPI key, not a read only one.") elif r.status_code == 409: print("\nThat account label is already connected to a different " "CreatorAPI key. Pick a different label, or disconnect it first " "from your dashboard.") elif r.status_code == 502: print("\nThe session was captured but did not verify against the " "platform. This sometimes happens if the login was not fully " "finished. Try again.") else: print(f"\nCreatorAPI returned {r.status_code}: {detail}") raise SystemExit(1) def main() -> None: ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) ap.add_argument("--platform", choices=sorted(_EXTRACTORS), help="onlyfans or fansly") ap.add_argument("--creator-id", help="account label of your choice") ap.add_argument("--key", help="CreatorAPI API key (or set CREATORAPI_KEY env var)") args = ap.parse_args() key = resolve_key(args.key) platform = args.platform or prompt_platform() creator_id = args.creator_id or prompt_creator_id() if not CREATOR_ID_RE.match(creator_id): print("Account label must be letters, numbers, underscores and hyphens only.") raise SystemExit(1) connect_account(key, platform, creator_id) if __name__ == "__main__": main()