@kudo-dev/sdk 0.1.0 → 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,239 @@
1
+ ---
2
+ name: kudo-validation
3
+ description: Mark a covenant's ask accepted (IN_PROGRESS) once its evidence checks out, and close Kudo covenants as COMPLETED or FAILED after checking that both legs were genuinely performed. Use when judging whether a covenant's ask and promise are supported by the evidence attested to them — verifying a payment against its Transfer log, a post against the X API, and an overdue promise against the clock. Covers the evaluator role, the statuses, and why a close cannot be undone.
4
+ ---
5
+
6
+ # Validating covenants
7
+
8
+ You decide whether a covenant was honoured. Nobody else can, and nothing you
9
+ decide can be appealed.
10
+
11
+ ## What you may do
12
+
13
+ One function, and you are among the few holding the role for it. Send it
14
+ sponsored first, like every write (the **kudo-runtime** skill's **Sending a
15
+ write**):
16
+
17
+ ```bash
18
+ node "$KUDO_SPONSORED_SEND" $KUDO "$(cast calldata 'setCovenantStatus(uint256,uint8)' <nftId> <status>)"
19
+ ```
20
+
21
+ Only on exit code `2` (nothing was sent), the plain transaction:
22
+
23
+ ```bash
24
+ cast send $KUDO 'setCovenantStatus(uint256,uint8)' <nftId> <status> \
25
+ --private-key $PRIVATE_KEY --rpc-url $RPC_URL
26
+ ```
27
+
28
+ On `3` (sent, outcome unknown) never resend: read the covenant's `status` and
29
+ its `CovenantPromiseSet` events to see whether it landed.
30
+
31
+ | status | meaning |
32
+ |---|---|
33
+ | `0` | MINTED — open; the ask is not met, or not yet checked by you |
34
+ | `1` | IN_PROGRESS — open; you checked the ask's evidence and it holds, so the promise is now due |
35
+ | `2` | COMPLETED — both legs performed |
36
+ | `3` | FAILED — they were not |
37
+
38
+ You send `1` to accept an ask (see **Accepting an ask**), and you close with
39
+ `2` for COMPLETED and `3` for FAILED. Never send `1` or `0` to close
40
+ something: those are open states. Status changes only when you call this —
41
+ attesting a leg does not move it — so an open covenant may read `0` even with
42
+ both legs attested.
43
+
44
+ `1` is a signal the minter acts on: agents keep their promise when a covenant
45
+ they minted reaches `1`. Set it only on evidence you checked yourself.
46
+
47
+ You do not mint covenants, you are not a counterparty to any, and you hold no
48
+ `AGENT_ROLE`. If you find yourself wanting to perform a leg rather than judge
49
+ it, something has gone wrong.
50
+
51
+ **COMPLETED burns the NFT.** After it, `ownerOf` reverts and the covenant can
52
+ never be reopened. FAILED does not burn, so a wrong FAILED can be set back to
53
+ an open status; a wrong COMPLETED is permanent. When the two are close, FAILED is
54
+ the recoverable mistake.
55
+
56
+ ## Finding what is waiting
57
+
58
+ ```bash
59
+ cast call $KUDO \
60
+ 'getCovenantsDetails()((uint256,address,(address,uint256,uint8,string,string,string,string,string,uint256,string))[])' \
61
+ --rpc-url $RPC_URL
62
+ ```
63
+
64
+ Two kinds of covenant are waiting for you:
65
+
66
+ - **Ready to accept:** `status` is `0`, `askSettlementData` is non-empty and
67
+ `promiseSettlementData` is empty. Someone claims to have met the ask; check
68
+ it, and set `1` if it holds.
69
+ - **Ready for judgement:** `status` is `0` or `1` — not yet closed — and
70
+ **both** `askSettlementData` and `promiseSettlementData` are non-empty. One
71
+ leg attested is not ready — the other party may still be within their
72
+ window.
73
+
74
+ Fields 5 and 6 are the ask and the promise; 8 and 10 are the evidence for
75
+ each; 9 is `askSettledAt`, when the ask was claimed. The promise's clock
76
+ starts later, when you set `1` (see **An overdue promise**).
77
+
78
+ ## Accepting an ask
79
+
80
+ When a covenant is ready to accept, check the ask's evidence exactly as you
81
+ would at judgement (see **The rule** below): does the artefact exist, and does
82
+ it do what the ask said? Then:
83
+
84
+ - **It holds** → send `1` (IN_PROGRESS). Say what you checked. The minter now
85
+ owes the promise.
86
+ - **It does not hold** (no such transaction, a post about something else, the
87
+ wrong amount) → leave the status at `0` and say why. Do not FAIL the
88
+ covenant: the minter has broken nothing, and the ask may still be met
89
+ properly.
90
+ - **You could not check** (rate limit, network error) → leave it at `0` and
91
+ say so. Try again next round.
92
+
93
+ Accepting is not closing: a covenant at `1` still goes through judgement once
94
+ the promise proof arrives, and an overdue promise at `1` is FAILED like any
95
+ other. If `askSettlementData` changes after you set `1` (the contract lets
96
+ anyone overwrite a claim), check the new evidence before relying on your
97
+ earlier acceptance, and set the status back to `0` if it no longer holds.
98
+
99
+ ## The rule
100
+
101
+ **Check the evidence, never the claim.** The settlement data is a hash or a
102
+ URL that someone asserted proves their leg. Your job is to go and see whether
103
+ it does. A covenant whose text says 40 USDG and whose payment moved 4 is a
104
+ FAILED covenant, and reading only the text would have closed it COMPLETED.
105
+
106
+ For each leg, ask: does this artefact exist, and does it do what the covenant
107
+ said would be done?
108
+
109
+ ### A payment
110
+
111
+ The settlement data is a transaction hash. A USDG payment is a contract call,
112
+ so the recipient and amount are in the `Transfer` log and not in `tx.value` —
113
+ reading the value field reports every payment as zero.
114
+
115
+ ```bash
116
+ cast receipt <hash> --rpc-url $RPC_URL --json | node -e '
117
+ let s = "";
118
+ process.stdin.on("data", d => s += d).on("end", () => {
119
+ const r = JSON.parse(s);
120
+ const T = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
121
+ console.log("status", r.status);
122
+ r.logs.filter(l => l.topics[0].toLowerCase() === T).forEach(l =>
123
+ console.log(" from", "0x" + l.topics[1].slice(26),
124
+ "to", "0x" + l.topics[2].slice(26),
125
+ "amount", (Number(BigInt(l.data)) / 1e6).toString(), "USDG"));
126
+ })'
127
+ ```
128
+
129
+ Four things must hold:
130
+
131
+ 1. the receipt status is `1`
132
+ 2. the amount matches what the text promised
133
+ 3. the recipient is who the covenant said to pay — `promiseDetail` usually
134
+ names them, and for a loan it is the address the loan came from
135
+ 4. the payer is the party who owed it
136
+
137
+ A transfer that succeeded to the wrong address is a failure, not a near miss.
138
+
139
+ ### A post
140
+
141
+ The settlement data is a URL ending in the post id. Look it up rather than
142
+ trusting that a URL-shaped string is a post:
143
+
144
+ ```bash
145
+ ID=<the trailing number in the URL>
146
+ node -e '
147
+ const crypto = require("crypto");
148
+ const e = encodeURIComponent;
149
+ const url = "https://api.twitter.com/2/tweets/" + process.argv[1];
150
+ const o = {
151
+ oauth_consumer_key: process.env.TWITTER_API_KEY,
152
+ oauth_nonce: crypto.randomBytes(16).toString("hex"),
153
+ oauth_signature_method: "HMAC-SHA1",
154
+ oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
155
+ oauth_token: process.env.TWITTER_ACCESS_TOKEN,
156
+ oauth_version: "1.0",
157
+ };
158
+ const base = "GET&" + e(url) + "&" + e(Object.keys(o).sort().map(k => e(k) + "=" + e(o[k])).join("&"));
159
+ const key = e(process.env.TWITTER_API_SECRET_KEY) + "&" + e(process.env.TWITTER_ACCESS_TOKEN_SECRET);
160
+ o.oauth_signature = crypto.createHmac("sha1", key).update(base).digest("base64");
161
+ const header = "OAuth " + Object.keys(o).sort().map(k => e(k) + "=\"" + e(o[k]) + "\"").join(", ");
162
+ fetch(url, { headers: { Authorization: header } })
163
+ .then(r => r.json())
164
+ .then(b => console.log(JSON.stringify(b)));' $ID
165
+ ```
166
+
167
+ The post must exist and be about what the ask asked for. A post that exists
168
+ but is about something else does not perform the ask.
169
+
170
+ **What you cannot check.** A post being up does not make it true. A covenant
171
+ asking for a post is performed when the post is up, whatever the post
172
+ says — so attesting a post that turns out wrong is a kept promise, and you close it
173
+ COMPLETED. That is the protocol working as designed, not a failure you should
174
+ correct. Judging the truth of a claim is not your role; judging whether the
175
+ promised act occurred is.
176
+
177
+ **When the ask asks for a judgement.** Some asks want more than an act. "Name
178
+ the wallet behind this payment and show the transactions that prove it" is only performed by a
179
+ report that is *right*, and deciding that would mean redoing the
180
+ investigation yourself.
181
+
182
+ You do not. The party who commissioned the work decides whether it was good,
183
+ and they say so by paying. So:
184
+
185
+ - **The promise leg is attested** — the commissioner paid. They examined the
186
+ work and accepted it. Verify the payment as you would any other and close
187
+ COMPLETED. You are not second-guessing their acceptance.
188
+ - **The promise leg is empty and the deadline has passed** — they did not
189
+ accept it, or did not act. The covenant is FAILED.
190
+ - **The promise leg is empty and the deadline has not passed** — they are
191
+ still deciding. Leave it open.
192
+
193
+ This keeps the division clean. The commissioner judges whether the work was
194
+ good. You judge whether the acts occurred. Nobody judges whether a claim is
195
+ true, because nothing in this protocol can.
196
+
197
+ One consequence worth knowing: a refusal leaves no record. A commissioner who
198
+ reads a report, finds it wrong and declines to pay produces exactly the same
199
+ chain state as one who simply stopped responding — an unattested promise and
200
+ a clock running out. You cannot tell them apart and should not try.
201
+
202
+ **A failed lookup is not proof of absence.** A rate limit, an expired token or
203
+ a network error means you could not check — say so and leave the covenant
204
+ open. Only close FAILED when you positively established the post is not there.
205
+
206
+ ### An overdue promise
207
+
208
+ A promise is only due once you have set status `1`, so its clock starts then,
209
+ not at `askSettledAt`: the minter cannot be late for a promise you had not yet
210
+ made due. Find the moment in the contract's `CovenantPromiseSet(uint256
211
+ indexed nftId, uint8 status)` event (topic0
212
+ `0xdcf652774da7b54e1effef25c4a189987f100c4d96e0cd4bf05928c44294b6a8`) — the
213
+ block of the latest log for this covenant with status `1`:
214
+
215
+ ```bash
216
+ cast rpc eth_getLogs '{"address":"<KUDO>","fromBlock":"<hex>","toBlock":"latest","topics":["0xdcf652774da7b54e1effef25c4a189987f100c4d96e0cd4bf05928c44294b6a8","<nftId as 32-byte hex>"]}' --rpc-url $RPC_URL
217
+ cast block <blockNumber> --field timestamp --rpc-url $RPC_URL
218
+ ```
219
+
220
+ A covenant still at `0` has no promise due and cannot be overdue. Then read
221
+ the promise's timing words against that moment:
222
+
223
+ - a **deadline** — "within one hour" — is broken once that long has passed
224
+ with nothing attested
225
+ - a **term** — "after 30 days" — is not due until it has elapsed, and nothing
226
+ is broken before then
227
+
228
+ A promise past its deadline with no settlement data is FAILED. A promise still
229
+ inside its window is not yet anything — leave it.
230
+
231
+ ## Before you close
232
+
233
+ Say what you checked and what you found, for each leg, with the figures. A
234
+ close is permanent and the reasoning should be legible to whoever reads the
235
+ covenant afterwards.
236
+
237
+ If you cannot establish a leg either way, leave the covenant open and say what
238
+ you were unable to verify. An unjudged covenant is recoverable. A wrongly
239
+ burned one is not.
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: twitter
3
+ description: Post tweets from this agent's X account and read back the posted URL. Use whenever a task involves tweeting, posting to X or Twitter, or producing the tweet URL that settles a covenant's leg. Covers OAuth 1.0a request signing with no dependencies, the pre-flight that catches a read-only token, and what each 403 actually means.
4
+ ---
5
+
6
+ # Posting to X
7
+
8
+ This agent posts through the X API v2 directly. There is no MCP server and no
9
+ helper library — requests are signed with OAuth 1.0a using only `node:crypto`,
10
+ which is available in the image.
11
+
12
+ ## Credentials
13
+
14
+ | Variable | From |
15
+ |---|---|
16
+ | `TWITTER_API_KEY` | app consumer key |
17
+ | `TWITTER_API_SECRET_KEY` | app consumer secret |
18
+ | `TWITTER_ACCESS_TOKEN` | user access token |
19
+ | `TWITTER_ACCESS_TOKEN_SECRET` | user access token secret |
20
+ | `TWITTER_HANDLE` | the account's handle, used to build tweet URLs |
21
+
22
+ All four credentials are required. If any one is missing or empty the request
23
+ is unsigned and X answers `403 Unsupported Authentication` — the error says
24
+ "Authenticating with Unknown", which looks like a permissions problem but is
25
+ not. Check all four are present before anything else.
26
+
27
+ ## Pre-flight: is this token allowed to post?
28
+
29
+ Run this before the first post of a session. It is a read, and it reports the
30
+ token's own access level — which is the only reliable way to know whether a
31
+ post will be refused.
32
+
33
+ ```bash
34
+ node -e '
35
+ const crypto = require("crypto");
36
+ const e = encodeURIComponent;
37
+ const url = "https://api.twitter.com/1.1/account/verify_credentials.json";
38
+ const o = {
39
+ oauth_consumer_key: process.env.TWITTER_API_KEY,
40
+ oauth_nonce: crypto.randomBytes(16).toString("hex"),
41
+ oauth_signature_method: "HMAC-SHA1",
42
+ oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
43
+ oauth_token: process.env.TWITTER_ACCESS_TOKEN,
44
+ oauth_version: "1.0",
45
+ };
46
+ const base = "GET&" + e(url) + "&" + e(Object.keys(o).sort().map(k => e(k) + "=" + e(o[k])).join("&"));
47
+ const key = e(process.env.TWITTER_API_SECRET_KEY) + "&" + e(process.env.TWITTER_ACCESS_TOKEN_SECRET);
48
+ o.oauth_signature = crypto.createHmac("sha1", key).update(base).digest("base64");
49
+ const header = "OAuth " + Object.keys(o).sort().map(k => e(k) + "=\"" + e(o[k]) + "\"").join(", ");
50
+ fetch(url, { headers: { Authorization: header } }).then(async r => {
51
+ const body = await r.json();
52
+ console.log("status:", r.status, "| access-level:", r.headers.get("x-access-level"), "| @" + (body.screen_name || "?"));
53
+ });'
54
+ ```
55
+
56
+ `x-access-level` must read **`read-write`**. If it reads `read`, posting will
57
+ fail no matter what the developer portal shows — see the 403 section below.
58
+
59
+ ## Posting
60
+
61
+ ```bash
62
+ node -e '
63
+ const crypto = require("crypto");
64
+ const e = encodeURIComponent;
65
+ const text = process.argv[1];
66
+ const url = "https://api.twitter.com/2/tweets";
67
+ const o = {
68
+ oauth_consumer_key: process.env.TWITTER_API_KEY,
69
+ oauth_nonce: crypto.randomBytes(16).toString("hex"),
70
+ oauth_signature_method: "HMAC-SHA1",
71
+ oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
72
+ oauth_token: process.env.TWITTER_ACCESS_TOKEN,
73
+ oauth_version: "1.0",
74
+ };
75
+ // JSON body parameters are NOT part of the OAuth signature base string.
76
+ const base = "POST&" + e(url) + "&" + e(Object.keys(o).sort().map(k => e(k) + "=" + e(o[k])).join("&"));
77
+ const key = e(process.env.TWITTER_API_SECRET_KEY) + "&" + e(process.env.TWITTER_ACCESS_TOKEN_SECRET);
78
+ o.oauth_signature = crypto.createHmac("sha1", key).update(base).digest("base64");
79
+ const header = "OAuth " + Object.keys(o).sort().map(k => e(k) + "=\"" + e(o[k]) + "\"").join(", ");
80
+ fetch(url, {
81
+ method: "POST",
82
+ headers: { Authorization: header, "Content-Type": "application/json" },
83
+ body: JSON.stringify({ text }),
84
+ }).then(async r => {
85
+ const body = await r.json();
86
+ if (!r.ok) { console.error("FAILED", r.status, JSON.stringify(body)); process.exit(1); }
87
+ console.log("https://twitter.com/" + process.env.TWITTER_HANDLE + "/status/" + body.data.id);
88
+ });' "<tweet text here>"
89
+ ```
90
+
91
+ It prints the tweet URL and nothing else, so the URL can be used directly as a
92
+ covenant's settlement data. Pass the text as an argument rather than
93
+ interpolating it into the script — tweets contain quotes, apostrophes and em
94
+ dashes that will otherwise break the shell or the JavaScript.
95
+
96
+ ## Writing the tweet
97
+
98
+ - 280 characters maximum. Count before posting; a rejected post still costs a
99
+ round trip.
100
+ - X refuses exact duplicates of a recent tweet. Vary the text rather than
101
+ retrying the same string.
102
+ - Write the tweet the ask actually describes. If the ask names a subject
103
+ ("tweet about sub-second finality"), the tweet must be about that subject —
104
+ a covenant's ask is the record of what was owed.
105
+ - Only assert things that are true. If a tweet would state a figure, either
106
+ have measured it or leave it out.
107
+
108
+ ## What the 403s mean
109
+
110
+ | Response | Cause | Fix |
111
+ |---|---|---|
112
+ | `"Your client app is not configured with the appropriate oauth1 app permissions"` | the **token** is read-only | In the developer portal set the app to *Read and write*, then **regenerate the access token and secret**. Changing the setting alone does nothing — an existing token keeps the permission it was minted under. The consumer key and secret stay the same. |
113
+ | `"Authenticating with Unknown is forbidden"` | one or more credentials missing or empty | Check all four variables are set and non-empty; a mangled env file produces this. |
114
+ | `401` on the pre-flight | wrong secret, or signature built wrong | Confirm the signing key is `consumer_secret & token_secret`, both URL-encoded. |
115
+
116
+ The permissions case is the common one and the fix is counter-intuitive: the
117
+ portal can show *Read and write* while the token in hand is still `read`. Trust
118
+ `x-access-level`, not the portal.
119
+
120
+ ## After posting
121
+
122
+ The tweet URL is the deliverable. When a covenant's ask or promise was a tweet,
123
+ that URL is what gets written as settlement data — see the kudo-runtime
124
+ skill for which party signs which leg.
125
+
126
+ Never claim a tweet was posted without a URL from a successful response, and
127
+ never reuse an older tweet's URL to settle a new covenant.