Presign

Know exactly what you sign.

Checking… Connect wallet

API, agents & alerts

The same deterministic engine behind the Presign UI, three ways: an HTTP API for wallets and backends, an MCP server for AI agents, and Watchtower alerts for multisig teams. Everything is read-only: Presign never takes keys and never signs.

HTTP API

JSON in, JSON out. Every response uses the envelope { success, data, error }; errors carry a stable code and a message that never contains provider URLs, keys or stack traces. Requests are rate-limited per client.

EndpointBodyWhat it does
POST /api/transaction/analyze{ "input": "<base64 | base58 | signature>", "walletAddress"?: "<signer>", "policy"?: {…} }Decode, simulate and risk-check a transaction. Squads approvals include the proposal's vault instructions, simulation and authority changes.
POST /api/multisig/inspect{ "input": "<Squads link | address | <multisig> #<n>>", "signer"?: "<member>", "policy"?: {…} }Inspect a proposal without a transaction to sign, a multisig's setup and recent proposals, or a Presign Guard and its scheduled actions.
POST /api/guard/prepare{ "kind": "veto" | "execute", "action": "<action>", "signer": "<wallet>" }Unsigned veto (guardians) or execute (anyone, after the delay) transaction for a Guard action, with a preparedToken; sign it in your wallet, then submit it with that token via /api/transaction/submit.
GET /api/token?mint=<mint>—Token security signals: authorities, Token-2022 extensions, concentration, age, metadata links.
GET /api/health—Cluster and configured data sources (no secrets).
POST /api/presign/connect{ "target"?: "https://<dapp>", "name"?: "<label>", "returnUrl"?: "https://<dapp>/<callback>" }Pre-connect checks before any wallet opens: Presign origin, HTTPS, session, request structure, target domain (unknown is never safe), expiry. Returns a session-bound connection token.
POST /api/presign/nonce → /connect/verify{ "walletAddress" } → { "walletAddress", "message", "signature", "nonceToken" }Wallet ownership: a one-time message that authorizes nothing, verified with ed25519; the nonce is spent.
POST /api/presign/signing/analyze{ "type": "TRANSACTION" | "MESSAGE", "payload", "payloadEncoding"?, "walletAddress", "connectionToken"?, "expectedEffects"?: { "summary", "maxSolOutLamports", "maxTokenOut" } }Pre-sign review of the exact payload: decode, simulate, rules (plus the application address and what the application declared versus what the simulation shows), the unchanged machine gate, and the human decision (technicalValidation, recommendedAction, userCanReview, userCanOverride).
POST /api/presign/signing/approve{ "analysisToken", "payload", "walletAddress", "choice": "SIGN" | "CONTINUE" | "OVERRIDE", "overrideConfirmed"? }The user's decision, bound to request + wallet + session + payload hash + expiry, single use. The wallet is asked only after this; /api/transaction/submit requires the approval token.
POST /api/presign/signing/verify-approval{ "approvalToken", "payloadHash" }For the browser extension: { valid } for a genuine, unexpired approval of exactly this payload hash, confirmed once. The extension computes the hash from the bytes it captured and sends nothing to the wallet without a valid answer.
POST /api/transaction/submit{ "signedTransaction", "expectedMessageHash", "approvalToken" | "preparedToken" }Relays a transaction your wallet signed — only one Presign reviewed and you approved (approval token, same session, single use) or one Presign prepared (Guard). Re-checks the exact bytes and every signature; never signs. Not a general relay.
curl -s -X POST https://<your-presign-host>/api/multisig/inspect \
  -H 'content-type: application/json' \
  -d '{"input":"2LW6PSEjp81xSEttWwXDB6Etb1eKdhYPbFEojYbyhx88 #7"}'

Key fields: risk (verdict, completeness, signals, evidence), gate, brief (plain-language summary), multisig (config, proposals, decoded and simulated vault payloads, privileged actions).

The gate

