What it is
Said-No is a personal consent firewall for Sui. Most wallets remember approvals; Said-No remembers the sends you rejected, with your own reason, and raises the friction on similar sends later.
It does not decide what is dangerous. It asks whether you still agree with a decision you already made, and you stay in control.
Architecture
pending send (recipient, amount)
│
┌─────────────────┴─────────────────┐
▼ ▼
Tatum Sui gRPC Walrus Memory
balance, history, your past decisions
dry-run, execute with typed reasons
│ │
└─────────────────┬─────────────────┘
▼
similarity + friction ladder
│
normal · reminder · reflection · hold
│
you approve, or you say no
│
a refusal becomes a new memoryTatum is reached over native gRPC, because its gateway does not serve grpc-web. Sui JSON-RPC has been shut down. Walrus Memory is reached through its managed relayer, and has no working testnet relayer, so it always uses mainnet.
How a verdict is made
Each stored decision is compared with the pending send using fixed, printed weights:
similarity = 0.35 × recipient (both new, or both known)
+ 0.30 × amount (closeness of size against your usual send)
+ 0.20 × hour (same band: night 00-05, or day)
+ 0.15 × interaction (same bucket: 0, 1-2, or 3+ past sends)| Level | When |
|---|---|
| Normal | No similar refusal (similarity below 0.40) |
| Reminder | Best similar refusal between 0.40 and 0.69 |
| Reflection | Best between 0.70 and 0.84, or a single refusal at 0.85 and above |
| Hold | Two or more refusals with one at 0.85 and above, or a hard memory on this recipient |
Approvals count against refusals. More approvals than refusals returns Normal; equal numbers drop one level. Any missing input (Tatum down, Memory unreadable, failed dry-run) raises the verdict to at least Reflection.
API
| Route | Does | Body |
|---|---|---|
GET /api/status | Network, memory backend, what is enabled | None |
POST /api/check | Verdict, chain facts, request log. Sends nothing. | recipient, amount, hour? |
POST /api/decide | Saves a decision to the signed-in wallet's own memory. Returns a job, about 25 s on Walrus. | Bearer token; action, recipient, amount, reason, kind, features, source?, target? |
GET /api/jobs/:id | Blob id once saved; 202 until then | Bearer token |
GET /api/memories | The signed-in wallet's own decisions and the count of unreadable ones. Empty without a token. | Bearer token |
POST /api/send | Re-checks on the server, then executes. Needs ALLOW_EXECUTE=true, 0.1 SUI maximum. | recipient, amount, acknowledged, hour? |
curl -X POST http://localhost:3200/api/check \
-H "content-type: application/json" \
-d '{"recipient":"0xabab...","amount":0.5}'Run it
cp .env.example .env # fill in the keys pnpm install pnpm test pnpm dev -p 3200 # real Walrus Memory: writes go to mainnet node scripts/dev-local.mjs 3201 # local memory: nothing leaves the machine
Visitors use their own wallet. The server holds no key that can spend: it checks the send, builds an unsigned transfer, and broadcasts what the visitor's wallet signed. Each wallet signs a message to unlock its own memories, kept in a separate Walrus Memory namespace. ALLOW_WRITES and ALLOW_EXECUTE switch saving and sending off for a read-only deployment.
What we do not claim
- This is counted statistics with fixed weights, not machine learning.
- It does not detect scams or say a transaction is unsafe. A label is your opinion, not an accusation.
- A denied send has no transaction. The proof is the memory blob, not a digest.
- Memories are Seal-encrypted by the relayer, which also decrypts them server-side. This is not zero-trust.
- A public deployment never shows or uses entries labelled real: it ignores them and reports only a count. Set
EXPOSE_PRIVATE=trueon a private deployment to see and use them. - Delegate keys are account-wide and can read and write. Authorise only agents you trust.
- Requests are rate limited per visitor (for example 10 checks a minute). The counter lives in one server instance, so on serverless hosting it slows a single visitor but is not a hard global cap.
- The recorded run on the check page is a recording of one real session, not a live demo. Its verdicts are re-computed by the real engine on every page load, so it cannot drift from the code.
- Transaction history starts at the node's earliest available data, so it may be incomplete.
- Demo data is labelled. Only memories marked real count toward the hackathon's requirement.