@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.
@@ -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.