Every analysis carries a gate for automated signers. It is a fixed mapping from the verdict and its completeness — no model, no judgment — so an agent cannot be talked out of it. Missing data never maps to no_known_risk.

  • block — Do not sign. At least one high or critical risk signal was found.
  • require_human_review — Do not sign automatically. A human must review the signals, or the analysis was incomplete.
  • no_known_risk — No risk signal found by complete checks. This is not a guarantee of safety.

block: CRITICAL or HIGH · require_human_review: MEDIUM, unrated, or any incomplete analysis · no_known_risk: LOW or no signal, with every check complete.

Secure connect & pre-sign review

For people signing in a wallet, Presign adds a second decision layer on top of the gate. The gate above stays fail-closed for bots and agents; a human who has seen the evidence may still decide. Presign advises, the user decides — but only about a request it could actually verify.

  • SAFE / LOW — sign (after a completed analysis; if some checks could not run, “Continue anyway”). MEDIUM / UNKNOWN — warning, “Continue anyway”. HIGH / CRITICAL — recommendation “do not sign”; the user can still choose “I understand the risk — sign anyway” after one explicit confirmation.
  • Cannot verify (malformed, unsupported, no simulation, wrong wallet, expired, changed after analysis) — Cancel only. No “sign anyway” for something Presign does not understand.
  • Exact payload — the approval is bound to the request id, wallet, browser session, sha256 of the bytes the wallet signs and a short expiry, and is single use (across server instances when a shared store is configured; otherwise per instance). The wallet is asked for exactly those bytes; a changed payload is refused before the wallet, after the wallet, and at submission.

An integration hands Presign its context with a link such as /connect?target=https://app.example&name=Example&return=https://app.example/done (the return address must be on the target's own origin), then sends each signing request to /api/presign/signing/analyze before forwarding it to the wallet. A standalone website cannot see what other websites ask a wallet to sign; the Presign browser extension does that, on the same boundary (lib/presign/interceptor.ts — the web app's own implementation is used by the demo dApp). An integration can also declare what a request is supposed to do (expectedEffects); a simulation that moves more, or moves tokens it did not mention, is a HIGH finding. Try it with your own wallet in the pre-sign demo.

Browser extension (any site)

The extension reviews signing requests on any website, with no link to share: when a site asks your wallet to sign through the wallet APIs it wraps, Presign opens its review first, and the wallet opens only after your decision. Not yet on the Chrome Web Store and not yet tried with a real wallet extension.

  • What it wraps — Wallet Standard wallets (Phantom, Solflare, Backpack and most others, including the legacy navigator.wallets registration) and injected providers such as window.phantom.solana: sign transaction(s), sign and send (one or several), sign message, off-chain message, Sign-In With Solana. Any other signing entry point is refused.
  • Same review — the request goes to /extension/review on Presign: the same analysis, decision rules and server approval as everywhere else. The site's address is the one the browser reports, not what the site says.
  • Exact bytes in, the result checked — the bytes are copied the moment the site asks, and after approval the wallet gets that copy, so a site cannot change the request while you read the review; right before the wallet call the extension checks that what the wallet receives hashes to what Presign's server approved. If the wallet returns a transaction or message that differs from what was reviewed, or a message signature that is not valid for it, the signature is withheld from the site (best effort against a site that hooks the wallet's own code).
  • Fails closed — while protection is on, a request Presign cannot review (too large, too many signatures, malformed, review window unavailable) is never sent to the wallet unreviewed; only the extension's own switches (off, or off for this site) let requests through.
  • Ownership, once per session — Presign approves only for a wallet you proved is yours with a message that authorizes nothing; your wallet extension works on the review page too.
  • Approvals are confirmed, not trusted — before anything goes to the wallet, the extension computes the payload hash from the bytes it captured and asks the Presign server the review came from (POST /api/presign/signing/verify-approval) whether its sealed approval is genuine, unexpired, for exactly that hash, and unused. Otherwise the request is blocked.
  • No keys; one kind of request — the extension never signs and never holds keys. Its only request is that confirmation, to a Presign origin only, without cookies; a test fails on any other.
