veralayer 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +113 -3
- package/dist/agent-kit.d.ts +27 -0
- package/dist/agent-kit.js +65 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +124 -0
- package/dist/client.d.ts +69 -0
- package/dist/client.js +106 -0
- package/dist/guard.d.ts +71 -0
- package/dist/guard.js +115 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/inspect.d.ts +29 -0
- package/dist/inspect.js +125 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.js +103 -0
- package/dist/policy.d.ts +104 -0
- package/dist/policy.js +112 -0
- package/dist/programs.d.ts +1 -0
- package/dist/programs.js +37 -0
- package/package.json +82 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VeraLayer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,113 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# veralayer
|
|
2
|
+
|
|
3
|
+
A payment guard for AI agents on Solana. It sits between your agent's wallet and the network and checks every outgoing transaction:
|
|
4
|
+
|
|
5
|
+
- **Decodes the transaction.** It finds where value goes: SOL transfers, SPL and Token-2022 transfers (each token account is resolved to the wallet that owns it), v0 lookup tables, and every program called.
|
|
6
|
+
- **Scores each recipient.** Every recipient is checked with [Vera](https://veraagent.io). The score is built from on-chain history, and every reason cites the fact behind it.
|
|
7
|
+
- **Verifies the signature.** Each result is signed by Vera. The guard verifies that signature locally against Vera's published ed25519 key, so a tampered score is rejected.
|
|
8
|
+
- **Applies your policy.** It returns `allow`, `review` or `block`, with a finding for each rule that fired. Only allowed transactions are signed and sent.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install veralayer @solana/web3.js
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Guard a wallet
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Connection } from '@solana/web3.js';
|
|
18
|
+
import { VeraGuard, VeraBlockedError } from 'veralayer';
|
|
19
|
+
|
|
20
|
+
const guard = new VeraGuard({
|
|
21
|
+
connection: new Connection(process.env.RPC_URL!),
|
|
22
|
+
policy: {
|
|
23
|
+
minTier: 'medium', // block low-trust recipients
|
|
24
|
+
tierLimitsSol: { high: 50, medium: 5 }, // max SOL per recipient per tx, by tier
|
|
25
|
+
dailyLimitSol: 20, // across all transactions today
|
|
26
|
+
tokenLimits: { EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: 500 }, // 500 USDC per tx
|
|
27
|
+
deny: ['<known bad address>'],
|
|
28
|
+
allow: ['<your treasury>'],
|
|
29
|
+
},
|
|
30
|
+
onReview: async (decision) => askHumanInTelegram(decision), // optional; without it, review = block
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const { signature, decision } = await guard.sendTransaction(tx, [agentKeypair]);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
if (e instanceof VeraBlockedError) console.log(e.decision.findings); // why it was stopped
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Decide before you build a transaction
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const d = await guard.evaluatePayment({ to: seller, amountSol: 1.5 });
|
|
44
|
+
if (d.action !== 'allow') return refuse(d.findings[0].message);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Just score an address
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { Vera } from 'veralayer';
|
|
51
|
+
const vera = new Vera();
|
|
52
|
+
const r = await vera.check(address); // { attestation: { score, tier, reasons, ... }, signature, summary }
|
|
53
|
+
await vera.verify(r); // { valid: true, expired: false }, checked offline
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Use it from Claude, Cursor or any MCP client
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{ "mcpServers": { "vera": { "command": "npx", "args": ["-y", "veralayer", "mcp"] } } }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Tools: `vera_check(address)` and `vera_guard_payment(to, amount_sol)`.
|
|
63
|
+
|
|
64
|
+
## Solana Agent Kit
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { createVeraPlugin } from 'veralayer/agent-kit';
|
|
68
|
+
agent.use(createVeraPlugin({ policy: { minTier: 'medium' } }));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Full docs: https://veraagent.io/docs
|
|
72
|
+
|
|
73
|
+
## CLI
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx veralayer check <address>
|
|
77
|
+
npx veralayer pay <address> 2.5 --min-tier medium --daily 20
|
|
78
|
+
npx veralayer inspect <base64 transaction> --rpc <url>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Policy reference
|
|
82
|
+
|
|
83
|
+
| Field | Default | Effect |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `minTier` | `'medium'` | Recipients below this tier are blocked. |
|
|
86
|
+
| `tierLimitsSol` | `{ high: ∞, medium: 5, low: 0 }` | Max SOL to one recipient per transaction. Over the limit → `review`; a limit of 0 → `block`. |
|
|
87
|
+
| `maxSolPerTx` | none | Total SOL per transaction → `block`. |
|
|
88
|
+
| `dailyLimitSol` | none | Total SOL per UTC day (ledger) → `block`. |
|
|
89
|
+
| `tokenLimits` | none | Per-mint cap in UI units → `review`. |
|
|
90
|
+
| `allow` / `deny` | `[]` | Skip scoring / always block. |
|
|
91
|
+
| `onUnknown` | `'review'` | Recipients with no history. |
|
|
92
|
+
| `onCheckError` | `'block'` | API failure, bad signature, or a result for another address. |
|
|
93
|
+
| `unknownPrograms` | `'review'` | Programs outside the well-known list. |
|
|
94
|
+
| `knownPrograms` | `'review'` | Well-known programs that move value the guard doesn't decode (swaps). Set `'allow'` if your agent swaps. |
|
|
95
|
+
| `riskyInstructions` | `'block'` | `approve`, `setAuthority`, `closeAccount` to someone else, or `assign`, signed by the wallet. |
|
|
96
|
+
|
|
97
|
+
A policy with a typo throws when the guard is created, rather than silently weakening a check.
|
|
98
|
+
|
|
99
|
+
## Trust
|
|
100
|
+
|
|
101
|
+
- **Pinned key.** Results must be signed by Vera's production key, which ships as `VERA_ISSUER`. For a self-hosted Vera, pass `vera: { apiUrl, issuer }`.
|
|
102
|
+
- **Address check.** A result for a different address than the one requested is rejected.
|
|
103
|
+
- **Bad amounts.** NaN, negative and infinite amounts block.
|
|
104
|
+
- **Daily limits.** Spend is reserved before sending and released if the send fails. Concurrent sends are serialised.
|
|
105
|
+
- **Relayers.** If a relayer pays fees, pass `wallet` so that only the agent's own transfers count.
|
|
106
|
+
|
|
107
|
+
## What it does not do (yet)
|
|
108
|
+
|
|
109
|
+
- **Value inside other programs.** Value moved inside swaps and other CPIs is not decoded. Those programs are listed, and unknown ones trigger `review`.
|
|
110
|
+
- **On-chain anchoring.** Vera's results are signed but not yet anchored on-chain.
|
|
111
|
+
- **Guarantees.** A score reads history, not intent. It is a risk signal, not a guarantee.
|
|
112
|
+
|
|
113
|
+
MIT · by [VeraLayer](https://veralayer.io)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Policy } from './policy.js';
|
|
3
|
+
type Action = {
|
|
4
|
+
name: string;
|
|
5
|
+
similes: string[];
|
|
6
|
+
description: string;
|
|
7
|
+
examples: {
|
|
8
|
+
input: Record<string, unknown>;
|
|
9
|
+
output: Record<string, unknown>;
|
|
10
|
+
explanation: string;
|
|
11
|
+
}[][];
|
|
12
|
+
schema: z.ZodTypeAny;
|
|
13
|
+
handler: (agent: unknown, input: Record<string, any>) => Promise<Record<string, any>>;
|
|
14
|
+
};
|
|
15
|
+
type Plugin = {
|
|
16
|
+
name: string;
|
|
17
|
+
methods: Record<string, any>;
|
|
18
|
+
actions: Action[];
|
|
19
|
+
initialize(agent: unknown): void;
|
|
20
|
+
};
|
|
21
|
+
export declare function createVeraPlugin(opts?: {
|
|
22
|
+
apiUrl?: string;
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
issuer?: string;
|
|
25
|
+
policy?: Policy;
|
|
26
|
+
}): Plugin;
|
|
27
|
+
export default createVeraPlugin;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Solana Agent Kit v2 plugin.
|
|
2
|
+
//
|
|
3
|
+
// import { SolanaAgentKit } from 'solana-agent-kit';
|
|
4
|
+
// import { createVeraPlugin } from 'veralayer/agent-kit';
|
|
5
|
+
// const agent = new SolanaAgentKit(wallet, rpcUrl, {}).use(createVeraPlugin({ policy: { minTier: 'medium' } }));
|
|
6
|
+
// await agent.methods.veraGuardPayment(to, 1.5);
|
|
7
|
+
//
|
|
8
|
+
// Types are structural, so this file needs zod but not solana-agent-kit itself.
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { Vera } from './client.js';
|
|
11
|
+
import { VeraGuard } from './guard.js';
|
|
12
|
+
export function createVeraPlugin(opts = {}) {
|
|
13
|
+
const vera = new Vera(opts);
|
|
14
|
+
const guard = new VeraGuard({ vera, policy: opts.policy });
|
|
15
|
+
const check = async (address) => {
|
|
16
|
+
const r = await vera.check(address);
|
|
17
|
+
const v = await vera.verify(r);
|
|
18
|
+
const a = r.attestation;
|
|
19
|
+
return { address, score: a.score, tier: a.tier, reasons: a.reasons, summary: r.summary, attestation_id: r.id, signature_valid: v.valid };
|
|
20
|
+
};
|
|
21
|
+
const actions = [
|
|
22
|
+
{
|
|
23
|
+
name: 'VERA_TRUST_CHECK',
|
|
24
|
+
similes: ['check counterparty', 'is this address safe to pay', 'trust score', 'vet wallet before payment'],
|
|
25
|
+
description: 'Score a Solana address with Vera before sending it funds: 0-100 score, tier, reasons, and whether the signed result verified.',
|
|
26
|
+
examples: [[{ input: { address: '<counterparty address>' }, output: { status: 'success', score: 55, tier: 'medium' }, explanation: 'Illustrative: score a recipient before paying it' }]],
|
|
27
|
+
schema: z.object({ address: z.string().min(32).max(44) }),
|
|
28
|
+
handler: async (_agent, input) => {
|
|
29
|
+
try {
|
|
30
|
+
return { status: 'success', ...(await check(String(input.address))) };
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
return { status: 'error', message: e.message };
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'VERA_GUARD_PAYMENT',
|
|
39
|
+
similes: ['should I pay', 'guard payment', 'check payment against policy'],
|
|
40
|
+
description: 'Decide whether a planned SOL payment may go ahead under the configured spending policy: allow, review or block, with the rule that fired.',
|
|
41
|
+
examples: [[{ input: { to: '<recipient>', amountSol: 1 }, output: { status: 'success', action: 'allow', findings: [] }, explanation: 'Illustrative: gate a 1 SOL payment' }]],
|
|
42
|
+
schema: z.object({ to: z.string().min(32).max(44), amountSol: z.number().positive() }),
|
|
43
|
+
handler: async (_agent, input) => {
|
|
44
|
+
try {
|
|
45
|
+
const d = await guard.evaluatePayment({ to: String(input.to), amountSol: Number(input.amountSol) });
|
|
46
|
+
return { status: 'success', action: d.action, findings: d.findings };
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
return { status: 'error', message: e.message };
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
return {
|
|
55
|
+
name: 'vera',
|
|
56
|
+
methods: {
|
|
57
|
+
veraCheck: check,
|
|
58
|
+
veraRequire: (address, minTier = 'medium') => vera.require(address, minTier),
|
|
59
|
+
veraGuardPayment: (to, amountSol) => guard.evaluatePayment({ to, amountSol }),
|
|
60
|
+
},
|
|
61
|
+
actions,
|
|
62
|
+
initialize() { },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export default createVeraPlugin;
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npx veralayer check <address> score one address
|
|
3
|
+
// npx veralayer pay <address> <sol> what the guard would decide for a SOL payment
|
|
4
|
+
// npx veralayer inspect <base64 tx> decode a transaction and decide on it
|
|
5
|
+
// options: --min-tier high|medium|low --daily <sol> --max-tx <sol> --json --api <url> --rpc <url>
|
|
6
|
+
import { Vera } from './client.js';
|
|
7
|
+
import { VeraGuard } from './guard.js';
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
// supports --name value and --name=value
|
|
10
|
+
const flags = new Map();
|
|
11
|
+
const pos = [];
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const a = args[i];
|
|
14
|
+
if (a === '--json') {
|
|
15
|
+
flags.set('json', '1');
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (a.startsWith('--')) {
|
|
19
|
+
const [k, v] = a.slice(2).split(/=(.*)/s);
|
|
20
|
+
if (v !== undefined)
|
|
21
|
+
flags.set(k, v);
|
|
22
|
+
else
|
|
23
|
+
flags.set(k, args[++i] ?? '');
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
pos.push(a);
|
|
27
|
+
}
|
|
28
|
+
const flag = (name) => flags.get(name);
|
|
29
|
+
const json = flags.has('json');
|
|
30
|
+
const [cmd, a1, a2] = pos;
|
|
31
|
+
const num = (name) => {
|
|
32
|
+
const v = flag(name);
|
|
33
|
+
if (v === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
const n = Number(v);
|
|
36
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
37
|
+
console.error(`--${name} must be a non-negative number`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
return n;
|
|
41
|
+
};
|
|
42
|
+
const minTier = flag('min-tier');
|
|
43
|
+
if (minTier !== undefined && !['high', 'medium', 'low'].includes(minTier)) {
|
|
44
|
+
console.error('--min-tier must be high, medium or low');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const daily = num('daily'), maxTx = num('max-tx');
|
|
48
|
+
const policy = {
|
|
49
|
+
...(minTier ? { minTier: minTier } : {}),
|
|
50
|
+
...(daily !== undefined ? { dailyLimitSol: daily } : {}),
|
|
51
|
+
...(maxTx !== undefined ? { maxSolPerTx: maxTx } : {}),
|
|
52
|
+
};
|
|
53
|
+
const vera = new Vera({ apiUrl: flag('api') ?? process.env.VERA_API_URL, apiKey: process.env.VERA_API_KEY, issuer: process.env.VERA_ISSUER });
|
|
54
|
+
const c = process.stdout.isTTY ? { r: '\x1b[31m', g: '\x1b[32m', y: '\x1b[33m', d: '\x1b[2m', b: '\x1b[1m', x: '\x1b[0m' } : { r: '', g: '', y: '', d: '', b: '', x: '' };
|
|
55
|
+
const tierColor = (t) => (t === 'high' ? c.g : t === 'medium' ? c.y : t === 'low' ? c.r : c.d);
|
|
56
|
+
const glyph = (k) => (k === 'ok' ? `${c.g}✓${c.x}` : k === 'warn' ? `${c.y}!${c.x}` : k === 'bad' ? `${c.r}×${c.x}` : `${c.d}i${c.x}`);
|
|
57
|
+
function printDecision(d) {
|
|
58
|
+
const col = d.action === 'allow' ? c.g : d.action === 'review' ? c.y : c.r;
|
|
59
|
+
console.log(`\n${c.b}${col}${d.action.toUpperCase()}${c.x} ${d.totalSol ? `${d.totalSol} SOL out` : ''}`);
|
|
60
|
+
for (const o of d.outflows)
|
|
61
|
+
console.log(` ${c.d}→${c.x} ${o.amount} ${o.kind === 'sol' ? 'SOL' : (o.mint ?? 'token').slice(0, 6) + '…'} to ${o.to}`);
|
|
62
|
+
for (const r of d.recipients) {
|
|
63
|
+
if (!r.ok) {
|
|
64
|
+
console.log(` ${c.r}×${c.x} ${r.address}: ${r.error}`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
console.log(` ${tierColor(r.tier)}${r.tier.toUpperCase()} ${r.score ?? '—'}${c.x} ${r.address} ${r.verified ? `${c.d}signature ✓${c.x}` : `${c.r}signature ✗${c.x}`}`);
|
|
68
|
+
}
|
|
69
|
+
for (const f of d.findings)
|
|
70
|
+
console.log(` ${f.action === 'block' ? c.r : c.y}${f.action}${c.x} ${f.message}`);
|
|
71
|
+
if (!d.findings.length)
|
|
72
|
+
console.log(` ${c.d}no policy findings${c.x}`);
|
|
73
|
+
}
|
|
74
|
+
async function main() {
|
|
75
|
+
if (cmd === 'mcp') {
|
|
76
|
+
const { runMcp } = await import('./mcp.js');
|
|
77
|
+
return runMcp({ apiUrl: flag('api') ?? process.env.VERA_API_URL, apiKey: process.env.VERA_API_KEY });
|
|
78
|
+
}
|
|
79
|
+
if (cmd === 'check' && a1) {
|
|
80
|
+
const r = await vera.check(a1);
|
|
81
|
+
const v = await vera.verify(r);
|
|
82
|
+
if (json)
|
|
83
|
+
return console.log(JSON.stringify({ ...r, verified: v }, null, 2));
|
|
84
|
+
const a = r.attestation;
|
|
85
|
+
console.log(`\n${c.b}${tierColor(a.tier)}${a.score ?? '—'}/100 ${a.tier.toUpperCase()}${c.x} ${a.subject}`);
|
|
86
|
+
if (r.summary)
|
|
87
|
+
console.log(`${c.d}${r.summary}${c.x}`);
|
|
88
|
+
for (const x of a.reasons)
|
|
89
|
+
console.log(` ${glyph(x.kind)} ${x.text}`);
|
|
90
|
+
console.log(`\n${c.d}attestation ${r.id}\nsignature ${v.valid ? 'verified' : 'INVALID'} · ${a.method} · expires ${a.expires_at}${c.x}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (cmd === 'pay' && a1 && a2) {
|
|
94
|
+
if (!Number.isFinite(Number(a2)) || Number(a2) < 0)
|
|
95
|
+
throw new Error(`invalid amount: ${a2}`);
|
|
96
|
+
const d = await new VeraGuard({ vera, policy }).evaluatePayment({ to: a1, amountSol: Number(a2) });
|
|
97
|
+
return json ? console.log(JSON.stringify(d, null, 2)) : printDecision(d);
|
|
98
|
+
}
|
|
99
|
+
if (cmd === 'inspect' && a1) {
|
|
100
|
+
const web3 = await import('@solana/web3.js').catch(() => { throw new Error('inspect needs @solana/web3.js: npm i @solana/web3.js'); });
|
|
101
|
+
const raw = Buffer.from(a1, 'base64');
|
|
102
|
+
let tx;
|
|
103
|
+
try {
|
|
104
|
+
tx = web3.VersionedTransaction.deserialize(raw);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
tx = web3.Transaction.from(raw);
|
|
108
|
+
}
|
|
109
|
+
const connection = new web3.Connection(flag('rpc') ?? process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com', 'confirmed');
|
|
110
|
+
const d = await new VeraGuard({ vera, policy, connection }).evaluate(tx);
|
|
111
|
+
return json ? console.log(JSON.stringify(d, null, 2)) : printDecision(d);
|
|
112
|
+
}
|
|
113
|
+
console.log(`veralayer: payment guard for AI agents on Solana
|
|
114
|
+
|
|
115
|
+
veralayer check <address> score an address (signed, verified locally)
|
|
116
|
+
veralayer pay <address> <sol> decide on a planned SOL payment
|
|
117
|
+
veralayer inspect <base64 tx> decode a transaction and decide on it
|
|
118
|
+
veralayer mcp run as an MCP server over stdio
|
|
119
|
+
|
|
120
|
+
--min-tier high|medium|low --daily <sol> --max-tx <sol> --json
|
|
121
|
+
--api <url> (or VERA_API_URL) --rpc <url> (or SOLANA_RPC_URL)`);
|
|
122
|
+
process.exitCode = cmd ? 1 : 0;
|
|
123
|
+
}
|
|
124
|
+
main().then(() => { }, (e) => { console.error(`${c.r}error${c.x} ${e.message}`); process.exitCode = 1; });
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type Tier = 'high' | 'medium' | 'low' | 'unknown';
|
|
2
|
+
export type Attestation = {
|
|
3
|
+
v: 1;
|
|
4
|
+
issuer: string;
|
|
5
|
+
subject: string;
|
|
6
|
+
score: number | null;
|
|
7
|
+
tier: Tier;
|
|
8
|
+
reasons: {
|
|
9
|
+
kind: 'ok' | 'warn' | 'bad' | 'info';
|
|
10
|
+
text: string;
|
|
11
|
+
}[];
|
|
12
|
+
features_sha256: string;
|
|
13
|
+
method: string;
|
|
14
|
+
issued_at: string;
|
|
15
|
+
expires_at: string;
|
|
16
|
+
};
|
|
17
|
+
export type CheckResult = {
|
|
18
|
+
id: string;
|
|
19
|
+
attestation: Attestation;
|
|
20
|
+
signature: string;
|
|
21
|
+
summary: string | null;
|
|
22
|
+
anchor?: {
|
|
23
|
+
cluster: string;
|
|
24
|
+
tx: string;
|
|
25
|
+
} | null;
|
|
26
|
+
features?: Record<string, unknown>;
|
|
27
|
+
};
|
|
28
|
+
export type VerifyResult = {
|
|
29
|
+
valid: boolean;
|
|
30
|
+
expired: boolean;
|
|
31
|
+
reason?: string;
|
|
32
|
+
};
|
|
33
|
+
export declare class VeraError extends Error {
|
|
34
|
+
status?: number | undefined;
|
|
35
|
+
constructor(message: string, status?: number | undefined);
|
|
36
|
+
}
|
|
37
|
+
/** Vera's production signing key. Results signed by anything else are rejected unless you pass your own `issuer`. */
|
|
38
|
+
export declare const VERA_ISSUER = "4VyCQLEb8eVmPYXDUwYkEpCMhXFBRshUiKHHuKTfPDFQ";
|
|
39
|
+
export declare const DEFAULT_API_URL = "https://veraagent.io";
|
|
40
|
+
export declare function canonical(v: unknown): string;
|
|
41
|
+
/** Verify an attestation offline. Pass the trusted Vera issuer key (from /.well-known/vera.json) to pin it. */
|
|
42
|
+
export declare function verifyAttestation(attestation: Attestation, signature: string, trustedIssuer?: string): VerifyResult;
|
|
43
|
+
export declare class Vera {
|
|
44
|
+
private apiUrl;
|
|
45
|
+
private apiKey?;
|
|
46
|
+
private issuer?;
|
|
47
|
+
private timeoutMs;
|
|
48
|
+
/**
|
|
49
|
+
* @param opts.issuer the key results must be signed with. Defaults to Vera's production key (VERA_ISSUER).
|
|
50
|
+
* Pass your own for a self-hosted Vera, or 'fetch' to trust /.well-known/vera.json on apiUrl.
|
|
51
|
+
*/
|
|
52
|
+
constructor(opts?: {
|
|
53
|
+
apiUrl?: string;
|
|
54
|
+
apiKey?: string;
|
|
55
|
+
issuer?: string;
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
});
|
|
58
|
+
private req;
|
|
59
|
+
/** Score a counterparty. Returns score, tier, reasons and a signed attestation. Throws if the result is for a different address. */
|
|
60
|
+
check(address: string, opts?: {
|
|
61
|
+
fresh?: boolean;
|
|
62
|
+
}): Promise<CheckResult>;
|
|
63
|
+
/** The key results must be signed with: pinned (default) or fetched once from /.well-known/vera.json. */
|
|
64
|
+
getIssuer(): Promise<string>;
|
|
65
|
+
/** Verify a check result locally against the trusted issuer key. */
|
|
66
|
+
verify(result: Pick<CheckResult, 'attestation' | 'signature'>): Promise<VerifyResult>;
|
|
67
|
+
/** Convenience gate: throws unless the result verifies, has not expired and meets the minimum tier. */
|
|
68
|
+
require(address: string, minTier?: Exclude<Tier, 'unknown'>): Promise<CheckResult>;
|
|
69
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Vera API client: score an address and verify the signed result locally against Vera's published key.
|
|
2
|
+
//
|
|
3
|
+
// import { Vera } from 'veralayer';
|
|
4
|
+
// const vera = new Vera(); // hosted API, results must be signed with VERA_ISSUER
|
|
5
|
+
// const check = await vera.check(counterparty);
|
|
6
|
+
// if (!(await vera.verify(check)).valid) throw new Error('bad attestation');
|
|
7
|
+
// if (check.attestation.tier === 'low') throw new Error(check.attestation.reasons[0].text);
|
|
8
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
9
|
+
import bs58 from 'bs58';
|
|
10
|
+
export class VeraError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(message, status) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Vera's production signing key. Results signed by anything else are rejected unless you pass your own `issuer`. */
|
|
18
|
+
export const VERA_ISSUER = '4VyCQLEb8eVmPYXDUwYkEpCMhXFBRshUiKHHuKTfPDFQ';
|
|
19
|
+
export const DEFAULT_API_URL = 'https://veraagent.io';
|
|
20
|
+
const TIERS = ['high', 'medium', 'low', 'unknown'];
|
|
21
|
+
// key-sorted JSON: must match the engine byte for byte
|
|
22
|
+
export function canonical(v) {
|
|
23
|
+
if (Array.isArray(v))
|
|
24
|
+
return `[${v.map(canonical).join(',')}]`;
|
|
25
|
+
if (v && typeof v === 'object')
|
|
26
|
+
return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(',')}}`;
|
|
27
|
+
return JSON.stringify(v) ?? 'null';
|
|
28
|
+
}
|
|
29
|
+
/** Verify an attestation offline. Pass the trusted Vera issuer key (from /.well-known/vera.json) to pin it. */
|
|
30
|
+
export function verifyAttestation(attestation, signature, trustedIssuer) {
|
|
31
|
+
const exp = Date.parse(attestation?.expires_at);
|
|
32
|
+
const expired = !(exp > Date.now()); // an unparseable date counts as expired
|
|
33
|
+
if (!attestation || typeof attestation.issuer !== 'string')
|
|
34
|
+
return { valid: false, expired, reason: 'malformed attestation' };
|
|
35
|
+
if (trustedIssuer && attestation.issuer !== trustedIssuer)
|
|
36
|
+
return { valid: false, expired, reason: 'issuer is not the trusted Vera key' };
|
|
37
|
+
if (!TIERS.includes(attestation.tier))
|
|
38
|
+
return { valid: false, expired, reason: `unexpected tier ${String(attestation.tier)}` };
|
|
39
|
+
try {
|
|
40
|
+
const ok = ed25519.verify(bs58.decode(signature), new TextEncoder().encode(canonical(attestation)), bs58.decode(attestation.issuer));
|
|
41
|
+
return ok ? { valid: true, expired } : { valid: false, expired, reason: 'signature does not match' };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { valid: false, expired, reason: 'malformed signature or issuer' };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class Vera {
|
|
48
|
+
apiUrl;
|
|
49
|
+
apiKey;
|
|
50
|
+
issuer;
|
|
51
|
+
timeoutMs;
|
|
52
|
+
/**
|
|
53
|
+
* @param opts.issuer the key results must be signed with. Defaults to Vera's production key (VERA_ISSUER).
|
|
54
|
+
* Pass your own for a self-hosted Vera, or 'fetch' to trust /.well-known/vera.json on apiUrl.
|
|
55
|
+
*/
|
|
56
|
+
constructor(opts = {}) {
|
|
57
|
+
this.apiUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, '');
|
|
58
|
+
this.apiKey = opts.apiKey;
|
|
59
|
+
this.issuer = opts.issuer === 'fetch' ? undefined : (opts.issuer ?? VERA_ISSUER);
|
|
60
|
+
this.timeoutMs = opts.timeoutMs ?? 30_000;
|
|
61
|
+
}
|
|
62
|
+
async req(path, init) {
|
|
63
|
+
let r;
|
|
64
|
+
try {
|
|
65
|
+
r = await fetch(this.apiUrl + path, { ...init, signal: AbortSignal.timeout(this.timeoutMs), headers: { 'content-type': 'application/json', ...(this.apiKey ? { 'x-api-key': this.apiKey } : {}), ...init?.headers } });
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
throw new VeraError(`Vera API unreachable: ${e.message}`);
|
|
69
|
+
}
|
|
70
|
+
const j = (await r.json().catch(() => ({})));
|
|
71
|
+
if (!r.ok)
|
|
72
|
+
throw new VeraError(j.error || `Vera API error ${r.status}`, r.status);
|
|
73
|
+
return j;
|
|
74
|
+
}
|
|
75
|
+
/** Score a counterparty. Returns score, tier, reasons and a signed attestation. Throws if the result is for a different address. */
|
|
76
|
+
async check(address, opts = {}) {
|
|
77
|
+
const r = await this.req('/v1/check', { method: 'POST', body: JSON.stringify({ address, fresh: opts.fresh }) });
|
|
78
|
+
if (r?.attestation?.subject !== address)
|
|
79
|
+
throw new VeraError('Vera returned a result for a different address');
|
|
80
|
+
return r;
|
|
81
|
+
}
|
|
82
|
+
/** The key results must be signed with: pinned (default) or fetched once from /.well-known/vera.json. */
|
|
83
|
+
async getIssuer() {
|
|
84
|
+
this.issuer ??= (await this.req('/.well-known/vera.json')).issuer;
|
|
85
|
+
return this.issuer;
|
|
86
|
+
}
|
|
87
|
+
/** Verify a check result locally against the trusted issuer key. */
|
|
88
|
+
async verify(result) {
|
|
89
|
+
return verifyAttestation(result.attestation, result.signature, await this.getIssuer());
|
|
90
|
+
}
|
|
91
|
+
/** Convenience gate: throws unless the result verifies, has not expired and meets the minimum tier. */
|
|
92
|
+
async require(address, minTier = 'medium') {
|
|
93
|
+
const rank = { unknown: 0, low: 1, medium: 2, high: 3 };
|
|
94
|
+
if (!(minTier in rank))
|
|
95
|
+
throw new VeraError(`invalid minTier: ${minTier}`);
|
|
96
|
+
const r = await this.check(address);
|
|
97
|
+
const v = await this.verify(r);
|
|
98
|
+
if (!v.valid)
|
|
99
|
+
throw new VeraError(`attestation invalid: ${v.reason}`);
|
|
100
|
+
if (v.expired)
|
|
101
|
+
throw new VeraError('attestation expired');
|
|
102
|
+
if (rank[r.attestation.tier] < rank[minTier])
|
|
103
|
+
throw new VeraError(`counterparty tier ${r.attestation.tier} is below ${minTier}: ${r.attestation.reasons[0]?.text ?? ''}`);
|
|
104
|
+
return r;
|
|
105
|
+
}
|
|
106
|
+
}
|
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Connection, Signer, Transaction, VersionedTransaction, SendOptions } from '@solana/web3.js';
|
|
2
|
+
import { Vera } from './client.js';
|
|
3
|
+
import { type Decision, type Policy, type RecipientCheck } from './policy.js';
|
|
4
|
+
/** Where the guard keeps today's spend. Swap in Redis/DB for multi-process agents. */
|
|
5
|
+
export interface SpendLedger {
|
|
6
|
+
spentToday(): Promise<number> | number;
|
|
7
|
+
add(sol: number): Promise<void> | void;
|
|
8
|
+
}
|
|
9
|
+
export declare class MemoryLedger implements SpendLedger {
|
|
10
|
+
private day;
|
|
11
|
+
private spent;
|
|
12
|
+
private roll;
|
|
13
|
+
spentToday(): number;
|
|
14
|
+
add(sol: number): void;
|
|
15
|
+
}
|
|
16
|
+
export declare class VeraBlockedError extends Error {
|
|
17
|
+
decision: Decision;
|
|
18
|
+
constructor(decision: Decision);
|
|
19
|
+
}
|
|
20
|
+
export declare class VeraReviewRequiredError extends Error {
|
|
21
|
+
decision: Decision;
|
|
22
|
+
constructor(decision: Decision);
|
|
23
|
+
}
|
|
24
|
+
export type GuardOptions = {
|
|
25
|
+
connection?: Connection;
|
|
26
|
+
/** The wallet being guarded. Default: each transaction's fee payer. Set it when a relayer pays fees for your agent. */
|
|
27
|
+
wallet?: string;
|
|
28
|
+
policy?: Policy;
|
|
29
|
+
vera?: Vera | {
|
|
30
|
+
apiUrl?: string;
|
|
31
|
+
apiKey?: string;
|
|
32
|
+
issuer?: string;
|
|
33
|
+
};
|
|
34
|
+
ledger?: SpendLedger;
|
|
35
|
+
/** Called for 'review' decisions. Return true to send anyway (e.g. after a human approves in Slack/Telegram). Without it, review = block. */
|
|
36
|
+
onReview?: (decision: Decision) => Promise<boolean> | boolean;
|
|
37
|
+
/** Called after every decision, for logs and audit trails. */
|
|
38
|
+
onDecision?: (decision: Decision) => void;
|
|
39
|
+
};
|
|
40
|
+
export declare class VeraGuard {
|
|
41
|
+
readonly vera: Vera;
|
|
42
|
+
readonly policy: Policy;
|
|
43
|
+
private connection?;
|
|
44
|
+
private wallet?;
|
|
45
|
+
private ledger;
|
|
46
|
+
private onReview?;
|
|
47
|
+
private onDecision?;
|
|
48
|
+
private lock;
|
|
49
|
+
constructor(opts?: GuardOptions);
|
|
50
|
+
/** Score recipients (deduplicated, in parallel) and verify each signed result against Vera's published key. */
|
|
51
|
+
checkRecipients(addresses: string[]): Promise<RecipientCheck[]>;
|
|
52
|
+
/** Decide on a planned payment without building a transaction (e.g. before quoting a price to another agent). */
|
|
53
|
+
evaluatePayment(payment: {
|
|
54
|
+
to: string;
|
|
55
|
+
amountSol?: number;
|
|
56
|
+
token?: {
|
|
57
|
+
mint: string;
|
|
58
|
+
amount: number;
|
|
59
|
+
};
|
|
60
|
+
}): Promise<Decision>;
|
|
61
|
+
/** Decode a transaction and decide on it. Does not send anything. */
|
|
62
|
+
evaluate(tx: Transaction | VersionedTransaction): Promise<Decision>;
|
|
63
|
+
private decide;
|
|
64
|
+
/** Throws VeraBlockedError / VeraReviewRequiredError unless the transaction may be sent. */
|
|
65
|
+
assert(tx: Transaction | VersionedTransaction): Promise<Decision>;
|
|
66
|
+
/** Evaluate, then sign and send only if allowed. The SOL is reserved in the ledger before sending and released if sending fails. */
|
|
67
|
+
sendTransaction(tx: Transaction | VersionedTransaction, signers?: Signer[], options?: SendOptions): Promise<{
|
|
68
|
+
signature: string;
|
|
69
|
+
decision: Decision;
|
|
70
|
+
}>;
|
|
71
|
+
}
|