Create & Rent Your Edge
Signal Source

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.

⚠️
Roadmap — Phase 2, draft. The schema and signing details below are an intended design and will change before launch. Nothing here is live yet.

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 signals

Endpoint

POST /v1/byo/sources/:source_id/signals
Host: api.sandboxghi.xyz
Content-Type: application/json

Example:

POST https://api.sandboxghi.xyz/v1/byo/sources/src_42/signals

The manifest

The manifest declares what kind of source you are — never how it decides. It sets the rules the gateway holds you to.

FieldPurpose
archetypeWhich role this source maps to (e.g. wallet-hunter, rug-risk)
schema_versionPayload schema the source emits
severity_levelsThe severity tags this source is allowed to use
max_rateDeclared upper bound on signal frequency
coverageWhat it watches (e.g. wallet set size, chains) — descriptive, not the logic
contactOperational 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 Accepted on 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
HeaderRequiredNotes
X-GHI-Source-IdyesMust match :source_id in the URL and source_id in the payload.
X-GHI-Key-IdyesRegistered key ID for this source.
X-GHI-TimestampyesISO-8601 UTC timestamp. Max clock skew: 300 seconds.
X-GHI-NonceyesUnique per source/key within the nonce window. Expires after 10 minutes.
X-GHI-Signatureyesed25519=: + base64 Ed25519 signature + :.
Idempotency-KeyrecommendedShould equal signal_id. Replays with same body are safe.
The request timestamp authenticates the delivery attempt. The payload 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"
}
FieldRequiredNotes
signal_idUnique per source; powers idempotency
tsEmission time — used for forward-only recording
tokenWhat the signal is about
severityMust be one of the manifest's declared levels
confidenceoptionalYour own confidence score
noteoptionalShort human-readable context
The ts you send is checked against arrival time. Signals can't be backdated — see Scoring & Proven.

Payload validation

RuleFailure
source_id must match URL and header400 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-Timestamp400 invalid_timestamp
token must be a valid mint/account address for the declared chain400 invalid_token
severity must be one of the manifest's severity_levels400 invalid_severity
confidence must be an integer from 0 to 100 when present400 invalid_confidence
note must be at most 280 chars when present400 invalid_note
Raw body must be valid JSON and at most 16 KB400 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

StatusMeaningRetry?
202 AcceptedSignal accepted for scoring.No
400 Bad RequestPayload, timestamp, manifest, or schema validation failed.No, fix request
401 UnauthorizedMissing key, unknown key, bad signature, stale timestamp, or replayed nonce.No, fix auth
409 ConflictSame 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 RequestsSource exceeded max_rate or quota.Yes, back off

Key rotation

Sources can have multiple keys so rotation does not require downtime:

  1. Create a new Ed25519 keypair on your server.
  2. Register the new public key for the source as pending.
  3. Send a test signal with X-GHI-Key-Id set to the new key.
  4. Promote the new key to active.
  5. Keep the old key in grace for a short overlap window.
  6. 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:

  1. Revoke the key immediately in the source dashboard/API.
  2. Stop all workers using the leaked key.
  3. Register a fresh keypair.
  4. Rotate source workers to the fresh key.
  5. Review recent accepted signals and nonces for suspicious activity.
  6. 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

SeesNever sees
The signals you emit + the manifestYour model, code, or full wallet list
Emission timestamps + outcomesHow 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_rate is throttled; sustained over-rate flags the source for review.
  • Use exponential backoff on 429 retries — 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.