Start / Introduction

Read it before you sign it

Pavise is a pre-sign firewall for Solana. It takes the bytes a wallet is about to sign, turns every instruction into a sentence, replays them on mainnet and scores how much of your money or authority they hand to someone else.

A pavise was the tall shield a crossbowman planted in front of himself while he reloaded. Signing is your reload: the one moment you stand still and trust what is in front of you. Most drains do not break any cryptography. They ask politely, the wallet shows a vague popup, and the user presses Approve.

What it does

Where it runs

Everything runs in your browser. The site, the Chrome guard and your own code all use the same engine, lib/pavise.js: one ES module, no dependencies, MIT. Network calls go to a public Solana RPC, DexScreener for market data and Jupiter for quotes. There are no accounts, no keys and no server that sees your transactions.

NotePavise never asks for a private key or a seed phrase and cannot sign anything. If something that calls itself Pavise asks for one, it is not Pavise.
Start / Quick start

Quick start

Use the site, or import the engine straight from it. No install, no build step.

On the site

  1. Copy the serialized transaction a dApp or a Blink gives you (base64 or base58).
  2. Paste it into 01 // Pre-sign firewall and press Inspect.
  3. Read the instructions in the sandbox and the verdict on the right. Sign only if you agree with every line.

In your code

The module is served with CORS, so any page can import it by URL.

import { inspect, describe } from "https://usepavise.vercel.app/lib/pavise.js";

const { tx, report } = inspect(base64Tx);          // parse + assess
console.log(report.verdict, report.score);          // "BLOCK" 100
report.decoded.forEach(d => console.log(describe(d)));
report.hits.forEach(h => console.log(h.id, h.detail));
<script type="module">
  import * as Pavise from "https://usepavise.vercel.app/lib/pavise.js";
  const { report } = Pavise.inspect(document.querySelector("#tx").value);
  document.querySelector("#out").textContent = report.verdict + " " + report.score;
</script>
# Node 18+: save the file, then import it as a module
curl -o pavise.mjs https://usepavise.vercel.app/lib/pavise.js
node -e 'import("./pavise.mjs").then(P => console.log(P.inspect(process.argv[1]).report.verdict))' <BASE64_TX>

Try it

This block imports the module from ../lib/pavise.js, the same file the snippet above loads. Press a sample or paste your own transaction.

Live · pavise.js

  
Start / How it works

How it works

Four passes over the same bytes. Each one can only add information, none of them changes the transaction.

1 · Parse

The payload is decoded from base64 or base58 and split into signatures, header, account keys, lifetime, instructions and lookup tables. Pavise knows all three layouts in use on mainnet today. See Transaction formats.

2 · Resolve

Versioned v0 transactions can load accounts from address lookup tables. Those addresses are not in the message, so Pavise reads the tables on mainnet and puts the real addresses back before any rule runs.

3 · Decode and assess

Every instruction of a known program is decoded into a name, arguments and roles: from, to, delegate, newAuthority. Then 21 rules look at the roles. Most of them ask one question: does this give something to an address that is not one of the signers?

4 · Simulate

Last, the same message runs through simulateTransaction on mainnet with signature checks off. The SOL balance of up to ten writable accounts is read before and after. See Simulation.

NoteThe score never depends on the simulation. A drainer that fails to simulate is still a drainer, and a clean simulation does not clear a SetAuthority.
Firewall / Transaction formats

Transaction formats

Solana has three wire formats in use. Pavise detects them from the first bytes and parses each one.

Legacy

A compact-u16 count of signatures, the signatures, then the message: three header bytes, account keys, recent blockhash and instructions. All accounts sit in the message.

Versioned v0

Same as legacy, but the message starts with 0x80 and ends with address lookup tables: for each table, the indexes of writable and read-only accounts it contributes.

Versioned v1

Starts with 0x81 and drops the signature prefix: the message comes first and the signatures last. Compute budget settings moved out of instructions into a config mask with fixed slots.

FieldSizeMeaning
versionu80x81
header3 × u8required signatures, read-only signed, read-only unsigned
config masku32which config slots follow (bits 0–1 priority price, 2 compute limit, 3 loaded data size, 4 heap)
lifetime32 bytesrecent blockhash
countsu8, u8instructions, addresses
addressesn × 32account keys
config valuesk × u32one per set bit of the mask
instruction headersn × 4program index, account count, data length (u16)
payloadsvariesaccount indexes then data, per instruction
signaturess × 64at the end
WarningThe v1 layout above was read from live mainnet transactions and checked against what RPC returns for the same signature. If the network changes it again, Pavise refuses to parse rather than guess.

