Opyt

How Opyt works.

Opyt is an MCP server. It exposes twelve tools to whatever AI client you already use, and stores everything in a single SQLite database at ~/.opyt. There is no UI, no account and no daemon: nothing is installed that runs on a timer. Your client calls the tools, and the tools read and write that one file.

It talks over stdio, which means your client starts it as a local process on your own machine and pipes to it. So it works in a desktop client: Claude Code, Claude Desktop, Cursor, Windsurf. It cannot work in a browser tab at claude.ai, which has no way to start a process on your machine.

The tools.

Twelve tools, in roughly the order you meet them. Every argument, its type and its default, and what comes back.

ToolEffectWhat it does
onboardwrites · re-entrant Acquires the one key, reads your browser session, takes consent, runs the first import.
oraclewrites · spends Screens the people your own signals already point at, confirms them, ingests them.
add_oracletwo-phase · spends Adds one person by handle or URL, finds everywhere they publish, pulls the archive.
searchread-only Ranked retrieval over the store, filterable by tag, kind, source, author and date.
openread-only · free The full stored text of one atom, plus its live source URL.
aggregateread-only · free Counts across the whole store rather than a sample: by topic, author, kind, date.
sittingwrites · spends Reads every atom on one topic end to end, in publication order, through a lens.
frontierwrites · free The ranked review queue of what your standing questions found.
hoppertwo-phase · spends Takes any URL, works out what it is, and saves it as an atom.
sharetwo-phase · publishes Shows you what is in your store, then hands you a link that lets someone search it.
acceptsingle-phase · writes Registers a knowledge base somebody shared with you, from the link they sent.
unsharetwo-phase · deletes Cuts off one reader, or every reader plus the served copy.

The server owns only the last segment of each name, and a client that namespaces adds its own prefix: search reaches Claude Code as mcp__Opyt__search. Argument names are the ones below in every client. Required arguments have no default and the call fails without them; optional ones name their default. All twelve return a JSON object.

onboard

writes config idempotent human in the loop

Sets up this machine. It recomputes where you are from disk on every call, so calling it twice never repeats a finished step. It runs in up to three calls, because two steps wait on a human: OpenRouter is authorised by OAuth (a browser tab opens, you click Approve; nothing is pasted into chat), your browser session is read for X cookies on this machine only, and consent is one question with two separate commitments. Then it runs the free curation collectors and hands off to oracle.

onboard(browser_profile=None, consent=None,
        skip_github=True, guided_login=False) -> dict

Parameters

browser_profilestringoptional
Which Chrome profile holds your X session, when several are logged in. Saved to settings.yaml, so you answer once.
consentstringoptional
One of "both" · "backlog" · "refresh" · "none". backlog is the one-time X bookmark import, bounded by a $1.00/day ceiling. refresh is the recurring job that keeps your Oracles current. The recurring half can be switched off later; the one-time import cannot be un-run.
skip_githubbooleanoptionaldefault True
GitHub is optional and skipped unless you ask for it.
guided_loginbooleanoptionaldefault False
Accept the offer to have Opyt open a browser window for you to log into X, when no browser on this machine holds a session.

Returns

statusstring
"ok" · "in_progress" · "needs_choice" · "needs_consent" · "needs_login" · "awaiting_login" · "warned" · "blocked" · "error".
phasestring
Where setup stands: keysbrowserconsentcurationdone.
messagestring
Written to be read out. On a needs_* status it is the question.
candidatesinteger
How many people the free collectors surfaced for oracle to screen.
next_toolstring
"oracle" once setup is done.

Notes

  • An unfunded OpenRouter account blocks exactly like a missing key. The four curation collectors are free, but anything they collect cannot be embedded or searched, and a store you cannot query is not worth building.
  • A couple of browsers still trip a native consent prompt: Full Disk Access for Safari, Keychain for Arc. When one applies, this tool warns you in the call before the dialog appears, and that warning call reads nothing.

oracle

writes spends on ingest action-dispatched

Decides which people the knowledge base deep-ingests. Candidates are drawn only from your own signals (follows, Lists, subscriptions, bookmarks, likes), so an author you never saved or followed is never a candidate. Ranking is lexicographic: person-level acts (follow, subscribe, add to a List) outrank content-level ones (bookmark, like), and anyone with two or more independent signals arrives pre-ticked. Four actions run the whole flow in chat.

