Pulsar node protocol (v0.4) — build your own agent
An agent is any program that pairs once, then loops: heartbeat → next → do the task → submit. All calls are POST https://astrina.io/api/v1/node/<endpoint> with Content-Type: application/json. After pairing, send the device secret as header X-Node-Key: pn_....
1. register (once, exchanges a pairing code for a secret)
POST /api/v1/node/register
{ "pair_code":"ABC123XYZ0", "kind":"cli", "label":"my-agent", "consent":1,
"ver":"0.3.0", "os":"Linux",
"caps":{"uptime":1,"perf":1,"ssl":1,"content":1,"keyword":1,"dns":1,"api":1,
"infer":1,"mixed":1,"redirect":1} }
→ { "ok":true, "device_id":42, "secret":"pn_…" } # secret shown ONCE
consent:1 is REQUIRED (the contributor accepted /pulsar/terms). caps declares which task kinds you can run — you only ever get those.
2. heartbeat (liveness + version gate)
POST /api/v1/node/heartbeat { "ver":"0.3.0", "os":"Linux" }
→ { "ok":true } # or { "pause":true } or { "min_version":"0.3.0" }
3. next (claim work)
POST /api/v1/node/next { "want":5, "pubkey":"<64-hex, optional>" }
→ { "ok":true, "tasks":[ { "assign_id":123, "nonce":"<32-hex>", "kind":"uptime.probe",
"payload":{"url":"https://…"} } ],
"next_poll":15 } # honor next_poll (seconds) as your poll interval
`nonce` (v0.4) is minted per assignment and is unpredictable — see signing below.
`pubkey` here is adopted ONCE, and only if the server has no key for this device yet: a device
enrolled before signing existed can start signing without re-pairing. Replacing an existing key
still requires `rotate-key`, where the new key is signed by the old one.
4. submit (return results)
POST /api/v1/node/submit
{ "results":[ { "assign_id":123, "result":{ "ok":true, "code":200, "ttfb_ms":80, "total_ms":140 },
"ms":150 } ] }
→ { "ok":true, "accepted":[123] }
Task kinds and the result shape each must return
uptime.probe/perf.measure→{ok,code,ttfb_ms,total_ms,bytes,tls_days}ssl.audit→{ok,tls_days,hsts,csp,xfo}(https only)content.watch→{ok,code,hash}(16-hex sha256 of normalized text; may addbodywhen hash≠payload.known_hash)keyword.check→{ok,found}·api.check→{ok,pass}·dns.check→{ok,ips:[…]}link.audit→{ok,code,found,follow,visible,indexable}— payload{url,target}: fetchurl, report whether an anchor to domaintargetis present, carries no nofollow/sponsored/ugc rel, is not hidden (inline style / hidden attr / an unclosed hidden container), and whether the page is indexable (no meta-robots/X-Robots-Tag noindex). Booleans only — the quorum compares them byte-for-byte.port.check→{ok,open}·infer.embed→{ok,sim,lang}compress.audit→{ok,code,raw_bytes,best_bytes,gzip,br,zstd,savings_pct}ipv6.parity→{ok,v4,v6}·mixed.scan→{ok,code,mixed}·redirect.chain→{ok,hops,loop}robots.diff→{ok,code,robots}(16-hex) ·captcha.rate→{ok,code,challenged}asset.integrity→{ok,code,ext_scripts,protected}·cdn.pop→{ok,code,cdn,pop}geo.block→{ok,code,blocked}·dns.dnssec→{ok,signed}·traceroute.map→{ok,hops}journey.check→{ok,steps,failed_step}(payload.steps = [{url,expect},…])cdn.pop→{ok,code,cdn,pop}·geo.block→{ok,code,blocked}·dns.dnssec→{ok,signed}traceroute.map→{ok,hops}·app.rank→{ok,rank,found}(payload {track_id,country,chart})
Rules every agent MUST follow
- Only http(s), only PUBLIC targets. Refuse private/reserved IPs even if asked (client-side SSRF guard).
- Send no cookies, no personal data. Identify with your own User-Agent.
- Results are cross-checked on several devices (quorum). Faking work loses trust and pay.
- Rate limit: 120 requests/min/device → HTTP 429 with
retry_after. Honornext_poll. - Byte-identical normalization matters for content.watch/infer — match the reference agent exactly.
infer.neural — the signature contract
A device that carries a neural embedding runtime reports {"sig":"<16 lowercase hex>"}: the 64-bit LSH signature of its embedding of the fetched text. Consensus is tolerance-based (Hamming distance <= 6 bits), because real embeddings are not bit-identical across backends.
Every client MUST project onto the same 64 directions, or honest devices will never agree. The basis is fixed and reproducible in ten lines:
seed = 0x50554C53 // "PULS", 32-bit next(): // xorshift32, then mapped to [-1, 1) x ^= x << 13; x &= 0xFFFFFFFF x ^= x >> 17 x ^= x << 5; x &= 0xFFFFFFFF return x / 2147483648.0 - 1.0 basis = 64 rows x dim components, filled row-major by next() bit_i = dot(embedding, basis[i]) >= 0 ? 1 : 0 sig = the 64 bits, most significant first, as 16 hex digits
Reference: nt_neural_basis() / nt_neural_sig() in private/lib/nodetasks.php.
What we measured, and what it does not prove: on real published material, signatures of the SAME material in DIFFERENT languages landed 3, 5 and 12 bits apart, unrelated material 13 and 16. That is a worst case and not the operating case — an infer.neural task asks several devices to embed the SAME text, where the only source of difference is backend arithmetic. We have not yet measured that case, because no client ships a neural runtime; when one does, check it against tol = 6 before trusting the number.
Adding a new kind of check
Extending the network, not just joining it: https://astrina.io/pulsar/setup?dl=newkind is the five-file checklist (capability, result whitelist, consensus key, generator, staged rollout) and what we ask before merging a contributed kind.
Sandbox — build your agent before you own a device
POST /api/v1/node/sandbox { "do":"next", "session":"your-name" } → task-shaped work with a nonce, exactly like /next
POST /api/v1/node/sandbox { "do":"submit", "session":"your-name", "pubkey":"<64-hex>", "results":[ { "assign_id":…, "result":{…}, "sig":"…" } ] } → per result: accepted true/false and WHY
No pairing, no key, nothing stored, nothing paid. Assignment ids and nonces are derived from your session string, so the same session always sees the same work and two people never collide. The submit side answers what the real server would: which fields it would drop from your result, whether your signature verifies (and against which message form), and whether the assign_id was ever issued. Use it to get the shapes right, then pair a real device.
Optional result signing (Ed25519)
Send pubkey (64-hex Ed25519 public key) at register, or later with next (see above). Then sign each result with the private key, hex, per result:
v0.4 sig = ed25519(assign_id + ":" + nonce + ":" + json(result with keys sorted)) v0.3 sig = ed25519(assign_id + ":" + json(result with keys sorted)) # still accepted
Use the v0.4 form whenever the task carried a nonce. Why it changed: assign ids run in sequence, so the v0.3 message could be signed for an id before it was handed out — the signature proved authorship but not that the work followed the assignment. The nonce cannot be guessed in advance, so a valid v0.4 signature can only have been made after this assignment was issued.
The server verifies and drops a replica whose signature does not match. No key → still accepted (quorum + bearer + TLS already protect you). The reference agent does this automatically.
The reference implementation (one PHP file) is the source of truth: https://astrina.io/pulsar/setup?dl=agent.
O que esta página responde
- escreva seu próprio nó
- registrar, heartbeat, próximo, enviar
- especificação do protocolo do nó
- agente de terceiros
- o que é o protocolo do nó Pulsar
- como o protocolo do nó Pulsar funciona
- o protocolo do nó Pulsar explicado
- guia de o protocolo do nó Pulsar
- como começar com o protocolo do nó Pulsar
- avaliações de o protocolo do nó Pulsar
- vale a pena o protocolo do nó Pulsar
- preços de o protocolo do nó Pulsar
- o protocolo do nó Pulsar alternativas
- o protocolo do nó Pulsar para iniciantes
- por que o protocolo do nó Pulsar
- o protocolo do nó Pulsar — o que você recebe