Accepted input

  • Full transaction in base64 (what dApps and Blinks return).
  • Full transaction in base58 (what some explorers copy).
  • Bare message bytes (what tx.serializeMessage() or tx.message.serialize() return).
  • A Uint8Array or plain array of bytes.
Firewall / Risk rules

Risk rules

Each rule that fires adds its weight. The score is the sum, capped at 100. The table is generated from the RULES export of the library you are reading this with.

All rules

RuleWeightLevelWhat it means

“Not you”

Most rules compare an address with the signers. The owner is the fee payer unless you pass owner, or set a wallet in the policy vault. Any address that signs the transaction counts as you, so a transfer between two of your own signing wallets does not fire.

Thresholds

  • Unlimited approve: amount ≥ 263.
  • Large SOL out: more than 1 SOL in plain transfers from a signer to a non-signer.
  • Token sweep: two or more token transfers to the same outside account.
  • High priority fee: price × compute limit above 0.01 SOL.
  • Unknown programs: counted for the first three only.
Firewall / Verdicts & policy

Verdicts and policy

The score maps to three verdicts. Your policy can turn any of them into a block.

Verdicts

ScoreVerdictMeaning
0–19CLEARNothing hands over funds or authority.
20–49CAUTIONMoves value or grants a right. Read every line.
50–100BLOCKTakes control of funds or authority.

Policy vault

The vault on the site stores four fields in localStorage under pavise.policy: wallet, 24 h SOL cap, maximum risk and a frozen flag, plus a ledger of spends you confirmed with I signed it.

const report = assess(tx, {
  owner: "YourWallet…",
  policy: { dailyCapSol: 5, spentSol: 1.2, maxRisk: 30, frozen: false }
});
report.blockedByPolicy;   // ["POLICY_SPEND_CAP"] when SOL out > 3.8
  • Frozen blocks every transaction that moves anything.
  • Spend cap blocks when plain SOL out is more than what is left of the 24 h cap.
  • Max risk blocks a CAUTION above your number. A BLOCK stays a BLOCK with its own reason.
NoteThis is a policy for your eyes, not an on-chain lock. It cannot stop a wallet you sign with elsewhere. It stops you from signing here without seeing that you went over.
Firewall / Simulation

Simulation

The transaction runs against the current mainnet state, without a signature, and nothing is sent.

const sim = await simulate(base64Tx);
sim.ok;            // false if a program returned an error
sim.deltas;        // [{ account, before, after, delta }] in SOL
sim.logs;          // program log lines
sim.unitsConsumed;
curl -s https://solana-rpc.publicnode.com -H 'content-type: application/json' -d '{
  "jsonrpc":"2.0","id":1,"method":"simulateTransaction",
  "params":["<BASE64_TX>",{"encoding":"base64","sigVerify":false,"replaceRecentBlockhash":true}]
}'
  • sigVerify: false and replaceRecentBlockhash: true, so unsigned and stale transactions still run.
  • Up to 10 writable accounts are watched: that is the limit of the public RPC.
  • Samples on the site are built with made-up accounts, so they skip simulation.
WarningA simulation is a snapshot. A program can behave differently a second later, and a durable nonce transaction can be sent days later. That is why the score does not trust it.
Tools / Mint audit

Mint audit

Who can still change a token after you buy it.

const a = await auditMint("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263");
a.score; a.verdict;   // 0 "CLEAR"
a.checks;             // [{ id, ok, weight, text }]
a.market;             // { symbol, priceUsd, liquidityUsd, … } from DexScreener
CheckWeightFires when
MINT_AUTHORITY20Someone can still print more supply.
FREEZE_AUTHORITY20Someone can freeze your token account.
PERMANENT_DELEGATE35Token-2022: one wallet can move tokens from every holder.
TRANSFER_HOOK15Token-2022: every transfer calls another program.
TRANSFER_FEE15Token-2022: transfer fee above 1%.
DEFAULT_FROZEN25New token accounts start frozen.
NON_TRANSFERABLE40The token cannot move at all.
PAUSABLE20An authority can pause the token.
LIQUIDITY10The deepest pool holds less than $10,000, or there is none.

Stablecoins like USDC keep mint and freeze authority on purpose and score CAUTION. That is a correct reading: the issuer can freeze you, and you should know.

Tools / X-ray & Watchman

X-ray and Watchman

Paste anything with a base58 shape and Pavise says what it is.

InputKindWhat you get
87–88 char signaturetransactionLanded or failed, slot, fee, decoded instructions, score, SOL changes
WalletwalletSOL balance and live token delegates
Token mintmintFull mint audit
Token accounttoken-accountOwner, mint, amount, delegate
ProgramprogramName if known, loader
Anything elsedata / emptyOwner program and size, or nothing
const x = await xray("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4");
x.kind;   // "program"
x.text;   // "Jupiter Aggregator v6. Executable code, owned by Upgradeable Loader."