oracle(action="screen", canonical_ids=None, add_handles=None,
       top_n=30, floor=15, force=False, x_lookback=None,
       web_lookback=None, query="", min_signals=1) -> dict

Actions

actionReadsWhat it does
"screen"floor Default. The ranked candidate list, each with its reflected signal ("you follow · subscribe · bookmarked 12×"), its pre_ticked flag and its canonical_id. Also carries oracle_freshness unasked: per-Oracle, per-source last-pulled times, worst first.
"candidates"query, top_n,
min_signals
The same people ranked by what they write rather than how hard you vouched. Evidence is either probed (a ~25-post timeline sample, unvetted) or saved (a post you kept, so it is a real atom). Scores compare only within a basis.
"confirm"canonical_ids,
add_handles
Commits picks into the oracles table. Idempotent. Raw handles are resolved at confirm time.
"ingest"canonical_ids, force,
x_lookback, web_lookback
Deep-ingests each confirmed Oracle: discovery mines their blog or Substack for their other profiles, then each trusted personal profile is pulled as atoms. An org link becomes an affiliation edge instead.

Parameters

actionstringoptionaldefault "screen"
See above.
canonical_idsstring[]optional
On confirm, the candidates you kept, verbatim from screen. On ingest, which confirmed Oracles to pull; omitting it means all of them.
add_handlesstring[]optional
confirm only. Raw X @handles or Substack URLs to add beyond the ranked list.
top_nintegeroptionaldefault 30
candidates only. Not a screen knob: screen classifies and returns every candidate.
floorintegeroptionaldefault 15
screen only. Minimum candidates shown before "see all". Nothing is ever hidden, only folded.
forcebooleanoptionaldefault False
ingest only, and it means two things: ingest a source the single-author gate would skip, and re-run discovery from scratch, ignoring the trust cache.
x_lookbackstringoptionaldefault "6mo"
"6mo" · "1yr" · "2yr" · "since_last". Hard-capped at two years whatever you pass. since_last pulls only what has appeared since this Oracle's last pull, so a five-day gap costs one request instead of about nineteen, and errors rather than guessing if there was no previous pull.
web_lookbackstringoptionaldefault "all"
"1yr" · "2yr" · "5yr" · "all". The archive is free and durable, so it already pulls everything. This only narrows.
querystringoptionaldefault ""
candidates only. A topic question to rank people by ("agent memory"). Omit it for "who has been sampled".
min_signalsintegeroptionaldefault 1
candidates only. Floor on distinct curation signals; 1 is everyone.

Returns

screenobject
total_candidates, recommended_count, shown_by_default_count, floor, classify, candidates[], lookback_options, oracle_freshness, and model_routing only when a model is dead or fragile. Each candidate card carries canonical_id, name, handle, kind, is_person, corroborated, pre_ticked, shown_by_default, distinct_signals, total_count, reflected, signals, identity_links, members.
confirmobject
confirmed, unresolved (handles a lookup could not find; nothing was written for those), unknown, total_oracles.
ingestobject
ingested_oracles, lookback (the windows that actually ran, including the two-year X clamp), and results[], one per-source outcome record each.

Notes

  • The X pull is free (it goes through your own logged-in browser session), but the default window is about six months, so a user who wanted an Oracle's whole visible history gets a fraction of it and is never told. That is why the tool asks rather than guessing.
  • Non-persons are demoted to the end of the list, never hidden. Every candidate carries a kind (person, org, media, project, aggregator), so a non-person can be described rather than silently dropped.
  • classify.ran = false means the kind classifier degraded open because the model was unavailable. Everyone stays person-eligible rather than being filtered out.

add_oracle

preview is free confirm writes spends

Adds a single person as an Oracle. Opyt discovers where they actually publish (X, GitHub, a Substack, a blog found by feed or sitemap), and verifies each profile belongs to them through links they attest to themselves, so it never merges two people with the same name. Two phases: a free preview showing who it resolved and what a confirm would do, then the pull once you agree.

add_oracle(reference, confirm=False, x_lookback=None,
           web_lookback=None, extra_source_urls=None,
           force=False) -> dict

