The Urbit meta-search engine.
About
It's time Urbit got some searching capabilities.
For most people, their computer is a bootloader to the closest search engine. Searching is a central part of everyone's computing workflow.
The %seax meta-search engine is able to aggregate results from open search APIs and bring them inside the comfort of your Urbit ship. No longer do you have to keep a second tab open.
Let's make searching fun again. :)
Contents
- Features
- Search sources
- Requirements
- Installation
- Usage
- How it works
- Ranking
- Privacy and network traffic
- Peers (disabled)
- Debugging
- Adding a search source
- Development
- Project layout
- Known issues
- History
- Credits
- License
Features
- One search box for several search engines. Your ship sends the query to all of them and shows their results on one page.
- Results appear as each source answers, without waiting for the slowest.
- A page that more than one source returns is shown once and ranked higher.
- Click a source's name to see only its results.
- Runs on your own ship at
/apps/seax/, with a tile in Landscape. Only you can search: visitors who aren't logged in are sent to the login page. - Nothing runs in the background: %seax sends requests only when you search.
Search sources
| Source | Shown as | What it is | Request your ship sends | Status (October 2026) |
|---|---|---|---|---|
| wiby | %wiby | "Search Engine for the Classic Web", an index of simple, older-style pages | GET https://wiby.me/json/?q=<query>, wiby's public JSON API | Works |
| Urbit docs | %urbit-dev | The site search of docs.urbit.org, which is built on GitBook | POST https://docs.urbit.org/~gitbook/search with {"query": "<query>", "scope": {"mode": "all"}} | Works |
| CrowdView | %crowdview | A search engine for forum discussions (Reddit, Hacker News, Stack Exchange and others) | GET https://crowdview-next-js.onrender.com/api/search-v3?query=<query>, the endpoint crowdview.ai's own page uses | Works, but has no results for many queries, such as "urbit" |
The query is URL-encoded in the GET requests, and sent as a JSON string to the Urbit docs, whose request also has a content-type: application/json header. Your ship adds no user agent, cookies or other headers: Vere sends only Host, and Content-Length when there's a body.
The page's sources row shows each source that is still loading or found something. A source that fails, or has no results, is hidden.
Until October 2026, %seax also searched Yep. By then Yep blocked requests that don't come from a browser, so it returned no results, but every search still sent it your query. Its official API needs an API key and is paid after 1,000 requests.
Requirements
- A ship running Urbit OS 408k, which needs Vere 4.4 or newer (4.6 is current). The desk declares
[%zuse 408]inzod/seax/sys.kelvin. When your ship moves to a newer Urbit OS, it suspends %seax until you install a version of the desk that declares the new one. A desk upgraded from the 2023 version is the exception; see Installation. - To build the desk: Nix with flakes enabled, on x86_64-linux, aarch64-linux or aarch64-darwin. Nix fetches Elm and the other build tools already built from cache.nixos.org, so the build only compiles the frontend and copies files.
Installation
INSTRUCTIONS.md has the full steps: on a fake ship to try %seax out, on your own ship, upgrading the 2023 version, and removing %seax. In short:
- Build the desk:
nix buildin a clone of this repository puts it inresult/. - In your ship's dojo, create and mount a desk:
|new-desk %seax, then|mount %seax. - Replace the files of the mounted desk,
<pier>/seax/, with those inresult/. Nix's files are read-only, so make the copies writable withchmod -R u+w <pier>/seax, or the next update can't replace them. - In the dojo:
|commit %seax, then|install our %seax.
If you installed the 2023 version, which was made for Urbit OS 413k, it was suspended before your ship could update to 408k. Upgrade it the same way, without |new-desk. The new agent drops the old searches, the list of peers, and the copy of your Tlon notes if the old version made one. Because the desk dates from before Urbit OS 410k, your ship counts it as essential: a later Urbit OS update waits until %seax supports it, instead of suspending %seax. Run |essential-desk %seax | to have it suspended like other apps.
To remove %seax, run |uninstall %seax, which stops it and keeps its data. |nuke %seax then deletes the data.
Usage
Open /apps/seax/ at your ship's web address, e.g. http://localhost:8080/apps/seax/, or click the %seax tile in Landscape. If you aren't logged in, you'll be asked for your +code first.
- Type a query, then press Enter or click search.
- The Sources row names each source that is still loading or has results. Sources that are loading pulse between grey and black, and can't be clicked until they finish.
- Click a source to show only its results. Click more sources to add theirs, and click a source again to remove it. With no source selected, all results are shown. The selection stays when you search again.
- Each result shows an emoji, the title and the address. The emoji is picked from a hash of the title and is only decoration. Titles are shown without HTML tags, and with HTML entities and
%escapes decoded. - Click a title to open the result in a new tab. The address under it isn't a link.
- While no source has results, the page says "No results found... :)". It also says so while the sources are still loading.
- The logo on the start page tilts as you move the mouse over it.
- The page's address doesn't change when you search, so you can't bookmark a search. The back button leaves the app, and reloading the page brings back the start page.
- The [About] link at the bottom leads to this README, and [Code] to this repository.
How it works
%seax is a Gall agent, zod/seax/app/seax.hoon, and an Elm app, frontend/. The agent serves the built Elm app from the desk's web/ directory.
sequenceDiagram
participant B as Browser (Elm app)
participant E as Eyre
participant S as %seax
participant I as Iris
participant W as wiby, Urbit docs, CrowdView
B->>E: subscribe to /search/<query> on %seax
E->>S: watch /search/<query>
S-->>B: %flush: every source is loading
S->>I: one HTTP request per source
I->>W: requests from your ship
W-->>I: responses
I-->>S: a response
S-->>B: %drain: what changed in the sources and ranked results
- Serving the app. The agent binds
/apps/seaxin Eyre and serves the files in the desk'sweb/directory to logged-in users. Everyone else is redirected to/~/login. The Landscape tile comes fromzod/seax/desk.docket-0. - Searching. The Elm app subscribes to
/search/<query>on the agent through an Eyre channel, using airlock. The agent accepts the subscription only from the ship itself. For a query that isn't open yet, it sends one HTTP request per source through Iris. Every new subscriber gets the current state right away. - Streaming. Each time a source answers, the agent parses the answer, ranks all the results so far and sends the change. The first message is the whole state (
%flush), and later ones are diffs of it (%drain), made bylib/sink.hoonandlib/noun-diff.hoon. airlock'sUr.Sub.sinkapplies them in the browser. - Sharing searches. While a query has subscribers, its results stay in the agent's state. Another tab that searches for the same query gets them right away, without new requests. Queries have to match exactly, including case and spaces. When the last subscriber of a query leaves, the agent forgets the query. A new search in the same tab leaves the old query, but closing or reloading a tab doesn't: Eyre ends the tab's subscriptions when it closes the tab's channel, 12 hours after the tab disconnects. Results aren't refreshed while a query stays open.
- Failures. A source fails when its response has no body, or the body isn't the JSON the source's parser expects. The agent doesn't look at the HTTP status. When Vere can't make a request (DNS, connection or TLS errors, or a connection that stalls for 5 minutes), Iris returns status 504 with no body, so that source fails too. Iris follows redirects with status 301, 303 and 307; other redirects make the source fail. Failed requests aren't retried.
Ranking
Each source has a weight in zod/seax/lib/engines.hoon, currently .2 (2.0) for all of them. zod/seax/lib/rank.hoon merges the results that have exactly the same URL. Each URL gets a score, which is the product of three numbers:
- the weights of the sources that returned it, multiplied together,
- the number of times it was returned,
- the sum of 1/position for each time, counting positions from 1.
Results are shown with the highest score first. For a URL that only one source returns, the score is the source's weight divided by the position: a first result scores 2, a second one 1, a third one 0.67. Since the weights are all the same, the first results of all sources come first, then their second results, and so on. Results with the same score come in no particular order.
URLs that differ only by a trailing slash or by http/https count as different URLs. A merged result keeps the title from one of its sources. See Known issues for a bug in the scores of merged results.
Privacy and network traffic
- Searches. Your query goes from your ship to wiby.me, docs.urbit.org and crowdview-next-js.onrender.com, over HTTPS. Those services see the query and your ship's IP address.
- Nothing else. %seax makes requests only when you search for a query that isn't already open. It has no timers, and it doesn't contact other ships, except once: an upgrade from an older version calls off that version's pings to its peers that are still waiting for an answer. See Peers (disabled).
- The page. It loads nothing from other sites: the fonts, scripts and images all come from your ship. airlock normally pokes ~zod when it opens a channel, and
frontend/airlock.patchremoves that poke. The bug is reported at iko-soy/airlock#15. Eyre's login page, which comes with Urbit, loads its fonts from rsms.me and storage.googleapis.com. - Stored data. Search results stay in the agent's state while their query is open, which can be up to 12 hours after you close its tab. As with everything your ship does, the requests and responses are also recorded in your pier's event log. The agent also prints your queries and their results in the dojo.
- Other ships. Only you can search. %seax refuses other ships' pokes and searches, so they can't use it through your ship.
Peers (disabled)
From August 2023 until this version, %seax kept a list of peers: other ships that might run %seax. The list started with ~zod and ~racfer-hattes. Every 30 seconds, %seax pinged its peers by subscribing to /ping on their %seax, and told them which peers had answered, so that ships running %seax could find each other.
Nothing ever used the list, and neither the code nor the commit messages say what it was for. The 2023 code didn't work well either: a peer was dropped after missing one ping, every other check emptied the list, and from October 2023 ships refused the peer lists that other ships sent them, because +on-poke couldn't read pokes from other ships.
This version disables the peer network:
- It doesn't ping other ships or send them anything, and it has no timers.
- An upgrade drops the list and calls off the pings that are still waiting for an answer.
- It refuses pings and peer lists from other ships.
Debugging
%seax prints a lot to the dojo:
- the wire of every response it gets from Arvo, e.g.
/search-result/<query>/<source>for each HTTP response, - each source's parsed results, followed by the state of every source,
- the path of every search that closes,
- the stack trace of every crash while it handles a response, such as the late answers in Known issues.
A few dojo commands help:
:seax +dbugprints the agent's state: each open query, with its number of subscribers and the results of each source. A source that is still loading has no entry,~means that it failed, and[~ ~]that it found nothing.:seax +dbug [%incoming ~]lists the subscriptions to the agent, i.e. open searches.:seax +dbug [%outgoing ~]lists the agent's own subscriptions. It should print "no matching subscriptions", also right after an upgrade from an older version.:seax +dbug [%state '~(key by search-subscriptions)']evaluates Hoon against the state, here listing the open queries.+vats %seaxshows whether the desk is running or suspended, e.g. after an Urbit OS update, and which Urbit OS version it declares in/sys/kelvin.
When Vere can't make a request, it prints http: fail (<n>, 504): <reason>, e.g. unknown node or service when a DNS lookup fails, and that source counts as failed.
Adding a search source
-
Create
zod/seax/lib/engines/<name>.hoon. It holds anenginecore:++requestbuilds the HTTP request for a query, and++resultsparses the response into a list of[title url], or~if it can't.++getinzod/seax/sur/seax.hoonmakes a GET request with the query appended to a URL. It escapes the query twice, because Vere unescapes the URL once before sending it. For example, for an API that returns[{"title": ..., "url": ...}, ...]:/- *seax ^- engine =< |% ++ request |= query=tape ^- request:http (get "https://example.com/search?q=" query) ++ results |= response=mime-data:iris ^- (unit (list search-result)) ;< jon=json _biff (de:json:html q.data.response) (apex jon) -- |% ++ apex =, dejs-soft:format (ar (ot ~[title+so url+so])) -- -
In
zod/seax/lib/engines.hoon, import it (/+ engines-<name>) and add[name=%<name> engine=engines-<name> weight=.2]to the list. The weight is a floating-point literal:.2is 2.0. -
Build and commit the desk again. Nix only sees files that git tracks, so
git addthe new file before building. The Elm app shows every source the agent has, so it doesn't need any change.
A source counts as failed if ++results returns ~ or an empty list. Note that ar from dejs-soft gives ~ if even one result doesn't parse.
Development
INSTRUCTIONS.md explains how to work on the frontend. dev/elm-live.sh serves it at http://localhost:8000 with Elm's debugger, rebuilds it when the code changes, and passes requests for /~/ on to a running ship.
The flake has these outputs:
packages.<system>.desk, the default: the desk, with the built frontend inweb/.packages.<system>.frontend: the built frontend.packages.<system>.fonts: the Roboto Light web font, converted to WOFF2 from nixpkgs' Roboto, and its license.packages.<system>.airlock-src: airlock at the commit pinned inflake.lock, withfrontend/airlock.patchapplied.devShells.<system>.default: a shell withelm0.19.2,elm-liveandelm2nix.
To change the agent, edit zod/seax, rebuild the desk and copy it into your mounted desk, as in the installation steps, then |commit %seax. The agent reloads with its state. Nix only sees files that git tracks, so git add new files before building.
Project layout
README.md, INSTRUCTIONS.md this file, and how to install and develop
LICENSE, NOTICE the MIT License, and the files from other projects
flake.nix, flake.lock the Nix build: desk, frontend, font and airlock
dev/elm-live.sh frontend development server, with a proxy to a ship
docs/screenshot.png the screenshot above
zod/seax/ the %seax desk
app/seax.hoon the agent
sur/seax.hoon the search-result and engine types, and ++get
lib/engines.hoon the sources and their weights
lib/engines/*.hoon one file per source
lib/rank.hoon merges and ranks results
lib/sink.hoon sends state as a %flush and then %drain diffs
lib/noun-diff.hoon diffs of nouns, for lib/sink.hoon
lib/dbug.hoon, lib/server.hoon from Urbit's base-dev files
lib/docket.hoon, sur/docket.hoon for the Landscape tile
lib/de-html.hoon, lib/extract-links.hoon, lib/extract-text.hoon, lib/words.hoon
HTML and text helpers from the first, crawler
version, which nothing uses now
mar/ marks for the desk's files
desk.docket-0 the tile: title, description, colour, image, URL,
version, website and license
desk.bill the agent to start: %seax
sys.kelvin [%zuse 408]
frontend/ the Elm app
src/Main.elm the whole app
src/Route.elm, src/Pages/Search.elm unused leftovers of an early version;
Pages/Search.elm doesn't compile
index.html, script.js the page, and the glue between Elm and the event stream
fetch-event-source.js event-stream client, from airlock's example
assets/ CSS, the logo and tile images, the logo tilt script
elm.json Elm 0.19.2 and its packages; airlock's code comes from airlock/src
elm-srcs.nix, registry.dat pinned Elm packages, for the Nix build
airlock.patch stops airlock from poking ~zod when it opens a channel
Known issues
The search sources:
- CrowdView often has no results, and %seax shows a source without results the same way as a failed one: it's hidden.
- One malformed result fails a whole source. If any result in a source's response doesn't parse, the whole source counts as failed.
The agent:
- Scores of merged results are wrong.
lib/rank.hoonadds the 1/position terms of a merged URL, and compares scores, as integers instead of floating-point numbers. As a result:- URLs that 2 sources return always come before URLs that only 1 returns. Among themselves, they come roughly in order of the product of their two positions, smallest first, instead of by their scores, and those whose positions multiply to 4 or less tie at the top.
- URLs that all 3 sources return come before those.
- Single-source results are ranked correctly.
- Late answers crash the agent. If you search for something else before every source has answered, the late answers to the old query crash the agent's
+on-arvo, and the dojo prints a stack trace for each one. Nothing else is affected. The agent never cancels its requests. - Restarting the ship during a search leaves sources loading. When Vere restarts, Iris cancels the requests that are still open. The agent crashes on each cancellation, and those sources keep loading until the query closes, which can be 12 hours after you close its tab.
- A source that stalls keeps loading until Vere's HTTP client gives up after 5 minutes.
- Results are never refreshed. A query that is open in any tab, or in a tab closed less than 12 hours ago, gives the results it already has.
The page:
- A
%in a title can hide it. Titles are%-decoded, and when that fails, as on a lone%, the title disappears and the result can't be clicked. Escapes that do decode are decoded even when they are part of the real title. - Narrow screens are cut off. The logo and the search box have fixed widths, so on a phone the search box and the search button are cut off, and on the start page the logo too. Titles and addresses never wrap, so long ones are cut off as well. The page can't scroll sideways.
- The source filter stays for the next search. If the selected sources have no results for the new query, the list is empty without the "No results found" message, and a selected source that is hidden can't be unselected.
- "Come back later" doesn't help. The no-results message says "We'll try and find you something. Just come back later.", but nothing searches again.
- Searching for the same query again does nothing, so the page can't refresh its results.
- Searches can't be bookmarked, because the page's address doesn't change.
- Smaller things: only the title of a result is a link, although the whole result looks clickable; the logo tilts only on the start page; the page has no favicon; the search box isn't focused when the page opens; and the page logs every event from the ship to the browser console.
History
- July 2023. iko created the repository on 23 July and started %seax on 27 July, as a web crawler with a reverse word index. On 29 July it switched to searching other search engines, with Yep, CrowdView, wiby and Alexandria as sources, and from 31 July it sent each query to all of them. Streaming results through
%sinkand noun diffs, ranking and the first Elm frontend were in place by 2 August, and filtering by source and animations by 5 August. - August 2023. iko added liveness checks between %seax ships on 9 August, and "bad peer discovery" (gossip) on 27 August.
- October 2023. Evangelos Lamprou added the logo and its tilt effect, improvements to the results page including the emoji, the Urbit developer docs as a source, and the first INSTRUCTIONS.md. He also added a
%get-notespoke that copied the ship's Tlon notebook notes into the agent's state. The notes were never searched. - Upstream. iko also contributed
%sinkand the noun-diff library to urbit/urbit in July 2023 (#6712, #6681), where they live inpkg/base-dev. - 2026. %seax was ported to Urbit OS 408k:
- It is built with a Nix flake, served from the ship, and has a Landscape tile.
- The docs.urbit.org search replaced the old urbit.org developer search.
- Alexandria, whose API is gone, and the Tlon notes were removed.
- The peer network, which nothing used, was disabled.
- Yep, which blocks requests that don't come from a browser, was dropped.
Credits
Created by ~racfer-hattes and ~dopnyr-figbud.
- Results come from wiby, the Urbit docs and CrowdView.
- The frontend is written in Elm with airlock, elm-ui and elm-animator, and includes the Roboto font.
License
MIT, see LICENSE. Files from other projects are listed in NOTICE.