v0.2 LIVE · Free decoder + B2B verification API

Integrate Zoza Sign

Catch UI-vs-bytes lies before they cost you $1.46B. Drop the verifier into your signing flow in 30 minutes — REST API, signed audit receipts, free decoder for end-users.

Read this first

Sign is positioned narrowly: independent decode + intent comparison + audit receipt. It is not a hardware-wallet replacement, a transaction simulator, or a risk scorer. See the competitor matrix on /about/sign for honest scope.

If you're a regulated custody team, also read the "What's NOT yet built" section before scoping a production integration.

Quick Start

The fastest path: hit the free decoder endpoint with raw transaction bytes, get back the decoded fields, eyeball the diff against what your UI claims.

curlno auth required
curl -X POST https://sign-api.zoza.world/v1/decode \
  -H "Content-Type: application/json" \
  -d '{"raw_tx":"0000000000000001000000000000002a0000000000005208742d35cc6634c0532925a3b844bc9e7595f2bd2800000000000000000000000000000000000000000000000de0b6b3a7640000"}'

# Response:
# {
#   "type": "transfer",
#   "chain_id": 1,
#   "nonce": 42,
#   "to": "0x742d35cc6634c0532925a3b844bc9e7595f2bd28",
#   "value": "1000000000000000000",
#   "value_human": "1.000000 ETH"
# }

Authenticated POST /v1/verify compares the decoded output against your claimed intent and returns a signed audit receipt. Used by exchanges in the signing pipeline before any approval reaches a hardware wallet.

Pick Your Integration

Two integration tiers. Most production teams use both.

🆓
End Users / Anyone

Free Decoder

Public POST /v1/decode. No auth. Rate-limited per IP. Use it to power "show me what I'm actually signing" UX in your wallet, custody dashboard, or block explorer.

🔌
Exchanges / Custody / Treasury

Verify API + Audit Receipts

Authenticated POST /v1/verify. Compares claimed intent vs decoded reality, flags mismatches, returns Ed25519-signed audit receipt for compliance. Counts toward your daily quota.

Apply for an API Key

The free /v1/decode endpoint needs no key — try it from any browser, any backend, any time. You only need an API key for /v1/verify (signed receipts + per-call quota tracking).

Fill the form below — we review within 24 hours and email your zsk_live_... key on approval. Keys are shown once at issuance.

Authentication

Pass your key as a Bearer token on every /v1/verify and /v1/verifications/* call:

Authorization: Bearer zsk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

POST /v1/decode needs no header at all — open to everyone.

Base URL

https://sign-api.zoza.world

All API paths below are relative to that base.

POST /v1/decode

POST /v1/decode FREE
Decode raw transaction hex into human-readable fields. No auth, IP-rate-limited.
Request
{
  "raw_tx": "0x0000000000000001..."
}
Response · 200
{
  "type": "transfer",
  "chain_id": 1,
  "nonce": 42,
  "gas_limit": 21000,
  "to": "0x742d35cc6634c0532925a3b844bc9e7595f2bd28",
  "value": "1000000000000000000",
  "value_human": "1.000000 ETH",
  "token": "ETH",
  "data": "",
  "method": ""
}

Recognized method signatures: transfer(address,uint256), approve(address,uint256) with infinite-allowance detection. Other 4-byte selectors return method: "unknown(0xa9059cbb)" + raw data for inspection.

POST /v1/verify

POST /v1/verify API KEY
Compare claimed UI intent against decoded raw bytes. Returns Ed25519-signed receipt.
Request
{
  "intent": {
    "type": "transfer",
    "to": "0x742d35cc6634c0532925a3b844bc9e7595f2bd28",
    "amount": "1.000000",
    "token": "ETH",
    "chain": "ethereum",
    "description": "Withdrawal to user wallet"
  },
  "raw_tx": "0x0000000000000001..."
}
Response · 200 (match)
{
  "id": "ver_a1b2c3...",
  "status": "match",
  "risk_level": "safe",
  "mismatches": [],
  "decoded_tx": { /* same shape as /v1/decode */ },
  "claimed_intent": { /* echoed back */ },
  "receipt": {
    "verification_id": "ver_a1b2c3...",
    "exchange_id": "exch_xyz...",
    "status": "match",
    "timestamp": "2026-04-17T13:42:00Z",
    "signature": "base64-ed25519-sig",
    "signer_pub": "base64-ed25519-pubkey"
  },
  "verified_at": "2026-04-17T13:42:00Z"
}
Response · 200 (mismatch — Bybit-class)
{
  "id": "ver_x9y8z7...",
  "status": "mismatch",
  "risk_level": "critical",
  "mismatches": [
    {
      "field": "to",
      "claimed": "0x742d35Cc...f2bD28",
      "actual": "0xDEADBEEF...DEADBEEF",
      "severity": "critical"
    }
  ],
  /* decoded_tx + claimed_intent + receipt as above */
}
On mismatch

Sign returns 200 OK with status:"mismatch" — not an HTTP error. Your signing pipeline must inspect status and risk_level and block on "critical". Logging-only integrations defeat the purpose.

GET /v1/verifications/{id}

GET /v1/verifications/{id} API KEY
Re-fetch a previously created verification + receipt. Idempotent. Use for retries and audit.

GET /v1/authority

GET /v1/authority FREE
Returns the current Ed25519 authority pubkey used to sign receipts. Pin this in your verifier.
Response · 200
{
  "algorithm": "ed25519",
  "public_key": "base64-pubkey",
  "context": "sign_receipt",
  "note": "Use this key with verifyReceipt() to verify any receipt offline."
}