Parameters

referencestringrequired
Polymorphic: an X handle ("@karpathy"), a Substack, blog or site URL ("https://simonwillison.net"), or a canonical_id from oracle(action="screen"), to promote a below-the-cut candidate. There is no name-search endpoint: a name has to be resolved to a handle or URL first.
confirmbooleanoptionaldefault False
False previews and writes nothing. True runs the full ingest.
x_lookbackstringoptionaldefault "6mo"
"6mo" · "1yr" · "2yr" · "since_last", hard-capped at two years. since_last is the top-up for someone already on the roster.
web_lookbackstringoptionaldefault "all"
"1yr" · "2yr" · "5yr" · "all". Narrows only.
extra_source_urlsstring[]optional
Home or channel pages found by web search (see followup below). They enter as low-confidence candidates and are trust-checked like any other source, never trusted on the caller's say-so. Individual posts and videos are dropped; send homes.
forcebooleanoptionaldefault False
Re-runs discovery from scratch, ignoring the cache. The cache key is the person's display name plus their declared links, so a source they created after the last run leaves that key identical and only this gets past it. Costs a full probe walk.

Returns

previewobject
confirm_required, reference, lookback, on_confirm, mode ("new" or "existing") and resolved: name, bio, followers, site for an X handle; the roster entry for someone already known. A reference that does not resolve comes back as unresolved with nothing to confirm.
confirmobject
added (canonical_id, name, source, was_already_oracle), lookback, ingest, and followup when the open-web step is worth running.
ingestobject
results[] plus counters: ingested, blocked, errors, atoms_added, discovery_ran_fresh, stage_seconds.

Notes

  • Two windows, asked separately, because they answer to different constraints. A single shared window would be wrong for one of them by construction: it either truncates the durable archive or over-pulls the ephemeral stream.
  • ingested is not the whole story. blocked counts sources where the host stopped the pull (Cloudflare, a truncated archive), and those wrote nothing and retry on the next run. atoms_added and dispatched diverge whenever posts are paywalled or fail the quality gate: the lookback bounds posts attempted, not atoms saved.
  • Four of the five discovery probes are deterministic: X bio, Substack convention, GitHub, Semantic Scholar. The fifth, the open web, is handed back to the calling model as followup, because it already has web search and Opyt would otherwise pay a second model for a worse version of it.

open

read-only idempotent free · no network

Follows an atom's pointer and returns its real stored snapshot text plus the live source URL, so a claim can always be traced back to where it was published. search routes you to an atom; this is the ground truth to reason from.

open(atom_id, kb=None) -> dict

Parameters

atom_idstringrequired
From a hit card, an aggregate row, or a prior open. An unknown id returns {"error": "not found"} rather than raising.
kbstringoptional
Must be whatever the hit card carried. An atom id is scoped to one knowledge base: the same tweet in two people's stores is one id in each, so opening a foreign id without its kb either finds nothing or hands back your own copy of the same source.

Returns

rawstring | null
The stored snapshot. This is what a claim is asserted from. raw_available says whether it is there at all.
source_urlstring
The live pointer. Re-fetch it for anything fresher than the snapshot.
body_statestring
"complete" · "partial" · "absent" · "pending", with body_basis saying how it was determined.
raw_pathstring | null
A path on this machine, so it is null for an atom read out of a shared knowledge base. raw carries the body either way.
also
atom_id, kb, source_type, what_kind, who_id, when_ts, when_precision, description, payload.

Notes

  • raw is only as complete as body_state says. On partial you hold a knowing fragment, so quoting it as the full article invents a citation; attribute what is there and send the reader to source_url.

aggregate

read-only idempotent free · pure SQL

Counts across the entire store rather than a sample. This is what answers distributional questions: what you saved most of, when your interests moved, how much of the store comes from people you confirmed. Scope is optional: omit everything for the whole store.

aggregate(tags=None, what_kind=None, source_type=None,
          who_id=None, date_from=None, date_to=None,
          kb=None) -> dict

Parameters