npm run build:extension
# Chrome → chrome://extensions → Developer mode → Load unpacked → extension/dist

Limits: the hook does not depend on built-ins a site can replace, but it cannot control a wallet's own page code — a site written for a particular wallet can still reach it around Presign, so the wallet's confirmation window stays the final check (wallet-level integration closes that); a transaction the wallet broadcasts itself (sign and send) can only be checked before the wallet; a sign-in whose account is chosen inside the wallet is signed there first and reviewed before the site gets it.

Team policy

Write your team's rules once; Presign checks every proposal and signature against them. A broken rule becomes a signal with the policy's severity, so the verdict, the gate and every Watchtower alert reflect it. A rule that cannot be checked — contents not decoded, not simulated, an account not loaded — is reported as such (MEDIUM), never as compliant. The policy is plain JSON: paste it in /verify (kept in your browser), send it as policy to /api/multisig/inspect or /api/transaction/analyze, or set PRESIGN_POLICY_FILE for Watchtower and the MCP server (one policy, or an array told apart by multisig).

{
  "version": 1,
  "name": "Security council",
  "severity": "HIGH",
  "authorityHolders": [],
  "requireGuardFor": [
    "admin-transfer",
    "upgrade-authority",
    "program-upgrade",
    "token-authority"
  ],
  "minGuardDelaySeconds": 86400,
  "minTimeLockSeconds": 3600,
  "minThreshold": 3,
  "allowedPrograms": [
    "system",
    "spl-token",
    "token-2022",
    "associated-token",
    "squads"
  ],
  "allowedRecipients": [],
  "outflowLimits": {
    "SOL": "100"
  },
  "requireVerifiedUpgrades": true,
  "forbidDurableNonce": true
}
RuleWhat it checks
multisigThe multisig the policy was written for. Applying it to another one is a violation.
severityMEDIUM, HIGH (default) or CRITICAL: how a broken rule counts. HIGH and CRITICAL make the gate block.
authorityHoldersAddresses that may receive an admin, upgrade, token or config authority, besides the multisig, its vaults and the signers of `guards`. Add "none" to allow removing an authority.
requireGuardForPrivileged kinds that must be scheduled through Presign Guard: admin-transfer, admin-action, upgrade-authority, program-upgrade, token-authority, account-reassign, program-close. Handing an authority to a listed guard is allowed.
guards, minGuardDelaySecondsWhich guards scheduled actions may use, and their minimum delay.
minTimeLockSeconds, minThresholdChecked on the current setup and on proposals that change it.
allowedProgramsPrograms the vault (or a scheduled action) may call directly: addresses, or system, spl-token, token-2022, associated-token, compute-budget, memo, bpf-loader, squads.
allowedRecipientsWallets that may receive SOL or tokens from the vault, or be approved as delegates. Token recipients are resolved to their owners from the simulation.
outflowLimitsMaximum net outflow per proposal from the vaults, in UI units, keyed by "SOL" or a mint. Batches add up; scheduled transfers count too.
requireVerifiedUpgradesProgram upgrades must deploy exactly a build verified in the OtterSec registry.
forbidDurableNonceSignatures must not use a durable nonce: checked on the transaction you sign, and on the votes already cast on a proposal.

A policy only adds signals: it can never lower a verdict or hide a finding. Every rule is optional; unknown keys are rejected.

MCP server for AI agents

Agents that hold a wallet can be prompt-injected. The MCP server gives them a pre-sign check whose answer comes from rules, not from the model. Tools: presign_verify_transaction, presign_inspect_multisig, presign_check_token. It calls your Presign API, so RPC keys stay on the server. Set PRESIGN_POLICY_FILE to hold every check to your team policy (the agent cannot change it). Requires Node.js 22.18+.

{
  "mcpServers": {
    "presign": {
      "command": "node",
      "args": ["/path/to/presign/mcp/server.ts"],
      "env": { "PRESIGN_API_URL": "https://<your-presign-host>" }
    }
  }
}

