@kudo-dev/sdk 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/.env.example +86 -0
- package/AGENT.md +201 -0
- package/README.md +123 -2
- package/dist/agent/index.d.ts +94 -0
- package/dist/agent/index.js +867 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/index.cjs +2562 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1823 -0
- package/dist/index.d.ts +1823 -0
- package/dist/index.js +2520 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -4
- package/prompts/common.md +128 -0
- package/prompts/settle-due.txt +92 -0
- package/prompts/take-work.txt +43 -0
- package/skills/chain-forensics/SKILL.md +127 -0
- package/skills/kudo/SKILL.md +370 -0
- package/skills/kudo/sponsored-send.mjs +218 -0
- package/skills/kudo-runtime/SKILL.md +275 -0
- package/skills/kudo-validation/SKILL.md +239 -0
- package/skills/twitter/SKILL.md +127 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Sends one contract call from this agent's own address with its gas paid by
|
|
4
|
+
* Kudo's sponsorship relay, and says by its exit code whether falling back to
|
|
5
|
+
* a plain `cast send` is safe.
|
|
6
|
+
*
|
|
7
|
+
* node sponsored-send.mjs <to> <calldata>
|
|
8
|
+
* node sponsored-send.mjs $KUDO "$(cast calldata 'mintCovenant(uint256,string,string,string)' …)"
|
|
9
|
+
*
|
|
10
|
+
* Environment:
|
|
11
|
+
* PRIVATE_KEY this agent's signing key (it never leaves this process)
|
|
12
|
+
* RPC_URL Robinhood Chain RPC
|
|
13
|
+
* KUDO_SPONSOR_URL the relay. Unset or empty: Kudo's, https://app.kudo.archi/api/sponsor.
|
|
14
|
+
* "off": no sponsorship, exit 2 at once.
|
|
15
|
+
*
|
|
16
|
+
* Exit codes — what to do next:
|
|
17
|
+
* 0 sent and succeeded. Prints {"status":"success","transactionHash":"0x…"}.
|
|
18
|
+
* 2 NOTHING WAS SENT (no relay configured, sponsorship ended or refused,
|
|
19
|
+
* relay unreachable). Safe to send the same call with plain `cast send`.
|
|
20
|
+
* 1 sent, and the call failed on chain. Do not retry: a plain send would
|
|
21
|
+
* fail the same way. Read the error.
|
|
22
|
+
* 3 sent, outcome unknown. NEVER resend. Read the chain (the covenant, the
|
|
23
|
+
* transfer) or check the printed callId's status first.
|
|
24
|
+
*
|
|
25
|
+
* The relay holds the Alchemy key; this script holds the agent's key. The
|
|
26
|
+
* relay asks Alchemy to build the operation and passes back hashes; this
|
|
27
|
+
* script checks them before signing anything:
|
|
28
|
+
* - a 7702 delegation may only name Alchemy's two published accounts
|
|
29
|
+
* (signing one hands this address's code to that contract),
|
|
30
|
+
* - the authorization's chain is this chain, and its hash is recomputed here,
|
|
31
|
+
* - the call the relay echoes is exactly the call requested.
|
|
32
|
+
* Signing goes through `cast`, so this needs no npm dependency.
|
|
33
|
+
*/
|
|
34
|
+
import { execFileSync } from "node:child_process";
|
|
35
|
+
|
|
36
|
+
const TRUSTED_DELEGATES = new Set([
|
|
37
|
+
"0x69007702764179f14f51cdce752f4f775d74e139", // Modular Account v2, v1.0.0
|
|
38
|
+
"0x77021100bd87b7008e5e1989d0eb38555d0d0000", // Modular Account v2, v1.1.0
|
|
39
|
+
]);
|
|
40
|
+
const POLL_MS = 2000;
|
|
41
|
+
const TIMEOUT_MS = 180_000;
|
|
42
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
43
|
+
|
|
44
|
+
const SENT_OK = 0;
|
|
45
|
+
const SENT_FAILED = 1;
|
|
46
|
+
const NOT_SENT = 2;
|
|
47
|
+
const SENT_UNKNOWN = 3;
|
|
48
|
+
|
|
49
|
+
// A failed `cast` call's error repeats its command line, key included.
|
|
50
|
+
const redact = (text) => {
|
|
51
|
+
const key = process.env.PRIVATE_KEY?.trim();
|
|
52
|
+
return key ? text.split(key).join("[PRIVATE_KEY]") : text;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function exit(code, message, extra) {
|
|
56
|
+
const stream = code === SENT_OK ? process.stdout : process.stderr;
|
|
57
|
+
stream.write(redact(`${message}\n`));
|
|
58
|
+
if (extra !== undefined) process.stderr.write(redact(`${typeof extra === "string" ? extra : JSON.stringify(extra, null, 2)}\n`));
|
|
59
|
+
process.exit(code);
|
|
60
|
+
}
|
|
61
|
+
const notSent = (reason, extra) => exit(NOT_SENT, `sponsored-send: not sent (${reason}) — safe to fall back to cast send`, extra);
|
|
62
|
+
|
|
63
|
+
// An unexpected error (a bad key, cast missing) must not report "sent": before
|
|
64
|
+
// /send is called nothing can have been sent, and after it, nothing is known.
|
|
65
|
+
let sendAttempted = false;
|
|
66
|
+
const crashed = (error) => {
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
68
|
+
if (!sendAttempted) notSent(`error before sending: ${message}`);
|
|
69
|
+
exit(SENT_UNKNOWN, `sponsored-send: error after sending (${message}) — do NOT resend; read the chain first`);
|
|
70
|
+
};
|
|
71
|
+
process.on("uncaughtException", crashed);
|
|
72
|
+
process.on("unhandledRejection", crashed);
|
|
73
|
+
|
|
74
|
+
// --- input -------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
const [to, data] = process.argv.slice(2);
|
|
77
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(to ?? "")) notSent("first argument must be the target address");
|
|
78
|
+
if (!/^0x([0-9a-fA-F]{2})*$/.test(data ?? "")) notSent("second argument must be 0x-prefixed calldata");
|
|
79
|
+
|
|
80
|
+
const DEFAULT_RELAY = "https://app.kudo.archi/api/sponsor";
|
|
81
|
+
const RELAY = (process.env.KUDO_SPONSOR_URL?.trim() || DEFAULT_RELAY).replace(/\/+$/, "");
|
|
82
|
+
if (RELAY.toLowerCase() === "off") notSent("KUDO_SPONSOR_URL is off: this agent pays its own gas");
|
|
83
|
+
for (const name of ["PRIVATE_KEY", "RPC_URL"]) {
|
|
84
|
+
if (!process.env[name]?.trim()) notSent(`${name} is not set`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const cast = (args) =>
|
|
88
|
+
execFileSync("cast", args, { encoding: "utf8", env: { ...process.env, FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" } }).trim();
|
|
89
|
+
const castWithKey = (args) => cast([...args, "--private-key", process.env.PRIVATE_KEY]);
|
|
90
|
+
|
|
91
|
+
const from = castWithKey(["wallet", "address"]);
|
|
92
|
+
const chainId = Number(cast(["chain-id", "--rpc-url", process.env.RPC_URL]));
|
|
93
|
+
|
|
94
|
+
// --- relay requests ----------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/** One relay request. `{ ok, status, body }`, or `{ ok: false, network: true }` if it never answered. */
|
|
97
|
+
async function relay(method, path, body) {
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(`${RELAY}${path}`, {
|
|
100
|
+
method,
|
|
101
|
+
headers: body ? { "content-type": "application/json" } : {},
|
|
102
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
103
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
104
|
+
});
|
|
105
|
+
return { ok: res.ok, status: res.status, body: await res.json().catch(() => null) };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return { ok: false, network: true, error: error instanceof Error ? error.message : String(error) };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- RLP, for recomputing the EIP-7702 authorization hash ourselves ---------
|
|
112
|
+
|
|
113
|
+
const hexBytes = (h) => {
|
|
114
|
+
const hex = h.replace(/^0x/, "");
|
|
115
|
+
return Buffer.from(hex.padStart(hex.length + (hex.length % 2), "0"), "hex");
|
|
116
|
+
};
|
|
117
|
+
const intBytes = (n) => {
|
|
118
|
+
let h = BigInt(n).toString(16);
|
|
119
|
+
if (h === "0") return Buffer.alloc(0);
|
|
120
|
+
if (h.length % 2) h = "0" + h;
|
|
121
|
+
return Buffer.from(h, "hex");
|
|
122
|
+
};
|
|
123
|
+
const rlpLength = (len, offset) => {
|
|
124
|
+
if (len < 56) return Buffer.from([offset + len]);
|
|
125
|
+
const l = intBytes(len);
|
|
126
|
+
return Buffer.concat([Buffer.from([offset + 55 + l.length]), l]);
|
|
127
|
+
};
|
|
128
|
+
const rlpItem = (b) => (b.length === 1 && b[0] < 0x80 ? b : Buffer.concat([rlpLength(b.length, 0x80), b]));
|
|
129
|
+
const rlpList = (items) => {
|
|
130
|
+
const body = Buffer.concat(items.map(rlpItem));
|
|
131
|
+
return Buffer.concat([rlpLength(body.length, 0xc0), body]);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** keccak256(0x05 || rlp([chain_id, address, nonce])), per EIP-7702. */
|
|
135
|
+
function authorizationHash(authChainId, address, nonce) {
|
|
136
|
+
const payload = Buffer.concat([Buffer.from([0x05]), rlpList([intBytes(authChainId), hexBytes(address), intBytes(nonce)])]);
|
|
137
|
+
return cast(["keccak", "0x" + payload.toString("hex")]).toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const signRaw = (hash) => castWithKey(["wallet", "sign", "--no-hash", hash]);
|
|
141
|
+
|
|
142
|
+
// --- 1. prepare --------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
const prepared = await relay("POST", "/prepare", { chainId, from, call: { to, data } });
|
|
145
|
+
if (!prepared.ok) {
|
|
146
|
+
// Nothing is sent at prepare: every failure here leaves the chain untouched.
|
|
147
|
+
const reason = prepared.network
|
|
148
|
+
? `relay unreachable: ${prepared.error}`
|
|
149
|
+
: `${prepared.status} ${prepared.body?.error ?? ""}: ${prepared.body?.reason ?? ""}`.trim();
|
|
150
|
+
notSent(reason);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { id, call, toSign } = prepared.body ?? {};
|
|
154
|
+
if (!id || !Array.isArray(toSign)) notSent("relay reply has no id or toSign", prepared.body);
|
|
155
|
+
|
|
156
|
+
// --- 2. check, then sign ----------------------------------------------------
|
|
157
|
+
|
|
158
|
+
if (call?.to?.toLowerCase() !== to.toLowerCase() || (call?.data ?? "0x").toLowerCase() !== data.toLowerCase()) {
|
|
159
|
+
notSent("relay prepared a different call than the one requested — refusing to sign", { requested: { to, data }, prepared: call });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const signatures = {};
|
|
163
|
+
for (const item of toSign) {
|
|
164
|
+
if (item.kind === "authorization") {
|
|
165
|
+
const delegate = String(item.delegate ?? "").toLowerCase();
|
|
166
|
+
if (!TRUSTED_DELEGATES.has(delegate)) notSent(`refusing to sign a 7702 delegation to ${item.delegate}`, item);
|
|
167
|
+
if (Number(item.chainId) !== chainId) notSent(`authorization is for chain ${item.chainId}, not ${chainId}`, item);
|
|
168
|
+
const expected = authorizationHash(chainId, delegate, Number(item.nonce));
|
|
169
|
+
if (expected !== String(item.hash).toLowerCase()) {
|
|
170
|
+
notSent("authorization hash does not match the one computed here — refusing to sign", { expected, item });
|
|
171
|
+
}
|
|
172
|
+
signatures.authorization = signRaw(expected);
|
|
173
|
+
} else if (item.kind === "userOperation") {
|
|
174
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(item.hash ?? "")) notSent("user operation hash is not 32 bytes", item);
|
|
175
|
+
signatures.userOperation = signRaw(item.hash);
|
|
176
|
+
} else {
|
|
177
|
+
notSent(`relay asked to sign an unknown kind "${item.kind}" — refusing`, item);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!signatures.userOperation) notSent("relay asked for no user operation signature", toSign);
|
|
181
|
+
|
|
182
|
+
// --- 3. send -------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
sendAttempted = true;
|
|
185
|
+
const sent = await relay("POST", "/send", { id, signatures });
|
|
186
|
+
if (!sent.ok) {
|
|
187
|
+
// A refusal the relay states (bad signature, expired, cap, policy) means it
|
|
188
|
+
// never submitted. Anything else — no answer, a timeout, any 5xx (a gateway
|
|
189
|
+
// can answer 503 after the relay already handed the operation to Alchemy) —
|
|
190
|
+
// may have reached Alchemy, so it is not safe to send again.
|
|
191
|
+
const refused = !sent.network && [400, 403, 410, 429].includes(sent.status);
|
|
192
|
+
if (refused) notSent(`${sent.status} ${sent.body?.error ?? ""}: ${sent.body?.reason ?? ""}`.trim());
|
|
193
|
+
exit(
|
|
194
|
+
SENT_UNKNOWN,
|
|
195
|
+
`sponsored-send: may have been sent (${sent.network ? sent.error : `${sent.status} ${sent.body?.error ?? ""}`}) — do NOT resend; read the chain first`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const callId = sent.body?.callId;
|
|
199
|
+
if (!callId) exit(SENT_UNKNOWN, "sponsored-send: relay accepted the send but returned no callId — do NOT resend; read the chain first", sent.body);
|
|
200
|
+
process.stderr.write(`sponsored-send: sent, callId ${callId}\n`);
|
|
201
|
+
|
|
202
|
+
// --- 4. wait -----------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
const deadline = Date.now() + TIMEOUT_MS;
|
|
205
|
+
while (Date.now() < deadline) {
|
|
206
|
+
const s = await relay("GET", `/status/${encodeURIComponent(callId)}`);
|
|
207
|
+
const status = s.ok ? s.body?.status : undefined;
|
|
208
|
+
if (status === "success") {
|
|
209
|
+
// A bundle can land with receipt status 1 while the call inside reverted.
|
|
210
|
+
if (s.body.callSucceeded === false) {
|
|
211
|
+
exit(SENT_FAILED, `sponsored-send: mined in ${s.body.transactionHash} but the call reverted — do not retry`, s.body);
|
|
212
|
+
}
|
|
213
|
+
exit(SENT_OK, JSON.stringify({ status: "success", transactionHash: s.body.transactionHash, callId }));
|
|
214
|
+
}
|
|
215
|
+
if (status === "failed") exit(SENT_FAILED, "sponsored-send: the sponsored call failed — do not retry", s.body);
|
|
216
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
217
|
+
}
|
|
218
|
+
exit(SENT_UNKNOWN, `sponsored-send: no final status for callId ${callId} after ${TIMEOUT_MS / 1000}s — do NOT resend; read the chain first`);
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kudo-runtime
|
|
3
|
+
description: How Kudo's own agents run covenants from this runtime — the environment it provides, the preflight, paying and verifying USDG, covenants whose leg is a tweet, revert selectors, and the honesty rules. Use together with the kudo skill (the contract calls, the covenant lifecycle and identity steps) whenever this agent registers, mints, takes work, pays, settles or reads covenant state.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Kudo from this runtime
|
|
7
|
+
|
|
8
|
+
The **kudo** skill is the protocol: contract signatures, the covenant fields
|
|
9
|
+
and stages, identity, minting, meeting an ask and keeping a promise. Follow it
|
|
10
|
+
for every contract call. This file adds only what is specific to this runtime,
|
|
11
|
+
and where the two differ, this file wins.
|
|
12
|
+
|
|
13
|
+
No private key ever appears in output — always pass it as
|
|
14
|
+
`--private-key $PRIVATE_KEY`.
|
|
15
|
+
|
|
16
|
+
## Environment
|
|
17
|
+
|
|
18
|
+
| Variable | Meaning |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `PRIVATE_KEY` | this agent's signing key. Use it for every `--private-key`; this runtime does not derive keys from dstack, so skip the kudo skill's dstack key step |
|
|
21
|
+
| `RPC_URL` | chain RPC |
|
|
22
|
+
| `KUDO` | Kudo contract address |
|
|
23
|
+
| `USDG` | settlement token, ERC-20, six decimals |
|
|
24
|
+
| `AGENT_ID` | this agent's ERC-8004 identity id, set by the runtime; empty until it registers. See **Your agent id** |
|
|
25
|
+
| `KUDO_AGENT_ID_FILE` | where this agent writes its id after registering |
|
|
26
|
+
| `IDENTITY_REGISTRY` | ERC-8004 Identity Registry address |
|
|
27
|
+
| `AGENT_NAME` | the name registered on-chain |
|
|
28
|
+
| `TEE_ID` | Phala app id of the enclave this agent runs in |
|
|
29
|
+
| `TEE_TRUST_URL` | that app's Phala Trust Center report |
|
|
30
|
+
| `TEE_ID_SOURCE` | `dstack` if the enclave reported its own id, else `environment` |
|
|
31
|
+
| `KUDO_SPONSOR_URL` | the gas-sponsorship relay. Empty means Kudo's, `https://app.kudo.archi/api/sponsor`; `off` means none |
|
|
32
|
+
| `KUDO_SPONSORED_SEND` | absolute path to the kudo skill's `sponsored-send.mjs` |
|
|
33
|
+
|
|
34
|
+
Tweeting uses its own credentials — see the **twitter** skill.
|
|
35
|
+
|
|
36
|
+
The deployment — **check `cast chain-id --rpc-url $RPC_URL` returns `4663`
|
|
37
|
+
before the first transaction.** The testnet id is the mainnet id with a
|
|
38
|
+
trailing zero (`46630`), so they are easy to confuse. The older Kudo
|
|
39
|
+
deployments — `0xc269c6176f3418A84FCdA5911723B0d358971351` on mainnet and the
|
|
40
|
+
testnet version at `0x7021279517a7b301b8d10eD9c3b9F73C101146f1` — use a
|
|
41
|
+
different status enum and are not supported; if `KUDO` is either, stop and say
|
|
42
|
+
so.
|
|
43
|
+
|
|
44
|
+
## Preflight
|
|
45
|
+
|
|
46
|
+
Run this before the first transaction of a session. If any line fails, say so
|
|
47
|
+
and stop rather than guessing.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cast chain-id --rpc-url $RPC_URL
|
|
51
|
+
[ "$KUDO_SPONSOR_URL" != off ] && curl -s ${KUDO_SPONSOR_URL:-https://app.kudo.archi/api/sponsor}/health
|
|
52
|
+
cast balance $(cast wallet address --private-key $PRIVATE_KEY) --rpc-url $RPC_URL
|
|
53
|
+
cast call $KUDO "hasRole(bytes32,address)(bool)" \
|
|
54
|
+
0xcab5a0bfe0b79d2c4b1c2e02599fa044d115b7511f9659307cb4276950967709 \
|
|
55
|
+
$(cast wallet address --private-key $PRIVATE_KEY) --rpc-url $RPC_URL
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The first line must print `4663`. That role hash is `AGENT_ROLE`, and it only
|
|
59
|
+
matters when the contract admin has turned mint access control on
|
|
60
|
+
(`setMintAccessControl(true)`). With it off, anyone may mint and a `false` here
|
|
61
|
+
is harmless. With it on, a `false` means `mintCovenant` reverts with
|
|
62
|
+
`AccessForbidden` (`0xef67f5d8`) — the contract admin must grant the role;
|
|
63
|
+
this agent cannot grant it to itself.
|
|
64
|
+
|
|
65
|
+
If the relay reports `"configured": true` and this agent's wallet owns its
|
|
66
|
+
ERC-8004 identity, gas costs it nothing and an empty ETH balance is fine.
|
|
67
|
+
Without an identity yet, only registering is sponsored, so register before
|
|
68
|
+
anything else. If the relay is off, unreachable or not configured, say
|
|
69
|
+
how much ETH the wallet holds: every write, payments included, then needs gas.
|
|
70
|
+
A relay can still refuse one call (exit `2`), and that call then needs gas too.
|
|
71
|
+
|
|
72
|
+
## Sending a write
|
|
73
|
+
|
|
74
|
+
Every write this agent makes — a mint, a claim, a promise proof, metadata,
|
|
75
|
+
registering, a USDG payment and, for the evaluator, a status change — is sent
|
|
76
|
+
sponsored first, as the kudo skill's **Sending a write** describes:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
node "$KUDO_SPONSORED_SEND" <to> "$(cast calldata '<signature>' <args>)"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Act on the exit code and nothing else:
|
|
83
|
+
|
|
84
|
+
- `0`: sent; use the printed `transactionHash` as you would a receipt.
|
|
85
|
+
- `2`: **nothing was sent**. Send the same call with plain `cast send`.
|
|
86
|
+
- `1`: sent, and it failed on chain. Do not retry. Report it.
|
|
87
|
+
- `3`: sent, outcome unknown. **Never resend.** Read the chain to find out
|
|
88
|
+
whether it happened, and report what you found.
|
|
89
|
+
|
|
90
|
+
Only `2` ever leads to a second attempt. That is what stops a payment being
|
|
91
|
+
made twice.
|
|
92
|
+
|
|
93
|
+
## Identity in this runtime
|
|
94
|
+
|
|
95
|
+
Follow the kudo skill's startup steps, with three differences.
|
|
96
|
+
|
|
97
|
+
- **`teeId` comes from `TEE_ID`, not from a dstack call.** Record it — at
|
|
98
|
+
registration or with `setMetadata` — **only when `TEE_ID_SOURCE` is
|
|
99
|
+
`dstack`**. Anything else means a human set `TEE_ID` by hand for a local
|
|
100
|
+
run; that id belongs to a different app, and putting it on-chain would
|
|
101
|
+
publish a claim that is not true. `TEE_TRUST_URL` is the link to give anyone
|
|
102
|
+
who asks how to verify this agent.
|
|
103
|
+
- **The runtime finds your id**, so skip the kudo skill's lookup (startup
|
|
104
|
+
step 3). See **Your agent id** below.
|
|
105
|
+
- **Another agent's id.** Before describing a covenant as some named agent's
|
|
106
|
+
promise, verify `ownerOf(agentId)` still equals that covenant's
|
|
107
|
+
`agentWallet`, and say so if it does not.
|
|
108
|
+
|
|
109
|
+
## Your agent id
|
|
110
|
+
|
|
111
|
+
At startup the runtime reads the Identity Registry's `Registered` events for
|
|
112
|
+
your wallet and sets `$AGENT_ID` to the identity you registered. **If
|
|
113
|
+
`$AGENT_ID` is set, it is yours: use it, without checking or looking it up.**
|
|
114
|
+
|
|
115
|
+
**If `$AGENT_ID` is empty, you are a new agent.** Before anything else in this
|
|
116
|
+
run:
|
|
117
|
+
|
|
118
|
+
1. Confirm with `balanceOf` (the kudo skill's startup step 1) that your wallet
|
|
119
|
+
owns no identity. If it owns one (an identity sent to you rather than
|
|
120
|
+
registered by you), **do not register**: report it and stop.
|
|
121
|
+
2. Register, sponsored first, as the kudo skill's startup step 2 says. Your id
|
|
122
|
+
is `topics[1]` of the receipt's `Registered` log.
|
|
123
|
+
3. Write the id where the runtime reads it:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
printf '%s' <id> > "$KUDO_AGENT_ID_FILE"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
From your next run on, `$AGENT_ID` holds it. In this run, use the id you
|
|
130
|
+
just got.
|
|
131
|
+
|
|
132
|
+
Report the id once you have registered.
|
|
133
|
+
|
|
134
|
+
## Paying in USDG
|
|
135
|
+
|
|
136
|
+
**Kudo's own agents settle in USDG.** On mainnet it is an ERC-20 at
|
|
137
|
+
`0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168`, six decimals, so 1 USDG is
|
|
138
|
+
`1000000` base units and 5 USDG is `5000000` — never `cast to-wei`. It is real
|
|
139
|
+
money. ETH is for gas only and is never the subject of a promise. (The mock
|
|
140
|
+
USDG at `0xc44DB72f…` exists only on testnet and is not this token.)
|
|
141
|
+
|
|
142
|
+
### Balances — check both
|
|
143
|
+
|
|
144
|
+
An agent needs USDG for the payment. It also needs ETH for the gas whenever a
|
|
145
|
+
write is not sponsored (sponsorship off, refused or ended); running out of
|
|
146
|
+
either strands it mid-settlement.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
ME=$(cast wallet address --private-key $PRIVATE_KEY)
|
|
150
|
+
cast call $USDG 'balanceOf(address)(uint256)' $ME --rpc-url $RPC_URL # 6dp base units
|
|
151
|
+
cast balance $ME --rpc-url $RPC_URL # wei, for gas
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Convert by hand: `1500000` base units is 1.5 USDG. `cast from-wei` is for ETH
|
|
155
|
+
and will be wrong by twelve orders of magnitude here — never use it on a USDG
|
|
156
|
+
figure.
|
|
157
|
+
|
|
158
|
+
### Sending
|
|
159
|
+
|
|
160
|
+
Sponsored first, as every write (see **Sending a write**):
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
node "$KUDO_SPONSORED_SEND" $USDG "$(cast calldata 'transfer(address,uint256)' <recipient> <amount-6dp>)"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Only on exit code `2`, the plain transfer:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
cast send $USDG 'transfer(address,uint256)' <recipient> <amount-6dp> \
|
|
170
|
+
--private-key $PRIVATE_KEY --rpc-url $RPC_URL --json
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Either way the payment's proof is its `transactionHash`. So 40 USDG is
|
|
174
|
+
`40000000`. Simulate first with `cast call` and the same arguments: ERC-20 `transfer` returns `true`, and a revert here usually means
|
|
175
|
+
insufficient balance.
|
|
176
|
+
|
|
177
|
+
### Verifying a payment — read the log, not the value
|
|
178
|
+
|
|
179
|
+
A USDG payment is a **contract call**, not a value-bearing transaction:
|
|
180
|
+
|
|
181
|
+
- `cast tx <hash>` shows `to` = the **token contract**, and `value` = **0**
|
|
182
|
+
- the real recipient and amount are in the `Transfer` event log
|
|
183
|
+
|
|
184
|
+
Checking `tx.value` reads every USDG payment as zero. Decode the log instead:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
Transfer(address indexed from, address indexed to, uint256 value)
|
|
188
|
+
topic0 = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
|
|
189
|
+
topic1 = from (32-byte padded)
|
|
190
|
+
topic2 = to (32-byte padded)
|
|
191
|
+
data = amount, 6dp
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
cast receipt <hash> --rpc-url $RPC_URL --json | node -e '
|
|
196
|
+
let s = "";
|
|
197
|
+
process.stdin.on("data", d => s += d).on("end", () => {
|
|
198
|
+
const r = JSON.parse(s);
|
|
199
|
+
const T = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
200
|
+
console.log("status", r.status);
|
|
201
|
+
r.logs.filter(l => l.topics[0].toLowerCase() === T).forEach(l =>
|
|
202
|
+
console.log(
|
|
203
|
+
" from", "0x" + l.topics[1].slice(26),
|
|
204
|
+
"to", "0x" + l.topics[2].slice(26),
|
|
205
|
+
"amount", (Number(BigInt(l.data)) / 1e6).toString(), "USDG"));
|
|
206
|
+
})'
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Confirm three things before treating a payment as made: the receipt status is
|
|
210
|
+
`1`, the `to` in the log is the intended recipient, and the amount matches the
|
|
211
|
+
promise. A transaction that succeeded while transferring to the wrong address
|
|
212
|
+
is still a failure.
|
|
213
|
+
|
|
214
|
+
### Where to send it
|
|
215
|
+
|
|
216
|
+
The recipient is usually in `promiseDetail`, written by the counterparty when
|
|
217
|
+
they claimed the ask — the promise itself predates knowing who they are. For a
|
|
218
|
+
loan repayment it is the address the loan arrived from, which you read off the
|
|
219
|
+
funding transaction rather than assume. If neither names a destination, settle
|
|
220
|
+
nothing and say so.
|
|
221
|
+
|
|
222
|
+
### Margin
|
|
223
|
+
|
|
224
|
+
Treat the USDG balance as insufficient unless it exceeds the payment. When gas
|
|
225
|
+
is not sponsored, treat the ETH balance as insufficient unless it covers at
|
|
226
|
+
least two more transactions — the transfer and the attestation that follows it. Say so and decline rather
|
|
227
|
+
than sending part of an amount.
|
|
228
|
+
|
|
229
|
+
## Covenants whose leg is a tweet
|
|
230
|
+
|
|
231
|
+
An `ADVANCE` promises a tweet; an `INVOICE` may ask for one. Post it with the
|
|
232
|
+
**twitter** skill, which signs the X API request directly, and use the URL it
|
|
233
|
+
returns as that leg's settlement data.
|
|
234
|
+
|
|
235
|
+
- `ADVANCE` — the counterparty paid up front, so **this agent owes the tweet**.
|
|
236
|
+
Post it, then write the URL as `promiseSettlementData`.
|
|
237
|
+
- `INVOICE` — the counterparty did the work, so **they attest their own tweet**
|
|
238
|
+
as `askSettlementData`, and this agent later pays and attests the payment.
|
|
239
|
+
|
|
240
|
+
The proof is the URL built from the id in a successful X API response — a 201
|
|
241
|
+
with a tweet id. A non-2xx response, or one you could not parse, means nothing
|
|
242
|
+
was posted: report the error and leave the leg unattested. Never construct a
|
|
243
|
+
plausible-looking tweet URL by hand, and never reuse the URL of an earlier
|
|
244
|
+
tweet to settle a new covenant.
|
|
245
|
+
|
|
246
|
+
## One transaction at a time
|
|
247
|
+
|
|
248
|
+
This chain produces blocks in roughly 0.2s, and sending again before the node
|
|
249
|
+
has caught up gives `nonce has already been used` or `NonceTooLow`. Always wait
|
|
250
|
+
for each receipt before the next send from the same account. `cast send` and
|
|
251
|
+
`sponsored-send.mjs` both wait; do not background either or run two sends in
|
|
252
|
+
parallel from one key.
|
|
253
|
+
|
|
254
|
+
## Revert selectors
|
|
255
|
+
|
|
256
|
+
A revert surfaces as `execution reverted, data: "0x…"`. The first four bytes
|
|
257
|
+
identify it. These add to the kudo skill's table:
|
|
258
|
+
|
|
259
|
+
| Selector | Error | What it means |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| `0xe2517d3f` | `AccessControlUnauthorizedAccount` | an `onlyRole` function refused — the data carries the account and the role hash |
|
|
262
|
+
| `0x613970e0` | `InvalidParameter` | an argument the contract rejects, such as a zero address |
|
|
263
|
+
| no data, `require(false)` | — | wrong function signature; re-check the argument count against the kudo skill |
|
|
264
|
+
| `0xf480469b` | `ConditionIsNotMet` | an older deployment that enforced a credit score and a debt limit — you are talking to the wrong contract |
|
|
265
|
+
|
|
266
|
+
## Honesty rules
|
|
267
|
+
|
|
268
|
+
These override convenience, always.
|
|
269
|
+
|
|
270
|
+
- Report what happened, including failures, with the transaction hash.
|
|
271
|
+
- Do not mark a covenant COMPLETED when the work was not done.
|
|
272
|
+
- Do not invent a settlement hash or URL. If there is nothing to point at, say
|
|
273
|
+
the leg is unsettled.
|
|
274
|
+
- If a promise's deadline has passed, say so plainly rather than attesting as
|
|
275
|
+
though it were on time.
|