Network Trail
A guide to the social graph — paste a profile, get the structure behind it
A research platform that turns public social-network data into something you can navigate: it crawls a profile's connections through official APIs, works out which communities the graph splits into, and ranks the traits those communities share. Six services — a React front end, a Flask API, a domain layer, a distributed request worker, a FastAPI analysis service and a token pool — built over three years and now published as one repository.

One profile's friend graph after community detection. The clusters are a school, a university course, a workplace and a home town — none of it stated on the profile itself.
The problem
You can do this by hand. Open a profile, open their friend list, open thirty of those, notice that eight went to the same school, that four of those also went to the same university, that the ones who did are the ones posting from the same city. It works, and it takes an afternoon per person, which is why nobody does it except when the stakes are high enough.
NTrail was an attempt to make the machine do the tedious half: the crawl, the joins, the counting, the ranking of what is actually unusual about a set of people — and to leave the judgement to the person reading the result.
How it works
The interface is a query bar, not a form. GET vk.user <id> friends
returns a set; that set can be split into communities, intersected with another
set, or asked what its members have in common. Because every result is a set,
the result of one query is the input of the next, and the analysis is a
sequence of small steps rather than one big configured report.
Underneath, three problems shaped the architecture:
Rate limits. VK allows about three requests a second per token, and Instagram had no usable API at all. A single client is useless at graph scale, so requests became atomic units — method, token, parameters, everything needed to run standalone — and a worker takes them in batches, spreads them over cores and machines, caches every result, and returns them together.
Dirty data. Ages that are wrong on purpose, empty fields, hidden friend lists. So attributes are inferred from the neighbourhood rather than read off the profile: if forty per cent of a person’s friends are in one university’s graph, that is a stronger signal than a blank education field.
Structure over attributes. The most useful thing about a friend graph is not any single profile — it is the shape. Communities come out of Louvain modularity over the graph, and each community gets ranked by what its members share. The clusters in the screenshot above were a school, a university course, a workplace and a home town; none of them are stated anywhere on the profile.
What it took to publish it
The project lived in six repositories with submodules pointing at each other,
and in two of them a committed env_file carried a VK application secret, an
access token and a database password. Publishing it meant:
- rewriting every history to purge those values, and revoking the tokens;
- merging six repositories into one with
git filter-repo, each into its own directory, so all 470 commits survive the move; - removing the product screenshots entirely — they were captures of real profiles, with names and faces of people who never agreed to appear in someone’s portfolio. The graph above is the one artefact that shows the work without showing anybody.
What I would do differently
The domain layer and the worker were the right split, and both would survive a rewrite. The Flask backend accumulated query-planning logic that belonged in the core, and the front end talked to a hard-coded API host, which is exactly the kind of detail that stops a project from being deployable by anyone but its author. The analysis service arrived late and never fully replaced the backend it was meant to replace — the cost of building a second thing before the first one is finished.
- GET vk.user <id> friends
- The friend list of a profile, as a set you can act on
- GET vk.community <id> members
- Everyone in a group, ready to intersect with another set
- split clusters
- Break the current set into communities by graph structure
- interesting properties
- Rank what the current set has in common — school, university, city, age
The interface is a query bar, not a form. Every result is addressable, and a result can be the input of the next query.
- frontend
- React 16 + Redux; sigma / vis for the graph, recharts for the distributions
- backend
- Flask API over PostgreSQL — query objects, selective execution, caching
- core
- Domain layer: VKUser, VKCommunity — friends(), groups(), posts()
- worker
- Executes batches of atomic API requests across cores, caches results
- api
- FastAPI + Redis service layer with the analysis stack
- credentials
- The token pool the workers draw from
Six repositories, now six directories, each keeping the history it came with.
$ docker run --rm -v $PWD/frontend:/app node:16-alpine npm run buildThe build folder is ready to be deployed.$ docker compose up -d --buildContainer ntrail-db-1 StartedContainer ntrail-backend-1 Started$ curl -o /dev/null -w '%{http_code}' http://127.0.0.1:8090/api/200The MVP runs again — static bundle behind nginx, backend in a container beside it.