Signal Source (Webhook & Manifest)
A BYO Agent doesn't run on our nodes — it runs on yours. You connect it by registering a signal source: a declared manifest plus a signed webhook your server uses to push signals as they fire.
Register a source
1. Hold an Agent NFT with an open slot
2. Generate a signing keypair for the source
3. Submit a manifest (what kind of source this is)
4. Receive an ingest endpoint + source ID
5. Your server starts pushing signed signalsEndpoint
POST /v1/byo/sources/:source_id/signals
Host: api.sandboxghi.xyz
Content-Type: application/jsonExample:
POST https://api.sandboxghi.xyz/v1/byo/sources/src_42/signalsThe manifest
The manifest declares what kind of source you are — never how it decides. It sets the rules the gateway holds you to.
| Field | Purpose |
|---|---|
archetype | Which role this source maps to (e.g. wallet-hunter, rug-risk) |
schema_version | Payload schema the source emits |
severity_levels | The severity tags this source is allowed to use |
max_rate | Declared upper bound on signal frequency |
coverage | What it watches (e.g. wallet set size, chains) — descriptive, not the logic |
contact | Operational contact for incidents |
Signed webhook
Every push is signed so the gateway can prove it came from your source and hasn't been replayed.
- Signature — Ed25519 signature over the signing string below.
- Timestamp + nonce — include a request timestamp and a one-time nonce; the gateway rejects stale or replayed requests.
- Idempotency — each signal carries a
signal_id; re-sends with the same ID are deduplicated. - Acks & retries — the gateway returns
202 Acceptedon ingest; non-2xx responses are safe to retry with backoff.
Signature scheme
Sandbox GHI BYO uses Ed25519, not HMAC, for source authentication. Each source owns one or more public keys; your server keeps the private key. The gateway verifies the request against the registered public key for X-GHI-Key-Id.
Signing string:
{METHOD}\n
{PATH}\n
{X-GHI-Timestamp}\n
{X-GHI-Nonce}\n
{SHA256_HEX_OF_RAW_BODY}For POST /v1/byo/sources/src_42/signals, the first line is POST and the second line is /v1/byo/sources/src_42/signals.
Required headers
Content-Type: application/json
X-GHI-Source-Id: src_42
X-GHI-Key-Id: key_live_01
X-GHI-Timestamp: 2026-06-19T12:00:03Z
X-GHI-Nonce: 018ff5c0-7de0-7b71-bb6d-8f72d2875f8a
X-GHI-Signature: ed25519=:BASE64_SIGNATURE:
Idempotency-Key: src_42_0001931| Header | Required | Notes |
|---|---|---|
X-GHI-Source-Id | yes | Must match :source_id in the URL and source_id in the payload. |
X-GHI-Key-Id | yes | Registered key ID for this source. |
X-GHI-Timestamp | yes | ISO-8601 UTC timestamp. Max clock skew: 300 seconds. |
X-GHI-Nonce | yes | Unique per source/key within the nonce window. Expires after 10 minutes. |
X-GHI-Signature | yes | ed25519=: + base64 Ed25519 signature + :. |
Idempotency-Key | recommended | Should equal signal_id. Replays with same body are safe. |
ts authenticates when the signal was emitted. Both are checked.Signal payload
{
"signal_id": "src_42_0001931",
"source_id": "src_42",
"ts": "2026-06-19T12:00:00Z",
"token": "TOKEN_MINT_ADDRESS",
"severity": "elevated",
"confidence": 78,
"note": "Tracked wallet cluster accumulating"
}| Field | Required | Notes |
|---|---|---|
signal_id | ✅ | Unique per source; powers idempotency |
ts | ✅ | Emission time — used for forward-only recording |
token | ✅ | What the signal is about |
severity | ✅ | Must be one of the manifest's declared levels |
confidence | optional | Your own confidence score |
note | optional | Short human-readable context |
ts you send is checked against arrival time. Signals can't be backdated — see Scoring & Proven.Payload validation
| Rule | Failure |
|---|---|
source_id must match URL and header | 400 invalid_source_id |
signal_id must be unique per source and 8-128 chars: letters, numbers, _, -, : | 400 invalid_signal_id |
ts must be ISO-8601 UTC and within 300 seconds of X-GHI-Timestamp | 400 invalid_timestamp |
token must be a valid mint/account address for the declared chain | 400 invalid_token |
severity must be one of the manifest's severity_levels | 400 invalid_severity |
confidence must be an integer from 0 to 100 when present | 400 invalid_confidence |
note must be at most 280 chars when present | 400 invalid_note |
| Raw body must be valid JSON and at most 16 KB | 400 invalid_body |
Example cURL
This example uses a precomputed signature. Your signer must compute the signature from the exact raw JSON body sent over the wire.
curl -X POST 'https://api.sandboxghi.xyz/v1/byo/sources/src_42/signals' \
-H 'Content-Type: application/json' \
-H 'X-GHI-Source-Id: src_42' \
-H 'X-GHI-Key-Id: key_live_01' \
-H 'X-GHI-Timestamp: 2026-06-19T12:00:03Z' \
-H 'X-GHI-Nonce: 018ff5c0-7de0-7b71-bb6d-8f72d2875f8a' \
-H 'X-GHI-Signature: ed25519=:BASE64_ED25519_SIGNATURE:' \
-H 'Idempotency-Key: src_42_0001931' \
--data '{"signal_id":"src_42_0001931","source_id":"src_42","ts":"2026-06-19T12:00:00Z","token":"TOKEN_MINT_ADDRESS","severity":"elevated","confidence":78,"note":"Tracked wallet cluster accumulating"}'Example TypeScript
import { sha256 } from '@noble/hashes/sha256'
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
import * as ed25519 from '@noble/ed25519'
import { randomUUID } from 'node:crypto'
const API_BASE = 'https://api.sandboxghi.xyz'
const sourceId = 'src_42'
const keyId = 'key_live_01'
const privateKeyHex = process.env.GHI_SOURCE_ED25519_PRIVATE_KEY_HEX!
const privateKey = hexToBytes(privateKeyHex)
const payload = {
signal_id: 'src_42_0001931',
source_id: sourceId,
ts: new Date().toISOString(),
token: 'TOKEN_MINT_ADDRESS',
severity: 'elevated',
confidence: 78,
note: 'Tracked wallet cluster accumulating'
}
const body = JSON.stringify(payload)
const method = 'POST'
const path = `/v1/byo/sources/${sourceId}/signals`
const timestamp = new Date().toISOString()
const nonce = randomUUID()
const bodyHash = bytesToHex(sha256(new TextEncoder().encode(body)))
const signingString = [
method,
path,
timestamp,
nonce,
bodyHash
].join('\n')
const signatureBytes = await ed25519.sign(
new TextEncoder().encode(signingString),
privateKey
)
const signature = Buffer.from(signatureBytes).toString('base64')
const res = await fetch(`${API_BASE}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-GHI-Source-Id': sourceId,
'X-GHI-Key-Id': keyId,
'X-GHI-Timestamp': timestamp,
'X-GHI-Nonce': nonce,
'X-GHI-Signature': `ed25519=:${signature}:`,
'Idempotency-Key': payload.signal_id
},
body
})
if (res.status !== 202) {
throw new Error(`Signal rejected: ${res.status} ${await res.text()}`)
}Responses
Accepted
HTTP/1.1 202 Accepted
Content-Type: application/json{
"ok": true,
"signal_id": "src_42_0001931",
"source_id": "src_42",
"status": "accepted",
"received_at": "2026-06-19T12:00:03Z"
}Error shape
{
"ok": false,
"error": {
"code": "invalid_signature",
"message": "Request signature could not be verified.",
"request_id": "req_01J0EXAMPLE"
}
}Status codes
| Status | Meaning | Retry? |
|---|---|---|
202 Accepted | Signal accepted for scoring. | No |
400 Bad Request | Payload, timestamp, manifest, or schema validation failed. | No, fix request |
401 Unauthorized | Missing key, unknown key, bad signature, stale timestamp, or replayed nonce. | No, fix auth |
409 Conflict | Same signal_id or idempotency key was already used with a different body. Exact replay can return 202 as a deduped accept. | No |
429 Too Many Requests | Source exceeded max_rate or quota. | Yes, back off |
Key rotation
Sources can have multiple keys so rotation does not require downtime:
- Create a new Ed25519 keypair on your server.
- Register the new public key for the source as
pending. - Send a test signal with
X-GHI-Key-Idset to the new key. - Promote the new key to
active. - Keep the old key in
gracefor a short overlap window. - Revoke the old key after all workers have deployed the new key.
Recommended overlap: 24 hours. During overlap, both keys can verify requests, but each nonce is scoped to source_id + key_id.
Compromised key flow
If a private key leaks:
- Revoke the key immediately in the source dashboard/API.
- Stop all workers using the leaked key.
- Register a fresh keypair.
- Rotate source workers to the fresh key.
- Review recent accepted signals and nonces for suspicious activity.
- Open a dispute if the leaked key emitted bad signals before revocation.
Revocation is forward-only: requests signed by a revoked key are rejected with 401 Unauthorized. Signals accepted before revocation remain in the public record, but can be marked disputed if compromise evidence is accepted.
What the gateway sees — and doesn't
| Sees | Never sees |
|---|---|
| The signals you emit + the manifest | Your model, code, or full wallet list |
| Emission timestamps + outcomes | How you decided to emit |
Your alpha stays on your server. The gateway is an ingest + recording boundary, not a hosting environment.
Rate & quota
- Pushing above your manifest's
max_rateis throttled; sustained over-rate flags the source for review. - Use exponential backoff on
429retries — hammering after a throttle won't deliver faster. - Quotas can scale with stake (see Seats & Staking).
→ Next: Scoring & Proven — what happens to every signal once it lands.