Receipt Shape

Every successful /v1/verify embeds a receipt object:

FieldTypeDescription
verification_idstringSign's internal ID — pin to your audit row
exchange_idstringYour account ID — pin to your customer record
statusstring"match" | "mismatch" | "suspicious" | "error"
timestampRFC3339UTC time the comparison ran
signaturebytes (base64)Ed25519 over JSON of the four fields above, with context "sign_receipt"
signer_pubbytes (base64)The pubkey that produced the signature — pinned for offline verify

Verify a Receipt Offline

You don't need to trust Sign's API to validate any receipt. Pull the canonical pubkey, recompute the JSON, and verify the signature locally.

TypeScriptusing @noble/ed25519 — no Sign dependency
import * as ed from '@noble/ed25519';

async function verifyReceipt(receipt) {
  const canonical = JSON.stringify({
    id: receipt.verification_id,
    exchange_id: receipt.exchange_id,
    status: receipt.status,
    timestamp: receipt.timestamp
  });
  const message = new TextEncoder().encode('sign_receipt|' + canonical);
  const sig = Buffer.from(receipt.signature, 'base64');
  const pub = Buffer.from(receipt.signer_pub, 'base64');
  return await ed.verify(sig, message, pub);
}

Storage & Audit

Treat receipts the way you treat database backups — keep them for as long as your regulator says (typically 7 years for FATF Travel Rule). They're small (~512 bytes JSON), append-only, and provably tamper-evident. Recommended layout:

// One row per signed verification
CREATE TABLE sign_receipts (
  verification_id  TEXT PRIMARY KEY,
  exchange_id      TEXT NOT NULL,
  status           TEXT NOT NULL,
  raw_tx           TEXT NOT NULL,    -- full hex you sent in
  intent_json      JSONB NOT NULL,   -- full intent you sent in
  decoded_json     JSONB NOT NULL,   -- decoded_tx returned by Sign
  receipt_json     JSONB NOT NULL,   -- full receipt object
  created_at       TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Mismatch Classes

Sign emits four mismatch classes today. Each has documented severity — your pipeline should map severity to your incident response.

FieldSeverityWhat it meansReal-world example
tocriticalDecoded destination differs from claimed destination (case-insensitive comparison).Bybit Feb 2025 — UI showed Safe address, bytes pointed to attacker proxy.
amountcriticalDecoded value differs from claimed amount (numeric comparison, suffix-stripped).Skim attacks — UI claims 50 ETH withdrawal, bytes drain 500 ETH.
typewarningDecoded transaction type differs from claimed type (transfer vs approval, etc.).UI claims "transfer", calldata is setApprovalForAll(spender, true).
approvalcriticalDecoded approval is unlimited (max uint256). Always flagged regardless of intent.Permit2 ice-phishing — spender granted infinite allowance.

Error Codes

CodeMeaningAction
400Invalid request (missing field, malformed hex, intent JSON unparseable)Read error field, fix request, retry
401Missing / invalid / revoked API keyCheck Authorization header, rotate key if compromised
404Verification ID not foundID may have expired (90-day window) or was never issued
409Application already approved/rejectedDon't double-submit application
429Rate limit exceeded (per IP for /decode, per email/IP for /applications)Back off and retry after 24h or upgrade plan
500Internal Sign errorRetry with exponential backoff. If persistent, email hello@zoza.world

Rate Limits & Plans

Free

$0/mo
1,000 verifications/day
  • Free decoder unlimited (IP-rate-limited)
  • 1K signed verifications/day
  • Email support, best-effort

Enterprise

Custom
10M+ verifications/day
  • Dedicated authority key
  • Receipts retained 7+ years (FATF)
  • SOC 2 report (when available)
  • Source-access NDA + audit hooks
  • Slack/PagerDuty incident channel

Quota resets at UTC midnight. Every authenticated response includes X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset-UTC headers.

CORS

The decoder endpoint allows requests from any origin (used by browser-based wallets and the live decoder on /about/sign). The verify endpoint also allows any origin but requires a valid API key — keys should never live in browser code in production. Run verify from your backend.

Going Live Checklist

Pre-production sign-off

Walk this list before pointing Sign at production withdrawals.

  1. Pin the authority pubkey. Fetch /v1/authority at deploy time, embed in your config. Verify it matches the signer_pub in receipts.
  2. Block on critical, alert on warning. Don't log-only — the whole point is preventing the signature, not documenting it.
  3. Store full receipts. Schema above. Don't extract status only — you need the signature for audit.
  4. Test the Bybit scenario. Build a fixture where intent says address A but raw_tx points to address B. Confirm your pipeline blocks.
  5. Test the unlimited-approval scenario. Build a fixture with approve(spender, MAX_UINT256). Confirm critical mismatch fires.
  6. Set up alerting. Webhook delivery isn't shipped yet (see gap analysis) — for now, poll /v1/verifications/{id} after each signature attempt and page on mismatch.
  7. Plan for authority rotation. Quarterly. When we rotate, the new pubkey will be served at /v1/authority and the old pubkey continues to validate historical receipts.
  8. Sign the source-access NDA if you need to audit the decoder yourself. Email hello@zoza.world.

Support

Read /about/sign before integrating

The overview page documents what Sign does NOT do — competitor matrix, attack surface analysis, and the honest "what's not yet built" gap list. If you're a custody team scoping a regulated integration, that page is required reading.