A beacon is an AgentSpaces peer that stays up at a known address so that newcomers have somewhere to go. This project runs one. It joins your group like any other peer and publishes a signed document that lists the group and its current seeds. Agents on a Spring Boot or Micronaut host can then find the group from DNS or from a URL, and Python and TypeScript agents can read the document at its URL, with no seed list in their configuration.
You can run the beacon as a standalone program, or embed its classes in your own application to build a beacon of your own. If peer, group, seed, locator, and chain are new words, read The ideas, one at a time in the agentspaces-bootstrap README first. This README uses the same running example: Acme Corp's research-fleet.
A newcomer to a group has to dial one member before the fabric can do anything else, and the group cannot tell it which one. A beacon answers that question. It has three parts:
- It is a member of the group. It has a stable address and usually plays the
RENDEZVOUSrole, a peer that caches many members' advertisements and answers discovery queries. It answers self-introductions andGROUP_AD_WANTrequests as every member does. Nothing in the group protocol marks a peer as a beacon, so the beacon adds nothing to the wire. - It publishes the beacon document. The document is one signed JSON file at
/.well-known/agentspaces/beacon.json. For each group the beacon has joined, it lists the name, the id, the seeds to dial, the wire hints, and the multicast group when one is configured. When the group has a signed founding, the document carries that too. - It prints the DNS records that point to the document. At startup the beacon prints a zone fragment with the records for the document and for its seeds. Your DNS team registers them, and a newcomer's
dns-sdanddns-txtlocators find them.
The beacon holds no secret: the document never carries a content key, a credential, or a membership hint. The group's own admission policy decides who joins, as it does for every member. You register the DNS records the beacon prints, because the beacon does not change your DNS.
Acme runs research-fleet, a group of agents on servers, in Kubernetes, and on engineers' laptops. Acme started the group from the text research-fleet-v1, and its network domain is corp.example. Acme wants a new agent to find the fleet without anyone editing a seed list.
1. Start the beacon. On a server with a stable address, build the project, copy the dependencies beside the jar, and start BeaconMain. The jar carries no dependencies, so the start command uses a classpath:
mvn -q clean package
mvn -q -pl beacon dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/lib
java -cp 'beacon/target/agentspaces-beacon-0.4.0.jar:beacon/target/lib/*' \
ai.badmonkey.agentspaces.beacon.BeaconMain --keystore ./beacon-keys --founding research-fleet-v1 \
--name research-fleet --bind 0.0.0.0:7500 --advertise beacon.corp.example:7500 \
--http 0.0.0.0:7580 --roles rendezvous --domain corp.example \
--url https://agentspaces.corp.example/.well-known/agentspaces/beacon.jsonThe beacon starts the group from the same text as every other Acme peer, plays the rendezvous role, advertises itself as beacon.corp.example:7500, and serves the document on port 7580. The --keystore directory keeps the beacon's identity between restarts, which matters because that identity signs the document.
2. Register the DNS records. Because of --domain and --url, the beacon prints what the DNS team registers:
beacon: peer peer:zDnaCm6XCa… serving 1 group(s) at http://localhost:7580/.well-known/agentspaces/beacon.json
beacon: research-fleet aspace://zDXTnY2y... seeds 1
register in corp.example:
_agentspaces._tcp.corp.example. IN PTR research-fleet._agentspaces._tcp.corp.example.
research-fleet._agentspaces._tcp.corp.example. IN SRV 10 50 7500 beacon.corp.example.
research-fleet._agentspaces._tcp.corp.example. IN TXT "v=1" "group=research-fleet" "id=aspace://zDXTnY2y..." "scheme=tls" "url=https://agentspaces.corp.example/.well-known/agentspaces/beacon.json"
agentspaces.corp.example. IN TXT "v=1" "url=https://agentspaces.corp.example/.well-known/agentspaces/beacon.json"
(The fragment also starts with a comment line naming the zone.) The PTR record names the group's instance under _agentspaces._tcp. Each SRV record gives a seed's host and port, and the TXT record carries the group name, the id, the transport scheme, and the document's URL. The last record, at agentspaces.corp.example, points at the document alone, for networks that prefer to publish one record.
3. Serve the document over TLS. Put TLS termination in front of port 7580. The document is public data and carries no secret, but a reader trusts the hostname. This is what a reader gets:
curl -s https://agentspaces.corp.example/.well-known/agentspaces/beacon.json{
"v": 1,
"issuer": "zDnaCm6XCa...",
"issuerKey": "<base64 Ed25519 public key>",
"issued": "2026-10-21T19:35:30Z",
"ttl": 300,
"groups": [
{
"name": "research-fleet",
"id": "aspace://zDXTnY2y...",
"seeds": [
{ "transport": "tls", "address": "beacon.corp.example:7500", "priority": 0 },
{ "transport": "tls", "address": "seed-a.corp.example:7500", "priority": 1 },
{ "transport": "tls", "address": "seed-b.corp.example:7500", "priority": 2 }
],
"wire": { "batch": 1, "hashes": 1, "tree": 1 }
}
],
"signature": "<base64 Ed25519 signature over every other field>"
}The response carries Cache-Control: public, max-age=300 (the document's ttl), an ETag that answers If-None-Match with 304, and read-only CORS headers, so a browser tool can read it. The beacon answers only GET and HEAD at that path, and it limits each source address to 50 requests a second by default.
4. Point an agent at the fleet. An agent that joins research-fleet needs the group's text and a way to look for it, and no seeds:
agentspaces:
groups:
- name: research-fleet
founding: research-fleet-v1
bootstrap:
order: [ dns-sd ]summarizer starts on a laptop on Acme's Wi-Fi. DHCP gives the laptop the search domain corp.example, so the dns-sd locator lists the instances under _agentspaces._tcp.corp.example, follows the TXT record's url= to the document, and receives the seeds. summarizer dials them and joins. The agentspaces-bootstrap README describes every locator and setting.
5. The document follows the fleet. The beacon builds its seed list from its live view of the group: itself first, then the members with the rendezvous role, then the other members that listen for connections, up to --max-seeds (8 by default). When seed-a and seed-b join, the next rebuild lists them, as the document above shows. The beacon rebuilds at most once every --refresh seconds (30 by default). A member that only dials out, such as a laptop, has no address to list and never appears.
The DNS records do not follow the fleet. Registered by hand, the SRV records name the seeds that existed on the day someone typed them:
research-fleet._agentspaces._tcp.corp.example. IN SRV 10 50 7500 beacon.corp.example.
research-fleet._agentspaces._tcp.corp.example. IN SRV 20 50 7500 seed-a.corp.example.
research-fleet._agentspaces._tcp.corp.example. IN SRV 30 50 7500 seed-b.corp.example.
If your seed set changes often, register only the pointer record at agentspaces.corp.example and let the document carry the seeds. A newcomer then uses the dns-txt locator, which reads that one record and fetches the document.
An AgentSpaces agent is a peer, and every peer needs a first address to dial. A beacon moves that address out of every agent's configuration and into one place that an operator maintains.
- Agents carry a group name and a place to look. A new agent needs
founding: research-fleet-v1(or, for a group with a signed founding, its group id) and onebootstrapsetting. You deploy a hundred agents with the same small configuration, on servers, in pods, and on laptops, and none of them holds a seed list that can go stale. - The seed list follows the fleet. Kubernetes reschedules a pod and its address changes. The beacon's next document lists the new address, and newcomers receive it.
- Every language reads the same document. The
urlanddocumentlocators inagentspaces-bootstrapread it for Spring Boot and Micronaut hosts. The Python and TypeScript clients fetch it, verify its signature, and take its seeds. Their one-callbootstrap(url)joins a group whose entry carries a signed founding, and the entry of a group started from a text carries none. - Other organizations join with an id and a URL. For a group with a signed founding, a Python or TypeScript agent needs only the group id and the document's URL. A Spring Boot or Micronaut agent also lists one seed, the beacon's address, because those hosts start a
join:group only when it has a configured seed or aBeaconLocatorbean. Each agent checks the founding against the id, and pins the beacon's PeerId withbeacon-peerwhen it wants to refuse a replaced document. - A lost beacon slows newcomers and nothing else. A member that has joined learns the rest of the group by gossip and does not need the beacon. Run a second beacon for availability: it joins the same group, and each beacon's document lists the other as a seed because the other is a member with the rendezvous role.
The standalone beacon is a small program over four classes, and you can assemble the same pieces inside your own application. You start with a running PeerNode that has joined your groups, and the node's PeerIdentity. Keep that identity in a keystore, so the beacon's PeerId survives a restart. The node can be the one that runs your agents or a dedicated one. A dedicated node is usually easier to keep at a stable address.
BeaconDocumentSource source = BeaconDocumentSource.of(node, identity);
BeaconServer server = new BeaconServer(source, 50, Path.of("/var/lib/agentspaces/beacon.json"));
int port = server.start(new InetSocketAddress("0.0.0.0", 7580));
System.out.print(RecordPrinter.zone(source.get(), "corp.example",
"https://agentspaces.corp.example" + BeaconDocument.WELL_KNOWN));BeaconDocumentSource.ofbuilds a signed document from the node's groups, with a 5-minute TTL, a rebuild at most every 30 seconds, and 8 seeds per group. The full constructor sets each of those, and takes the multicast group to advertise and a list of customizers.BeaconServerserves the document on the JDK HTTP server. Its second argument is the per-source request rate, and the third is an optional file that the beacon replaces atomically each time a request, or startup, builds a changed document. Nothing refreshes the file on a timer, so an ingress or a CDN that serves only the file sees a new document after the next request to the beacon's port.RecordPrinter.zonerenders the DNS records for a document, for your DNS team or for your own DNS automation.
A BeaconDocumentCustomizer adjusts each group's entry before the beacon signs it. This one adds a region parameter that readers receive in the hint:
BeaconDocumentSource source = new BeaconDocumentSource(node, identity, InstantSource.system(),
Duration.ofMinutes(5), Duration.ofSeconds(30), 8, null,
List.of((draft, group) -> draft.parameter("region", "eu-west-1")));To serve the document through a web layer you already run, such as Spring MVC or Micronaut HTTP, skip BeaconServer and return source.get().toBytes(), the signed JSON, at BeaconDocument.WELL_KNOWN. BeaconPublisher is the interface a web layer implements to take the server's place. The Spring Boot and Micronaut adapters (agentspaces.beacon.*) are not built yet, so an application on either framework wires these classes itself.
| Option | Default | What it does |
|---|---|---|
--keystore <dir> |
none, which makes a new identity at every start | where the beacon keeps its identity keys. Always set it: the beacon's PeerId signs the document, and readers that pin the PeerId with beacon-peer refuse a beacon whose id changed |
--founding <text> |
none | starts the group from a text, as every other peer of a text group does |
--join <aspace://id> |
none | joins a group by id, which needs a seed that serves the signed founding |
--name <name> |
the founding text, else fleet |
the group's name |
--seeds a:1,b:2 |
none | addresses to dial in order to join |
--bind <host:port> |
0.0.0.0:7500 |
where the peer listens |
--advertise <host:port> |
the bind address | the address other peers dial, carried in the beacon's advertisement |
--http <host:port> |
0.0.0.0:7580 |
where the document is served |
--roles rendezvous,relay |
rendezvous |
the roles the peer plays |
--no-tls |
TLS on | turns TLS off for the peer's connections |
--membership open|invite |
open |
the group's membership policy |
--domain <zone> |
none | the DNS zone to print records for |
--url <url> |
the local server's URL | the document's public URL, printed in the records |
--write-to <file> |
none | keeps a copy of the document in this file, rewritten when a request or startup builds a changed document |
--ttl <seconds> |
300 | the document's time to live |
--refresh <seconds> |
30 | the most often the document is rebuilt |
--max-seeds <n> |
8 | the seed limit per group |
--rate <per-second> |
50 | requests per second per source address |
--multicast <host:port> |
none | turns on the multicast beacon and lists its group in the document |
ai.badmonkey.agentspaces:agentspaces-beacon:0.4.0, depending on
agentspaces-bootstrap (the document model), agentspaces-peering, and the
core starter's framework-free wiring for the standalone main.
| Class | What |
|---|---|
BeaconDocumentSource |
builds the document from a running PeerNode: one entry per joined group, the founding when the group has one, the seeds (this node first, then rendezvous members, then the rest, listening members only, capped), the node's wire hints, the multicast group; signed with the node's identity; rebuilt lazily at most once per refresh |
BeaconDocumentCustomizer |
a hook per group before signing: add a parameter, replace the seeds, or change the entry's other fields |
BeaconServer |
serves the document on the JDK HTTP server: GET and HEAD of the well-known path only, Cache-Control from the TTL, ETag with 304, read-only CORS, a per-source token bucket, an optional file copy for an ingress to serve |
BeaconPublisher |
the interface a host's own web layer implements instead of BeaconServer |
RecordPrinter |
the DNS-SD PTR, SRV, and TXT set and the WPAD-style TXT pointer, rendered as a zone fragment |
BeaconMain |
the standalone beacon |
mvn -B -ntp verify
The tests start a real peer on loopback and serve its document over HTTP; they
need no network beyond the machine. The AgentSpaces 0.4.0 artifacts, including
agentspaces-bootstrap, resolve from Maven Central, or from the local
repository after mvn install in those projects.
See CONTRIBUTING.md and the code of conduct. Report a vulnerability privately, as SECURITY.md describes.
Apache License 2.0, Copyright 2026 Bad Monkey, Inc. See LICENSE and NOTICE.