tagsstring[]optional
Topic slugs, matched as in search.
what_kindstringoptional
"opinion" or "artifact".
source_typestringoptional
"x" · "github" · "substack" · "blog" · "paper".
who_idstring | string[]optional
Ids, not handles. To scope to a person, call search(who="@handle") first and pass its insights.resolved_who[].who_ids here: resolution has one home rather than two that can drift.
date_fromstringoptional
Earliest date, inclusive. Same formats and same widening as search; any other shape raises.
date_tostringoptional
Latest date, inclusive.
kbstringoptional
Summarize a shared knowledge base instead of your own. trusted_atoms then counts atoms whose author that owner confirmed, not you.

Returns

totalinteger
Atoms in scope.
by_what_kindobject
Counts keyed by kind.
by_source_typeobject
Counts keyed by source.
trusted_atomsinteger
Atoms whose author is a confirmed Oracle.
top_topicsobject[]
Up to 15, as {topic, count}.
top_entitiesobject[]
Up to 15, as {who_id, count}.
recent_descriptionsobject[]
The 12 newest, as {atom_id, description, who_id, when_ts}. These are mechanical descriptions, so they can be read without opening, but a claim still needs open.
also
scope as applied, kb, and notices in the same shape search uses, so one channel serves both.

Notes

  • Counts here are a plain filter result: an undated atom is excluded by either bound, and a year-only atom counts if its year overlaps the window. Unlike search, this tool does not report how many undated atoms that dropped.

sitting

writes standing queries spends on read action-dispatched

Reads every atom on one topic end to end, in publication order. It is the opposite of search: search finds the few best-matching items, this assembles every item on a topic and reads the whole set in date order. Search answers "where is it"; this answers "what happened". A topic too big for one sitting is read in parts, oldest stretch first, each part carrying forward the claims the earlier ones established and being asked to confirm, revise or refute them.

sitting(action="preview", query=None, sitting_id=None, atom_ids=None,
        floor=None, budget_tokens=None, lens=None, claim=None,
        add=None, drop=None) -> dict

Actions

actionCostWhat it does
"preview"one embedding Default. Names a topic and reports what is actually there: the region's size, the stretch of time it covers, how many people wrote it, a few items by name, and anything about its shape that would make a read disappointing. Calls no model. Not a permission step: it exists because a phrase can resolve to four items or two hundred and nobody can tell which in advance.
"read"spends Reads the assembled region with a model, under one of two lenses. Lens queries (the default) emits the standing research questions that drive frontier. Lens claims extracts 8–15 falsifiable claims instead, each naming specifics, citing the atoms that support it, and stating what would prove it wrong.
"render"free Hands back a region that was already built, as the document a reader would see. Nothing is re-grown and nothing is re-read.
"lens"only on new material Hands back an instruction and a document for the calling model to read in the conversation and answer from. Each stretch is summarized once ever, so asking the same lens again, or a different question of the same lens, is free.
"watchlist"free The standing questions currently being watched, with how often each runs, how many times it has come up, and whether it was typed or proposed by a read.

Parameters

actionstringoptionaldefault "preview"
See above.
querystringoptional
The topic, in your own words. It does not have to be wording that appears in the material: the match is by meaning, so "prediction markets" finds a thread nobody in the corpus ever called that. If nothing is close enough it says so rather than assembling something plausible out of near-misses.
sitting_idstringoptional
An id from an earlier preview. Required for render; on read and lens it is the alternative to query.
atom_idsstring[]optional
Seed the region from specific items instead of a phrase. Free, no embedding.
floorfloatoptionalunset by default
How tightly related an item must be to join the region, 0–1. Higher is narrower, and a value below the corpus' measured noise ceiling is raised to it. Two builds of one topic at different floors are two different regions with separate read histories.
budget_tokensintegeroptionalunset by default
Cap on how much is read in one sitting.
lensstringoptional
On read: "queries" (default) or "claims". On lens: "briefing" (what the material says, as knowledge) · "trajectory" (how the thinking moved: what changed, reversed, got abandoned) · "disconfirmation" (what here would undermine a belief) · "gaps" (answer using only this material, and say how close it comes if nothing does) · "sprouts" (everything no sitting has ever read; needs no region at all). On preview, which lens's warnings to compute.
claimstringoptional
The belief or question being tested, for lens="disconfirmation" or lens="gaps". Without one, disconfirmation red-teams the material's own apparent thesis.
addstring[]optional
watchlist only. Questions to start watching. These never decay and are removed only by drop.
dropstring[]optional
watchlist only. Retires a question everywhere: the list is one list of questions, not a copy per region.