Watchman on the site is the same function with a chat log around it. It is rule-based: no model reads your input and nothing is stored.

Tools / Intent check

Intent check

Say what you want in plain words, see the price and whether your policy allows it, before any app builds a transaction.

PatternExample
swap|sell|convert N A to|for Bswap 2 SOL to USDC
buy N A of Bbuy 1 SOL of BONK
send|pay N A to ADDRESSsend 0.5 SOL to 7xKX…

Tokens by ticker: SOL, USDC, USDT, JUP, BONK, WIF, JTO, PYTH, RAY. Anything else by mint address.

const r = await quoteIntent(parseIntent("swap 2 SOL to USDC"), { dailyCapSol: 5 });
r.allowed;          // true
r.quote.out;        // USDC you get
r.quote.impactPct;  // above 5% the intent is blocked
curl -s "https://lite-api.jup.ag/swap/v1/quote?inputMint=So11111111111111111111111111111111111111112&outputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&amount=2000000000&slippageBps=50"
Tools / Blinks

Blinks

A Blink is a Solana Action behind a link. X renders it as a card with buttons, and each button returns a transaction to sign. Pavise opens the same card and checks that transaction.

Flow

  1. GET the action URL: title, icon, description and buttons.
  2. Press a button: POST {"account": "<your wallet>"} to its href.
  3. The action answers {"transaction": "<base64>"}, and Pavise inspects it like any other.

Links can be a direct URL, solana-action:https://…, or a page URL with ?action=. Your wallet address is sent to the action only when you press a button.

Our check-in Blink

Pavise serves its own action at /api/actions/verify, listed in /actions.json. It returns a zero-value Memo transaction signed by you, so the whole Blink flow can be watched with nothing at stake. It scores 0 and simulates clean.

curl -s https://usepavise.vercel.app/api/actions/verify
curl -s -X POST https://usepavise.vercel.app/api/actions/verify \
  -H 'content-type: application/json' -d '{"account":"<YOUR_WALLET>"}'
const b = await fetchBlink("solana-action:https://usepavise.vercel.app/api/actions/verify");
const r = await runBlink(b.meta.links.actions[0].href, wallet);
r.report.verdict;  // "CLEAR"
WarningThe Actions spec requires CORS, but some actions only answer known hosts. If a Blink fails to load here, that is the reason, and it still deserves the same suspicion.
Tools / Delegate revoker

Delegate revoker

Every Approve you ever signed leaves a delegate on a token account until you revoke it. Quiet drains months later come from these.

const list = await delegations(wallet);      // SPL Token + Token-2022
// [{ account, mint, delegate, delegated, amount, unlimited, program }]
const t = await buildRevoke(wallet, list);  // unsigned, fresh blockhash
t.base64;  // sign it in any wallet tool that accepts a raw transaction

The transaction has one Revoke (tag 5) per account and nothing else. Paste it back into the firewall and it reads CLEAR. The blockhash expires in about a minute, so build it right before you sign.

NotePublic RPCs refuse getTokenAccountsByOwner to browsers, so this one call goes through /api/rpc on this site: a read-only proxy that forwards that single method, in jsonParsed form, and returns only the token accounts that still have a delegate.
Guard / Chrome guard

Chrome guard

The guard stands between a dApp and your wallet. It sees the transaction at the moment a site asks for a signature, before the wallet popup.

Install

  1. Download pavise-guard-v0.1.0.zip and unzip it.
  2. Open chrome://extensions and switch on Developer mode.
  3. Press Load unpacked and pick the pavise-guard folder.

What it wraps

signTransaction, signAllTransactions, signAndSendTransaction and signAndSendAllTransactions of window.phantom.solana, window.solflare, window.backpack and window.solana. The guard serializes the message, runs the same inspect() and, if the score is above 0, shows a card. Block rejects with code 4001, the same error a wallet gives when you press Cancel. Continue hands the untouched transaction to the wallet.

FileWhat it is
manifest.jsonMV3, one content script in the page's main world, no permissions
pavise.jsThe engine, the same code as lib/pavise.js without network calls
guard.jsWraps providers and draws the card in a closed shadow root
WarningWallet Standard adapters that never touch the injected providers are not covered in v0.1.0, and neither is signMessage. The guard reads transactions offline: no RPC, no simulation, no lookup tables.
Reference / API reference

API reference

Everything lib/pavise.js exports. Offline functions work anywhere, network ones call public endpoints.

Offline