Guard snippet

For bots and backends that sign programmatically: refuse anything the gate does not clear.

async function presignGuard(serializedTx: string, signer: string) {
  const res = await fetch(`${PRESIGN_URL}/api/transaction/analyze`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ input: serializedTx, walletAddress: signer }),
  });
  const { success, data, error } = await res.json();
  if (!success) throw new Error(`Presign unavailable: ${error?.message}`); // fail closed
  if (data.gate !== "no_known_risk") {
    throw new Error(`Presign ${data.gate}: ${data.risk.signals.map((s) => s.title).join("; ")}`);
  }
  return data; // data.messageHash: sign exactly these bytes
}

Watchtower alerts

Watchtower sends every new proposal's brief — verdict, authority changes, top signals and a verify link — to every signer at once, through a channel independent of the UI that created the proposal. It watches Presign Guards too: each scheduled action is announced with its countdown and a veto link. A new watch records a baseline; later changes alert.

Self-service (Telegram): add the bot to your signers' group, then:

/watch <multisig or guard address, or Squads link>   (group admins only)
/unwatch <address>
/list
/check <Squads link, proposal, or <multisig> #<n>>     (anyone)

Limits (the bot is public): 20 targets per chat and 500 through the bot in all; 6 /check or /watch a minute per person and 60 for everyone. Each cycle polls the environment's targets first, then up to 100 bot-added targets in turns, at most 4 at once — so other chats' watches cannot delay the operator's own alerts. With many subscriptions a bot-added target is polled less often than every cycle; alerts arrive on the next poll, not instantly.

Running it: one process, state in a local SQLite file. Targets can also come from the environment:

PRESIGN_API_URL=https://<your-presign-host> \
TELEGRAM_BOT_TOKEN=<bot token> \
WATCH_MULTISIGS=<multisig>[,…]  WATCH_GUARDS=<guard>[,…]  TELEGRAM_CHAT_ID=<group id> \
npm run watchtower            # add -- --once for a single cycle (cron)
  • ALERT_WEBHOOK_URL: Slack or Discord incoming webhook for environment targets.
  • POLL_SECONDS (default 30, minimum 10), PRESIGN_PUBLIC_URL for links, WATCH_DB for the state file, ALERT_EXISTING=true to report already-pending items on first start.
  • PRESIGN_POLICY_FILE: your team policy; every alert then says whether the proposal complies.
  • Tokens are read from the environment only and never logged.
🛑 New proposal #7 on multisig 2LW6…hx88 — CRITICAL
• Drift Protocol v2 updateAdmin → H7Pi…7ZgL (NOT controlled by the multisig)
• CRITICAL: Admin moves outside the multisig
• HIGH: No time lock
Verify before signing: https://<your-presign-host>/verify?q=…

Presign Guard (on-chain)

Warnings help only if someone can act on them. Presign Guard is a Solana program that holds a protocol's critical authorities — admin, upgrade authority, mint authority — so that anything done with them is scheduled, waits a fixed delay, and can be vetoed by any single guardian. Routine operations stay on the multisig and stay fast; only critical ones wait.

  • The multisig vault is the proposer: a normal Squads proposal calls schedule; the action then waits the delay.
  • Guardians (members, or an independent security key) can veto alone; they can never execute anything.
  • After the delay anyone can execute; only then does the guard's PDA sign. Configuration changes go through the same delay and veto.
  • Presign decodes scheduled actions inside proposals, shows the countdown in /verify (paste a guard or action address), offers veto / execute, and Watchtower announces every scheduled action.

Status: deployed on devnet (program A8cpj1d7zxF3T9kZzVn2wkEueGxqGVgd9VaqBA54EDRS) and exercised end-to-end with a real Squads multisig — schedule, CRITICAL alert, veto, execution refused. Unaudited — do not hand it mainnet authorities. This deployment has no Guard program configured.