Returns

statusstring
"preview" · "ok" · "skipped" (with a reason, most often already read) · "failed" · "error". A skipped or failed read wrote no queries and left the region unread, so it can be retried once the reason is fixed.
warningsobject[]
On a preview. They say when a region is a poor fit for the question: a region spanning three days has no arc to find; a region that is 85% one author generates queries pointing back at that author's own work. Advisory: nothing here refuses to read.
consensusobject
From read with lens queries: how the conversation moved, what reversed, what is unresolved, alongside the queries it emitted.
claimsobject[]
From read with lens claims. Each is {claim, falsified_by, atom_ids}.
instruction
document
string
From lens. The document is not raw text: a topic read across several sittings comes back as those per-stretch summaries, labeled with the dates they cover.
schedulerobject
Present only when the rail that drains the read queue needs a human: it has never run, or it stopped after repeated failures. Its absence is the normal case.

Notes

  • A region is read once per lens. A second read of the same region with the same lens is refused as already read: it would be the same input for the same money. The two read lenses do not share that guard: a region read for queries can still be read for claims.
  • A preview alone queues nothing. Consumption subscribes a region; construction does not. Nothing reads and nothing spends until a read or lens actually consumes it.
  • The answer a lens produces is never written anywhere (no queries, no table, no record), because it is about the topic as it stands today and would be wrong the moment anything is added.

frontier

writes · free not idempotent

The review queue for everything the standing questions found: arXiv preprints, GitHub repos, and published literature across every discipline via OpenAlex. Ranked by convergence: how many of your independent standing questions turned up the same artifact. The ranking is recomputed on every call and never stored, so it measures your own interests overlapping rather than anyone's popularity. Nothing is ever pushed into your context, and nothing is filtered away.

frontier(limit=20, dismiss=None, include_dismissed=True) -> dict

Parameters

limitintegeroptionaldefault 20
How many cards to return. There is no cursor; see the note below.
dismissstring[]optional
candidate_ids to stop surfacing. Dismissals are written before the ranking runs, so anything dismissed in a call comes back in that same response labeled dismissed rather than vanishing between request and answer.
include_dismissedbooleanoptionaldefault True
Passing False hides dismissed candidates and reports how many it hid, under hidden_by_include_dismissed.

Returns

candidatesobject[]
Each card carries candidate_id, source, title, url, published, summary, payload, state, shown_before, queries (how many standing questions found it), score, and why, the ordering, explained.
statestring
Per card: "new" · "seen" · "dismissed" · "materialized" (stage 3 put it in the knowledge base) · "rejected" (the fetch mechanically failed, never a quality judgment; stage 3 has no judge).
duplicate_ofstring[]
Present only when a card absorbed others. One artifact is one card even when several sources staged it under different ids: the same preprint reached by DOI and by its abstract page. It is a merge, so no signal is lost.
showing
total
remaining
integer
Delivered, ranked, and left below the cut.
notestring
Present only when nothing is staged. That is not an error and there is nothing to fix: stage 2 has either not run yet or ran and found nothing new.

Notes

  • Calling it again advances the queue. The tool records what it showed, and being shown demotes, so a second call surfaces the next batch rather than re-pitching the same head. That is why there is no cursor. Nothing gets stranded: an unseen candidate carries no penalty at all, so it outranks everything already shown.
  • Every term demotes; none excludes. A dismissed candidate still comes back, ranked last and labeled.
  • There is no save step here. Admission into the knowledge base is Frontier stage 3's job, it is autonomous, and it runs on its own schedule with nothing to call.

hopper

preview is free confirm writes idempotent

Takes any URL (a paper, repo, Substack post, article or single X post), detects what it is, routes it to the right ingester, and shows you what it found before writing anything. This is the only manual save path; there is no separate save-a-paper or save-a-repo tool. Every atom it stores is stamped entry_mode="user-saved", the same mark an X bookmark gets, and that is load-bearing downstream: hand-saved items steer the standing research queries.

hopper(reference, confirm=False, kind_hint=None) -> dict

Parameters