FunctionReturns
parseTransaction(payload){ version, feePayer, signatures, signed, accounts, instructions, lookups, compute, messageBytes }
decodeInstruction(ix){ program, programId, known, name, args, roles }
assess(tx, { owner, policy }){ score, verdict, summary, hits, decoded, solOut, tokenTransfers, fee, blockedByPolicy }
inspect(payload, opts){ tx, report }
describe(decoded)One English line
buildTransaction({ feePayer, recentBlockhash, instructions }){ bytes, base64, message }, unsigned legacy
ix.*Builders: solTransfer, assign, advanceNonce, tokenTransfer, approve, revoke, setAuthority, closeAccount, computeLimit, computePrice, memo, loaderSetAuthority
samples()Seven ready transactions: drainer, clean, approve, nonce, sweep, authority, assign
parseIntent(text){ action, amount, from, to }
b58encode / b58decode / b64encode / b64decodeBytes helpers
isAddress(s) / isSignature(s)Boolean

Network

FunctionCalls
rpc(method, params)Solana JSON-RPC, publicnode by default
setRpc(url)Use your own RPC for everything
resolveLookups(tx)getMultipleAccounts on lookup tables
simulate(payload, tx?)simulateTransaction + balances
auditMint(mint)RPC + DexScreener
xray(input)RPC, plus auditMint or delegations
delegations(wallet)getTokenAccountsByOwner via /api/rpc
buildRevoke(wallet, list)getLatestBlockhash
fetchBlink(url) / runBlink(href, account)The action itself
quoteIntent(intent, policy)Jupiter quote API

Constants

RULES, VERDICTS, PROGRAMS (23 labelled programs), TOKENS, SYSTEM, TOKEN, TOKEN22, COMPUTE, MEMO, LOADER, STAKE, U64_MAX, VERSION.

Reference / Errors

Errors

What each message means and what to do.

MessageCauseFix
Empty payloadNothing pastedPaste the transaction string
Not base64 or base58Characters outside both alphabetsCopy the raw transaction, not a JSON wrapper
Truncated …The bytes end too earlyThe string was cut while copying
v1 length mismatchExtra bytes after a v1 messageReport it: the format may have changed
A maximum of 10 accounts is allowedPublic RPC limitUse setRpc() with your own RPC
Request blockedThe public RPC refuses this method to browsersUse the site, or setRpc()
Action returned no transactionThe Blink answered without transactionThe action needs other parameters or is down
No route foundJupiter has no path for this pair or sizeLower the amount or check the mint
Reference / Threat model & limits

Threat model and limits

What Pavise catches, and what it cannot.

Catches

  • Authority handoffs: token account owner, mint and freeze authority, program upgrade authority, stake authority, nonce authority.
  • Wallet reassignment through Assign.
  • Unlimited and outside delegates.
  • Sweeps: several token transfers to one outside account, rent sent elsewhere on close.
  • Durable nonces that let a signature wait for weeks.
  • Bait memos that talk about airdrops while funds leave.

Does not catch

  • What an unknown program does with your accounts. Pavise names it as unread and the simulation shows SOL deltas, but token deltas inside a custom program are not decoded.
  • Phishing that never asks for a signature, such as a fake site asking for your seed phrase.
  • A signMessage request that is later replayed off-chain.
  • A compromised machine. If malware controls the page, it controls Pavise too.
WarningA CLEAR verdict means none of the rules fired. It does not mean the dApp is honest. A clean transfer to the wrong address is still clean.
Token / $PAVISE

$PAVISE

The tool is free and stays free. The token is how the people who use it say so, and how the next feature gets voted onto the list.

Ticker$PAVISE
ChainSolana
Contractsoon
LinksBuy $PAVISE Chart X

The contract above comes from the same settings line as the main page. If someone sends you another address, Pavise would flag that before anything else.

NoteAudit our mint the way you audit any other: paste it into Mint audit and read every line.
Token / FAQ

FAQ

Does Pavise see my transactions?

No server of ours does. Parsing and scoring happen in your browser. Simulation and lookups go straight from your browser to a public RPC. The one proxied call, getTokenAccountsByOwner, carries a wallet address and nothing else.

Can it sign or send for me?

No. It has no keys and asks for none. It builds unsigned transactions, like the revoke, and you sign them in your wallet.

Why did a normal swap get a few points?

Swaps go through programs Pavise does not decode in full and load accounts from lookup tables. That adds 4 points per unknown program, three at most, and 4 for lookup tables. It stays CLEAR.

Why is USDC CAUTION in the mint audit?

Because the issuer can mint and freeze. That is by design for a regulated stablecoin, and still worth knowing.

Is the score an AI guess?

No. It is 21 fixed rules with fixed weights, listed in Risk rules. Same bytes, same score, every time.

Can I run my own copy?

Yes. Save lib/pavise.js, call setRpc() with your RPC and you have the whole engine. MIT.