Solinkify DePIN · Android contribution
A phone becomes a relay node: connect, forward signed transactions, earn USDC. Here is the exact protocol and a path from MVP to a live devnet test.
A Lite node is a pure transport relay. It opens an outbound WebSocket to the coordinator, receives a Solana transaction that is already signed, submits it to an RPC, and reports back the signature. The node never builds or changes a transaction (ADR-006), so it holds no funds and carries almost no trust: the worst a coordinator can do is withhold a job.
Outbound-only is deliberate. Phones sit behind carrier NAT and have no public IP, so the node dials out to the coordinator rather than the other way around.
0.015/day × active-factor, where active-factor = min(session_hours, 12) / 12. Paid out in weekly batches.Before a node can go online, its wallet must be a registered, active Lite node.
register_node instruction, which maps the wallet to the Lite tier on-chain. For the MVP this can be done on the website /depin/setup or the CLI, so the app does not need to build the stake transaction on day one.denied: "wallet is not an active Lite node" until the wallet has staked and is Lite tier on-chain. Registration is a one-time step: run register.js from the reference repo once on any PC using the same keypair the app will use. The Android app itself never needs to build a transaction.| Coordinator WS | wss://api.solinkify.com/coordinator/ws |
| Status (GET) | https://api.solinkify.com/coordinator/status |
| Solana RPC | https://api.devnet.solana.com |
| Program ID | A8qSJCS2uxxnEMdCcpjX2L8hUUMydoeCi5xq5qvzS22B |
| $SINKY mint | EAMRKocV7wTrj2S7CwYQVAbBqB4xa51TV1Akdp9FKa8R |
| Auth domain | solinkify-lite-v1: |
| Lite stake | 25 SINKY · on-chain tier byte = 3 |
This is the contract, one JSON object per text frame. It is identical to the reference Node.js runner already relaying on devnet, so the Android app can port it 1:1.
challenge with a random nonce."solinkify-lite-v1:" + nonce and returns auth with the wallet pubkey and base58 signature.ready (or denied with a reason).job: a job_id and a base64 already-signed transaction.result with the signature (or error with a message).// 1. server → client
{"type":"challenge", "nonce":"<uuid>"}
// 2. client → server (sig = base58 of ed25519 over the UTF-8 bytes below)
{"type":"auth", "wallet":"<base58 pubkey>",
"signature":"<base58 ed25519( 'solinkify-lite-v1:' + nonce )>"}
// 3. server → client
{"type":"ready"}
{"type":"denied", "reason":"wallet is not an active Lite node (register 25 SINKY first)"}
// 4. server → client
{"type":"job", "job_id":"<id>", "tx":"<base64 signed tx>"}
// 5. client → server
{"type":"result", "job_id":"<id>", "signature":"<sig>"}
{"type":"error", "job_id":"<id>", "message":"<err>"}
sendTransaction with encoding base64 and returns the signature. That is a plain JSON-RPC POST, no full Solana SDK is needed for the runner core. Also reply to WebSocket ping with pong."solinkify-lite-v1:" + nonce (the colon is part of the prefix; sign the message itself, not a hash of it). The signature is a 64-byte ed25519 detached signature encoded as base58 (not base64, not hex). Keypair format pitfall on the JVM: a Solana keypair JSON array is 64 bytes, the first 32 are the ed25519 seed and the last 32 the public key. BouncyCastle and Tink expect the 32-byte seed only; tweetnacl-style APIs expect the full 64 bytes. Passing the wrong slice is the most common cause of denied: "signature invalid". To confirm you are online after ready: GET the status endpoint from the table above, your wallet should appear in the list.These match the coordinator, keep them exact.
auth frame within 15s of the challenge.result/error per job, then falls back to another route.sendTransaction (base64) and parse the signature. Only later, in-app staking would need richer Solana tooling.Most of the runner is light. The two real challenges are Android platform behavior, not the protocol:
A build path in four phases. Phase 3 is the proof: a real payment relayed by the phone, verifiable on-chain. (Ticks are a reading aid and do not save.)