referencestringrequired
The URL to save: an article, a paper (arXiv, DOI or .pdf), a GitHub repo, a Substack post, or an x.com status link. Anything that is not an http(s) URL comes back unroutable with nothing written, rather than being filed somewhere plausible.
confirmbooleanoptionaldefault False
False previews: no fetch, no writes. True runs the ingest.
kind_hintstringoptional
Consulted only when the host matches nothing known: a recognized host always wins, because the host is a fact. "substack" is the one value worth passing: a Substack post on a custom domain cannot be detected without fetching, and routed as a plain article it gets a different atom id and never dedupes against the same post saved from a bookmark. "paper", "github" and "x" cannot override anything: those adapters check the host themselves.

Returns

statusstring
"preview" · "saved" · "already_present" · "rejected" (the page was nav or promo boilerplate, not an error) · "blocked" (a bot-check; retryable) · "failed" · "unroutable" · "budget_paused". Every non-saved status wrote nothing.
previewobject
routable, kind, why (what the route was decided from), atom_id, already_present, entry_mode, cost, and a note for the two kinds whose id cannot be predicted offline.
detailstring
On rejected, blocked and failed: what actually happened, and whether it is worth retrying.
warningstring
On a saved result, marks a degraded success: most often a paper whose full text landed but whose metadata lookup was throttled, so it is stored with no title, date or author. The atom is searchable by its body, but re-saving will not repair it.

Notes

  • The preview fetches nothing for an article, paper, repo or Substack post. The one exception is an x.com status: the post is read (about $0.00015) and returned as a description, because a bare id like x:2086520133909168332 is unverifiable by a human and that description is the only way to catch a wrong link.
  • It stores public content only. A paywalled Substack post is skipped by the adapter and comes back failed: the same cookie-less public endpoint a reader hits, so it cannot see past a wall either.
  • Repeat calls are idempotent: an unchanged item is never re-fetched or re-embedded, and already_present in a preview means a confirm is a no-op.
  • budget_paused can come back from either phase. It means the daily runaway guard tripped: that much spend in one day means something is looping. Nothing was fetched and nothing was written. It resets at UTC midnight.
  • It never adds a person to the tracked roster. Saving someone's article does not start following them; add_oracle is the only way in.

share

preview is free confirm publishes returns a link

Hands you a link to send someone. They open it, and from then on their AI client can search everything you have read, attributed to you. The preview is the consent step and it carries the whole picture (how many atoms, whose, from which sources, over what date span), because a summary you have to ask for separately is one that gets skipped. Sharing is the whole knowledge base, standing: there is no way to share a slice, and access lasts until you revoke it.

share(confirm=False, as_name=None, for_whom=None) -> dict

Parameters

confirmbooleanoptionaldefault False
False previews: nothing is uploaded, nothing is minted, nothing is sent. True registers this install if it is not registered yet, starts the upload, mints a one-time invite and returns the link.
as_namestringoptional
What to call this knowledge base's owner: your name or handle. Required on the first share and ignored after, because everyone you share with sees it as the name to search under. It is a suggestion rather than a claim: two people may pick the same one, and a recipient who already knows another alex gets alex-2.
for_whomstringoptional
A label for who this invite is for. It is the handle that makes them nameable afterwards: unshare(reader="Leo") cuts off exactly them. Skip it and they can only be named by a token id.

Returns

statusstring
"preview" · "shared" · "empty" (nothing in the knowledge base to share yet) · "needs_name" (a first share with no as_name) · "register_failed" · "grant_failed". All but "grant_failed" left this install exactly as they found it. "grant_failed" on a first share means the registration already succeeded: the owner token is saved and the routing key is claimed, so retrying mints the invite without registering again.
invitestring
On a confirm: the link, like https://useopyt.com/invite#…. Send it however you like. The code is in the URL fragment, which a browser never transmits, so no server ever sees it.
consentlist
On a preview: the sentences describing what sharing means, meant to be read back before confirming.
also
On a preview, the shape of the store: atoms, by_source_type, by_what_kind, top_entities, top_topics, trusted_atoms, date_span, plus already_shared and already_published. On a confirm, owner, the routing key this knowledge base is served under, and publishing, whether the background upload started.

Notes

  • Publishing is self-service. No invitation, no waiting list, no account. The first confirm registers you and assigns an opaque routing key that lives in a URL and a filename and nowhere a person reads.
  • The first share uploads the whole corpus, which runs in the background and usually takes a minute or two. The link works immediately: a reader who accepts inside that window is registered normally, and it is their first search that comes back saying the copy is still arriving. The next one works. Later shares are instant.
  • The served copy keeps itself current. It refreshes when somebody has read it since your last push and your store has changed since then. Both, so an unchanged store never re-uploads and a knowledge base nobody reads costs nothing. The refresh lands one session behind the reading that triggered it.
  • It publishes nothing to the open web. The service serves only people holding a link, one reader token each, revocable.
  • An invite is not a standing credential: it buys exactly one reader token and then it is spent, so what sits in that chat window afterwards opens nothing.

accept

single-phase read-only access

Takes an invite somebody sent you and registers their knowledge base locally. After one call, search, open and aggregate accept kb="their-name". There is deliberately no preview: a grant code buys one reader token and then dies, so checking it would spend it, and pasting the invite is already the yes.

accept(invite, name=None) -> dict

Parameters

invitestringrequired
The invite link, the fragment, or the bare code. All three work: the code is found inside whatever was pasted.
namestringoptional
What kb= should call this knowledge base on this machine. Omitted, the owner's own name is used.

Returns

statusstring
"accepted" · "code_unavailable" (already used, or not one this service issued) · "not_an_invite". Both failures registered nothing.
kbstring
The name to pass as kb=. Use this string: a name already taken on this install is suffixed rather than overwritten, because the registry holds the only copy of each reader token in existence and replacing one destroys access nothing can restore.

Notes

  • It goes one way. Accepting gives you read access and gives them nothing. After your first search of theirs that finds something, Opyt offers once to share yours back, and never raises it again for that person. A second person's knowledge base gets its own single offer.
  • Read access is all it is. The service exposes three read endpoints and your token carries the reader role, so there is no call this install can make that writes to their knowledge base.
  • What the service records is that you read, never what you searched for. There is no query column in its schema.
  • You configure nothing. Your install already knows the service address, and an invite to somebody's self-hosted one carries its own.

unshare

preview is free confirm deletes

Stops sharing, with one person or with everybody. reader picks which, and the two are not the same act. Name a reader and they lose access while the copy stays up and everyone else keeps reading. Leave it out and every reader is cut off and the served copy is deleted, which is one act rather than two because a person who says "stop sharing my knowledge base" means both halves and will not say it twice.

unshare(confirm=False, reader=None) -> dict

Parameters

confirmbooleanoptionaldefault False
False previews what this scope would do, and changes nothing. True does it.
readerstringoptional
Who to cut off, matched against the label you gave at share time as for_whom, case-insensitively. A token id from an earlier return also works, which is how you name somebody you invited without a label. Omitted means everybody, so it is required whenever the user named a person.

Returns

statusstring
"preview" · "unshared" · "reader_revoked" · "already_gone" · "no_such_reader" · "ambiguous_reader" · "not_shared" · "unreachable" · "failed". The three reader statuses revoked nobody.
scopestring
On a preview: "one_reader" or "everyone". Read it before confirming; it is the field that says which of the two acts you are looking at.
readers_revokedinteger
On a whole-knowledge-base confirm: how many lost access. Effective on their very next request: there is no refresh cycle and no window.
also
On a whole-knowledge-base preview, readers, how many hold access now, and reader_labels, whatever for_whom called them; on its confirm, export_deleted. On anything scoped to a reader, reader_label and readers_remaining. When a name does not resolve, readers is the roster to pick from, each with a label and an id.

Notes

  • One reader is cheap to undo; everybody is not. Letting one person back in is another share link. Taking the whole knowledge base down kills every link ever sent, so everyone has to be re-invited by hand.
  • A name that matches two readers refuses rather than picking one, and hands back both with their ids. The wrong guess is somebody's access, and from here the two are indistinguishable.
  • Readers are cut off before the file is removed, so a failure in the middle leaves people already revoked and a stray file a retry clears, never live access to a knowledge base that is half gone.
  • Your own ability to publish survives, so re-sharing is one call and not a re-setup.

Where content comes from.

Six curation feeds are read from the browser session you are already signed into, with no API key and no password. Everything else is public or handed over by you.

Xbookmarks · likes · follows · Listsyour browser session
Substacksubscriptions · saved posts, full textyour browser session
GitHuba tracked person's repos and READMEspublic
Blogsa tracked person's whole archive, found by feed or sitemappublic
arXiv · OpenAlexpapers matching your standing questionspublic
Any URLwhatever you hand to hopperyou

Opyt only reads. It never posts, follows, likes, or changes anything on any account. Opening a session is what starts a pull: the server fires each feed as a one-pass job that outlives the session, so a long pull finishes after you close the client and a session opened minutes ago starts nothing new. Nothing runs on a timer, so the store grows on the days you open a session and not on the days you don't.

The growth loop.

This is the part that runs without you. Reading a topic is what creates the watchers for it.

  1. A full read emits standing questions. Reading a topic end to end with sitting raises questions, and those become durable queries. A question's identity is a hash of its normalized text, so asking the same thing twice does not create a second watcher.
  2. They come due on a cadence against arXiv, GitHub and OpenAlex, and whatever is due runs the next time you open a session. A query that keeps finding nothing decays rather than dying, going daily to weekly to monthly. Nothing is ever deleted automatically; only you retire a question.
  3. Finds are kept and queued, never pushed into your context.
  4. You review them in frontier when you feel like it, ranked by how many of your own questions independently converged on the same thing.

Sharing.

You say "share my knowledge base" and get a link. They paste it. Their next question can draw on everything you have read. Nobody opens a terminal, and the copy keeps itself current.

  1. You share. share shows you what is in there first (how many atoms, whose, over what span), then registers you, starts the upload, and hands back a link. Publishing is self-service: no invitation, no account, no waiting. The first upload runs in the background and takes a minute or two; the link works immediately.
  2. They accept. The link opens a page with one line to paste into their client, and the command to install Opyt first if they do not have it. accept registers your knowledge base on their machine, and from then on their agent passes kb="your-name" to search, open and aggregate. Those three over HTTPS, plus the lookup that tells their install which embedding model to match. No upload, no grant, no write of any kind.
  3. It stays current on its own. The served copy refreshes when somebody has read it since your last push and your store has changed since then. Both, so an unchanged store never re-uploads and a knowledge base nobody reads costs nothing. The accepted cost, stated plainly: the refresh lands one session behind, so the first reader after a change sees the previous version.
  4. You stop whenever, with one person or with everyone. unshare(reader="Leo") cuts off Leo and leaves the copy serving for everybody else. unshare() cuts off every reader and deletes the copy from the service, in one act. Either way it takes effect on that reader's very next request: there is no refresh cycle and no window.

Every result from a shared knowledge base is labeled with whose it is, so an answer built from a friend's research says so. It goes one way: accepting gives you read access and gives them nothing, and Opyt offers once, after your first search that finds something, to share yours back.

The invite code travels in the URL fragment, which a browser never transmits, so no server ever sees it. The name a reader searches under is theirs to pick and never has to be unique; the address in the URL is an assigned key nobody types. What the service records is that a read happened, never what was searched for. TELEMETRY.md is the full schema, checked against the code by a test.

What it costs.

Opyt is free and MIT-licensed. The reading and reasoning run on the AI subscription you already pay for. One key covers everything else.

Your AI clientevery question, every answer, all the reasoningyour existing subscription
OpenRoutersorting, extracting, and embedding what comes in, on a cheap open modelpay-as-you-go credits
Xposts, timelines, profiles, threadsfree, through your own browser
Everything elsepapers, blogs, GitHub, Substack, any URL you hand itfree

Anything that costs money shows you a preview and waits for a yes. A call that fails writes nothing and is not marked done, so a failure never leaves a partial record behind.

useopyt.comwhat Opyt is for
use casesnine questions it answers, with the tools that answer them
Opyt vs the alternativeshow it compares to read-it-later apps, notebooks, agent memory and Grok
github.com/maimond123/Opytsource, issues, MIT license
hello@useopyt.comquestions and feedback
© 2026 Opyt Built with ❤️ in New Jersey MIT Use cases Compare Docs GitHub