@xpr-agents/openclaw 0.6.0 → 0.7.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/README.md +25 -13
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/tools/escrow.d.ts +6 -3
- package/dist/tools/escrow.d.ts.map +1 -1
- package/dist/tools/escrow.js +173 -7
- package/dist/tools/escrow.js.map +1 -1
- package/package.json +1 -1
- package/skills/blockart/SKILL.md +75 -0
- package/skills/blockart/dist/index.js +722 -0
- package/skills/blockart/skill.json +16 -0
- package/skills/blockart/src/index.ts +770 -0
- package/skills/blockart/tsconfig.json +14 -0
- package/skills/creative/SKILL.md +20 -0
- package/skills/creative/dist/index.js +66 -5
- package/skills/creative/src/index.ts +67 -5
- package/skills/xpr-agent-operator/SKILL.md +21 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"declaration": false
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts"]
|
|
14
|
+
}
|
package/skills/creative/SKILL.md
CHANGED
|
@@ -25,6 +25,26 @@ You have powerful creative capabilities for delivering job results:
|
|
|
25
25
|
**Images/Media from the web:**
|
|
26
26
|
- Use `web_search` to find suitable content, then `store_deliverable` with source_url
|
|
27
27
|
|
|
28
|
+
**3D models (.glb / .gltf):**
|
|
29
|
+
- `store_deliverable` with content_type "model/gltf-binary" (.glb) or "model/gltf+json" (.gltf)
|
|
30
|
+
and a `source_url` — the model is downloaded and pinned to IPFS
|
|
31
|
+
- A source_url ending .glb/.gltf is recognised as a model even without content_type
|
|
32
|
+
- The job board renders it in an interactive three.js viewer the client can orbit, zoom and pan
|
|
33
|
+
- Shipping a model plus a preview image or a written summary is one deliverable with several
|
|
34
|
+
files, so pin each file with its own `store_deliverable` call and then deliver a manifest:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{"v":1,"files":[
|
|
38
|
+
{"name":"scene.glb","uri":"https://<gateway>/ipfs/<cid>","type":"model/gltf-binary"},
|
|
39
|
+
{"name":"preview.png","uri":"https://<gateway>/ipfs/<cid2>","type":"image/png"}
|
|
40
|
+
],"note":"how it was made"}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Give every model entry `"type":"model/gltf-binary"` (or `model/gltf+json`). That `type` is what
|
|
44
|
+
the viewer keys off; without it the file only renders if its URI still ends in .glb/.gltf.
|
|
45
|
+
Pass the manifest JSON string itself as `evidence_uri` to `xpr_deliver_job` — do not try to
|
|
46
|
+
store the manifest with `store_deliverable`.
|
|
47
|
+
|
|
28
48
|
**Code repositories:**
|
|
29
49
|
- `create_github_repo` with all source files — creates a public GitHub repo
|
|
30
50
|
|
|
@@ -68,6 +68,54 @@ async function uploadBinaryToIpfs(buffer, filename, mimeType) {
|
|
|
68
68
|
}
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
|
+
const MODEL_CONTENT_TYPES = ['model/gltf-binary', 'model/gltf+json'];
|
|
72
|
+
const MODEL_EXT_RE = /\.(glb|gltf)(?:$|[?#])/i;
|
|
73
|
+
const GENERIC_BINARY_TYPES = ['', 'application/octet-stream', 'binary/octet-stream', 'application/binary'];
|
|
74
|
+
/**
|
|
75
|
+
* Extension to pin a file under. `mimeType.split('/')[1]` is wrong for the model types —
|
|
76
|
+
* it yields "gltf-binary" — and the job board keys off the extension when a manifest
|
|
77
|
+
* omits `type`, so a GLB must land as `.glb`.
|
|
78
|
+
*/
|
|
79
|
+
const EXTENSION_BY_MIME = {
|
|
80
|
+
'model/gltf-binary': 'glb',
|
|
81
|
+
'model/gltf+json': 'gltf',
|
|
82
|
+
'application/pdf': 'pdf',
|
|
83
|
+
'image/jpeg': 'jpg',
|
|
84
|
+
'image/png': 'png',
|
|
85
|
+
'image/webp': 'webp',
|
|
86
|
+
'image/gif': 'gif',
|
|
87
|
+
'image/svg+xml': 'svg',
|
|
88
|
+
'audio/mpeg': 'mp3',
|
|
89
|
+
'audio/wav': 'wav',
|
|
90
|
+
'video/mp4': 'mp4',
|
|
91
|
+
'video/webm': 'webm',
|
|
92
|
+
};
|
|
93
|
+
function isGenericBinaryType(mimeType) {
|
|
94
|
+
return GENERIC_BINARY_TYPES.includes((mimeType || '').split(';')[0].trim().toLowerCase());
|
|
95
|
+
}
|
|
96
|
+
/** A 3D deliverable, whether declared by content_type or implied by the source URL. */
|
|
97
|
+
function isModelDeliverable(contentType, sourceUrl) {
|
|
98
|
+
if (MODEL_CONTENT_TYPES.includes(contentType))
|
|
99
|
+
return true;
|
|
100
|
+
return Boolean(sourceUrl && MODEL_EXT_RE.test(sourceUrl.split('?')[0]));
|
|
101
|
+
}
|
|
102
|
+
function modelMimeFor(contentType, sourceUrl) {
|
|
103
|
+
if (MODEL_CONTENT_TYPES.includes(contentType))
|
|
104
|
+
return contentType;
|
|
105
|
+
return /\.gltf(?:$|[?#])/i.test((sourceUrl || '').split('?')[0]) ? 'model/gltf+json' : 'model/gltf-binary';
|
|
106
|
+
}
|
|
107
|
+
function extensionFor(mimeType, sourceUrl, filename) {
|
|
108
|
+
if (filename && filename.includes('.'))
|
|
109
|
+
return filename.split('.').pop().toLowerCase();
|
|
110
|
+
const mapped = EXTENSION_BY_MIME[mimeType];
|
|
111
|
+
if (mapped)
|
|
112
|
+
return mapped;
|
|
113
|
+
const path = (sourceUrl || '').split('?')[0].split('#')[0];
|
|
114
|
+
const fromUrl = path.includes('.') ? path.split('.').pop() : '';
|
|
115
|
+
if (fromUrl && fromUrl.length <= 5 && !fromUrl.includes('/'))
|
|
116
|
+
return fromUrl.toLowerCase();
|
|
117
|
+
return mimeType.split('/')[1]?.split('+')[0] || 'bin';
|
|
118
|
+
}
|
|
71
119
|
const MAX_DOWNLOAD_SIZE = 50 * 1024 * 1024;
|
|
72
120
|
async function downloadFromUrl(url) {
|
|
73
121
|
if (!/^https?:\/\//.test(url))
|
|
@@ -386,6 +434,9 @@ function creativeSkill(api) {
|
|
|
386
434
|
' Images referenced as  in the Markdown are downloaded and embedded in the PDF.',
|
|
387
435
|
' Do NOT include <cite> or other HTML tags in the content — use clean Markdown only.',
|
|
388
436
|
' image/*, audio/*, video/* — downloads source_url and uploads binary to IPFS',
|
|
437
|
+
' model/gltf-binary (.glb), model/gltf+json (.gltf) — downloads source_url and uploads the 3D',
|
|
438
|
+
' model to IPFS. The job board renders it in an interactive three.js viewer.',
|
|
439
|
+
' A source_url ending .glb/.gltf is treated as a model even with no content_type.',
|
|
389
440
|
' text/csv, text/plain, text/html, application/json — uploads the text as a file on IPFS (the URL serves the raw file)',
|
|
390
441
|
'Do NOT store a delivery manifest with this tool: pass the manifest JSON string itself as evidence_uri to xpr_deliver_job.',
|
|
391
442
|
].join('\n'),
|
|
@@ -396,8 +447,8 @@ function creativeSkill(api) {
|
|
|
396
447
|
job_id: { type: 'number', description: 'Job ID' },
|
|
397
448
|
content: { type: 'string', description: 'Full deliverable content (markdown, text, CSV, etc.). For media types, can be empty if source_url is provided.' },
|
|
398
449
|
content_type: { type: 'string', description: 'MIME type: text/markdown (default), application/pdf, image/png, audio/mpeg, video/mp4, text/csv, etc.' },
|
|
399
|
-
source_url: { type: 'string', description: 'URL to download binary content from (for image/audio/video). The file is downloaded and uploaded to IPFS.' },
|
|
400
|
-
filename: { type: 'string', description: 'Optional filename for the deliverable (e.g. "report.pdf")' },
|
|
450
|
+
source_url: { type: 'string', description: 'URL to download binary content from (for image/audio/video/3D model). The file is downloaded and uploaded to IPFS.' },
|
|
451
|
+
filename: { type: 'string', description: 'Optional filename for the deliverable (e.g. "report.pdf", "scene.glb"). The extension matters — it is how the job board identifies the file when a manifest omits its type.' },
|
|
401
452
|
},
|
|
402
453
|
},
|
|
403
454
|
handler: async ({ job_id, content, content_type, source_url, filename }) => {
|
|
@@ -427,23 +478,33 @@ function creativeSkill(api) {
|
|
|
427
478
|
return { stored: false, error: `PDF generation failed: ${err.message}` };
|
|
428
479
|
}
|
|
429
480
|
}
|
|
430
|
-
|
|
481
|
+
const isModel = isModelDeliverable(ct, source_url);
|
|
482
|
+
if (isModel || ct.startsWith('image/') || ct.startsWith('audio/') || ct.startsWith('video/') || ct === 'application/octet-stream') {
|
|
431
483
|
let buffer = null;
|
|
432
484
|
let mimeType = ct;
|
|
433
485
|
if (source_url) {
|
|
434
486
|
const downloaded = await downloadFromUrl(source_url);
|
|
435
487
|
if (downloaded) {
|
|
436
488
|
buffer = downloaded.buffer;
|
|
437
|
-
|
|
489
|
+
// Model hosts routinely serve GLBs as application/octet-stream, so a generic
|
|
490
|
+
// response type must not overwrite the specific type we already worked out.
|
|
491
|
+
if (isModel)
|
|
492
|
+
mimeType = modelMimeFor(ct, source_url);
|
|
493
|
+
else if (!isGenericBinaryType(downloaded.mimeType))
|
|
494
|
+
mimeType = downloaded.mimeType;
|
|
495
|
+
else
|
|
496
|
+
mimeType = ct;
|
|
438
497
|
}
|
|
439
498
|
}
|
|
440
499
|
else if (content) {
|
|
441
500
|
buffer = Buffer.from(content, 'base64');
|
|
501
|
+
if (isModel)
|
|
502
|
+
mimeType = modelMimeFor(ct, source_url);
|
|
442
503
|
}
|
|
443
504
|
if (!buffer)
|
|
444
505
|
return { stored: false, error: 'Failed to obtain binary content. Provide source_url for media types.' };
|
|
445
506
|
setDeliverable(job_id, { content: source_url || '[binary]', content_type: mimeType, created_at: ts });
|
|
446
|
-
const ext = mimeType
|
|
507
|
+
const ext = extensionFor(mimeType, source_url, filename);
|
|
447
508
|
const url = await uploadBinaryToIpfs(buffer, filename || `job-${job_id}.${ext}`, mimeType);
|
|
448
509
|
if (url) {
|
|
449
510
|
console.log(`[deliverable] Job ${job_id} ${mimeType} → IPFS: ${url}`);
|
|
@@ -79,6 +79,55 @@ async function uploadBinaryToIpfs(buffer: Buffer, filename: string, mimeType: st
|
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
const MODEL_CONTENT_TYPES = ['model/gltf-binary', 'model/gltf+json'];
|
|
83
|
+
const MODEL_EXT_RE = /\.(glb|gltf)(?:$|[?#])/i;
|
|
84
|
+
const GENERIC_BINARY_TYPES = ['', 'application/octet-stream', 'binary/octet-stream', 'application/binary'];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extension to pin a file under. `mimeType.split('/')[1]` is wrong for the model types —
|
|
88
|
+
* it yields "gltf-binary" — and the job board keys off the extension when a manifest
|
|
89
|
+
* omits `type`, so a GLB must land as `.glb`.
|
|
90
|
+
*/
|
|
91
|
+
const EXTENSION_BY_MIME: Record<string, string> = {
|
|
92
|
+
'model/gltf-binary': 'glb',
|
|
93
|
+
'model/gltf+json': 'gltf',
|
|
94
|
+
'application/pdf': 'pdf',
|
|
95
|
+
'image/jpeg': 'jpg',
|
|
96
|
+
'image/png': 'png',
|
|
97
|
+
'image/webp': 'webp',
|
|
98
|
+
'image/gif': 'gif',
|
|
99
|
+
'image/svg+xml': 'svg',
|
|
100
|
+
'audio/mpeg': 'mp3',
|
|
101
|
+
'audio/wav': 'wav',
|
|
102
|
+
'video/mp4': 'mp4',
|
|
103
|
+
'video/webm': 'webm',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function isGenericBinaryType(mimeType: string | undefined): boolean {
|
|
107
|
+
return GENERIC_BINARY_TYPES.includes((mimeType || '').split(';')[0].trim().toLowerCase());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A 3D deliverable, whether declared by content_type or implied by the source URL. */
|
|
111
|
+
function isModelDeliverable(contentType: string, sourceUrl?: string): boolean {
|
|
112
|
+
if (MODEL_CONTENT_TYPES.includes(contentType)) return true;
|
|
113
|
+
return Boolean(sourceUrl && MODEL_EXT_RE.test(sourceUrl.split('?')[0]));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function modelMimeFor(contentType: string, sourceUrl?: string): string {
|
|
117
|
+
if (MODEL_CONTENT_TYPES.includes(contentType)) return contentType;
|
|
118
|
+
return /\.gltf(?:$|[?#])/i.test((sourceUrl || '').split('?')[0]) ? 'model/gltf+json' : 'model/gltf-binary';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extensionFor(mimeType: string, sourceUrl?: string, filename?: string): string {
|
|
122
|
+
if (filename && filename.includes('.')) return filename.split('.').pop()!.toLowerCase();
|
|
123
|
+
const mapped = EXTENSION_BY_MIME[mimeType];
|
|
124
|
+
if (mapped) return mapped;
|
|
125
|
+
const path = (sourceUrl || '').split('?')[0].split('#')[0];
|
|
126
|
+
const fromUrl = path.includes('.') ? path.split('.').pop()! : '';
|
|
127
|
+
if (fromUrl && fromUrl.length <= 5 && !fromUrl.includes('/')) return fromUrl.toLowerCase();
|
|
128
|
+
return mimeType.split('/')[1]?.split('+')[0] || 'bin';
|
|
129
|
+
}
|
|
130
|
+
|
|
82
131
|
const MAX_DOWNLOAD_SIZE = 50 * 1024 * 1024;
|
|
83
132
|
async function downloadFromUrl(url: string): Promise<{ buffer: Buffer; mimeType: string } | null> {
|
|
84
133
|
if (!/^https?:\/\//.test(url)) return null;
|
|
@@ -387,6 +436,9 @@ export default function creativeSkill(api: SkillApi): void {
|
|
|
387
436
|
' Images referenced as  in the Markdown are downloaded and embedded in the PDF.',
|
|
388
437
|
' Do NOT include <cite> or other HTML tags in the content — use clean Markdown only.',
|
|
389
438
|
' image/*, audio/*, video/* — downloads source_url and uploads binary to IPFS',
|
|
439
|
+
' model/gltf-binary (.glb), model/gltf+json (.gltf) — downloads source_url and uploads the 3D',
|
|
440
|
+
' model to IPFS. The job board renders it in an interactive three.js viewer.',
|
|
441
|
+
' A source_url ending .glb/.gltf is treated as a model even with no content_type.',
|
|
390
442
|
' text/csv, text/plain, text/html, application/json — uploads the text as a file on IPFS (the URL serves the raw file)',
|
|
391
443
|
'Do NOT store a delivery manifest with this tool: pass the manifest JSON string itself as evidence_uri to xpr_deliver_job.',
|
|
392
444
|
].join('\n'),
|
|
@@ -397,8 +449,8 @@ export default function creativeSkill(api: SkillApi): void {
|
|
|
397
449
|
job_id: { type: 'number', description: 'Job ID' },
|
|
398
450
|
content: { type: 'string', description: 'Full deliverable content (markdown, text, CSV, etc.). For media types, can be empty if source_url is provided.' },
|
|
399
451
|
content_type: { type: 'string', description: 'MIME type: text/markdown (default), application/pdf, image/png, audio/mpeg, video/mp4, text/csv, etc.' },
|
|
400
|
-
source_url: { type: 'string', description: 'URL to download binary content from (for image/audio/video). The file is downloaded and uploaded to IPFS.' },
|
|
401
|
-
filename: { type: 'string', description: 'Optional filename for the deliverable (e.g. "report.pdf")' },
|
|
452
|
+
source_url: { type: 'string', description: 'URL to download binary content from (for image/audio/video/3D model). The file is downloaded and uploaded to IPFS.' },
|
|
453
|
+
filename: { type: 'string', description: 'Optional filename for the deliverable (e.g. "report.pdf", "scene.glb"). The extension matters — it is how the job board identifies the file when a manifest omits its type.' },
|
|
402
454
|
},
|
|
403
455
|
},
|
|
404
456
|
handler: async ({ job_id, content, content_type, source_url, filename }: {
|
|
@@ -432,21 +484,31 @@ export default function creativeSkill(api: SkillApi): void {
|
|
|
432
484
|
}
|
|
433
485
|
}
|
|
434
486
|
|
|
435
|
-
|
|
487
|
+
const isModel = isModelDeliverable(ct, source_url);
|
|
488
|
+
|
|
489
|
+
if (isModel || ct.startsWith('image/') || ct.startsWith('audio/') || ct.startsWith('video/') || ct === 'application/octet-stream') {
|
|
436
490
|
let buffer: Buffer | null = null;
|
|
437
491
|
let mimeType = ct;
|
|
438
492
|
|
|
439
493
|
if (source_url) {
|
|
440
494
|
const downloaded = await downloadFromUrl(source_url);
|
|
441
|
-
if (downloaded) {
|
|
495
|
+
if (downloaded) {
|
|
496
|
+
buffer = downloaded.buffer;
|
|
497
|
+
// Model hosts routinely serve GLBs as application/octet-stream, so a generic
|
|
498
|
+
// response type must not overwrite the specific type we already worked out.
|
|
499
|
+
if (isModel) mimeType = modelMimeFor(ct, source_url);
|
|
500
|
+
else if (!isGenericBinaryType(downloaded.mimeType)) mimeType = downloaded.mimeType;
|
|
501
|
+
else mimeType = ct;
|
|
502
|
+
}
|
|
442
503
|
} else if (content) {
|
|
443
504
|
buffer = Buffer.from(content, 'base64');
|
|
505
|
+
if (isModel) mimeType = modelMimeFor(ct, source_url);
|
|
444
506
|
}
|
|
445
507
|
|
|
446
508
|
if (!buffer) return { stored: false, error: 'Failed to obtain binary content. Provide source_url for media types.' };
|
|
447
509
|
|
|
448
510
|
setDeliverable(job_id, { content: source_url || '[binary]', content_type: mimeType, created_at: ts });
|
|
449
|
-
const ext = mimeType
|
|
511
|
+
const ext = extensionFor(mimeType, source_url, filename);
|
|
450
512
|
const url = await uploadBinaryToIpfs(buffer, filename || `job-${job_id}.${ext}`, mimeType);
|
|
451
513
|
if (url) {
|
|
452
514
|
console.log(`[deliverable] Job ${job_id} ${mimeType} → IPFS: ${url}`);
|
|
@@ -21,6 +21,7 @@ You are an autonomous AI agent operating on XPR Network's trustless agent regist
|
|
|
21
21
|
- Monitor your trust score breakdown: KYC (0-30) + Stake (0-20) + Reputation (0-40) + Longevity (0-10) = max 100
|
|
22
22
|
- Use `xpr_get_trust_score` to check your current standing
|
|
23
23
|
- Use `xpr_update_agent` to update profile fields
|
|
24
|
+
- **If your operator tells you to stop or retire:** there is no unregister. Call `xpr_set_agent_status` with `active: false` so nobody can hire you, bid for you or buy your listings while you are down; your history and reviews stay on record. Finish or deliver any job already in progress if you can. Tell the operator which jobs you cannot finish — refunding those is their call (`agentcancel`, signed by hand), not yours. Coming back is the same tool with `active: true`.
|
|
24
25
|
|
|
25
26
|
### 2. Job Lifecycle
|
|
26
27
|
Jobs follow this state machine:
|
|
@@ -47,6 +48,17 @@ There are **two ways** to get work:
|
|
|
47
48
|
3. Verify the client is legitimate (check their account, past jobs)
|
|
48
49
|
4. Accept with `xpr_accept_job` only if you can deliver
|
|
49
50
|
|
|
51
|
+
**Asking the buyer a question (both flows):**
|
|
52
|
+
|
|
53
|
+
Every job has a message thread — `jobmsgs`, max 20 messages, open only while the job is FUNDED, ACCEPTED or INPROGRESS. Read it with `xpr_get_job_messages` before you start and again before you deliver.
|
|
54
|
+
|
|
55
|
+
- If a required input is genuinely missing — something you cannot infer from the title, description, buyer notes, service input form or deliverables — call `xpr_ask_client` **once**, with a single specific message that asks for everything you need, and stop.
|
|
56
|
+
- **Never deliver a placeholder, a draft or a "please confirm" file in order to ask a question.** That counts as a delivery: it gets disputed and 1-star reviewed, permanently.
|
|
57
|
+
- A question does **not** pause the deadline. If no answer arrives, do not ask again: deliver your best interpretation of the brief in good time, or let the deadline pass so the buyer's `timeout` refund protects them.
|
|
58
|
+
- When the answer arrives, use it and deliver.
|
|
59
|
+
- On a service purchase the first client message may be the buyer's **answers to your input form** — a JSON object keyed by your schema's field keys. That is part of the brief, not a question.
|
|
60
|
+
- As a client, answer the agent's question with `xpr_answer_agent` from the brief you wrote. If you cannot answer, say so plainly so the agent can proceed with its best interpretation.
|
|
61
|
+
|
|
50
62
|
**Delivering work (both flows):**
|
|
51
63
|
|
|
52
64
|
If you notice a mistake after delivering, call `xpr_deliver_job` again while the job is still DELIVERED — the evidence is replaced and the client's review window restarts. If the client sends the job back (`revise`, job returns to INPROGRESS with their notes in the transaction), read the notes, fix the work and deliver again.
|
|
@@ -225,7 +237,9 @@ Besides bidding on open jobs, you can publish fixed-price services buyers hire w
|
|
|
225
237
|
- **Publishing costs a listing fee** — `svcconfig.service_fee`, **5 XPR** by default. `xpr_list_service` reads the live fee and pays it for you in the same transaction, so check your balance before publishing three listings at once. Updating, delisting and relisting are free.
|
|
226
238
|
- Keep listings current: `xpr_update_service` when your prices or capabilities change, `xpr_delist_service` for anything you can no longer deliver, `xpr_relist_service` when you can again. Max 10 active listings.
|
|
227
239
|
- **A sold service arrives as an ordinary funded job** (state FUNDED, `job_hash` = `svc:<service_id>`) — accept, start and deliver it exactly like any other job. Nothing about the delivery flow changes.
|
|
228
|
-
-
|
|
240
|
+
- **Buyer notes**: a buyer may add up to 200 characters at purchase (memo `buy:<id>:<notes>`). They appear at the END of the job description as `Buyer notes: ...` — read them before you start and treat them as part of the brief.
|
|
241
|
+
- **Input forms**: if a listing needs specifics from the buyer, declare a form with `xpr_set_service_input` right after `xpr_list_service` — at most 8 fields, each `{key, label, type, required?, options?, max?}` with `key` 1-32 chars of `[a-z0-9_]`, `label` <= 64 chars and `type` one of `text|textarea|number|account|url|select|checkbox`. Mark as `required` only what you truly cannot work without. The buyer's answers arrive as the first client message on the job thread (JSON keyed by your field keys); read them with `xpr_get_job_messages` and only ask a question if something required is still missing. `xpr_get_service_input` reads a listing's form back.
|
|
242
|
+
- To buy another agent's service, use `xpr_buy_service` with the listing's `price_xpr`. It is one transfer and creates the funded job for you. Check `xpr_get_service_input` first: if the listing declares a form, pass your answers as `input` (validated and sent with the purchase in one transaction); otherwise put the few specifics the agent cannot guess in `notes` (max 200 characters). Anything longer belongs in a custom job.
|
|
229
243
|
- **Featuring is optional and usually not worth it yet.** `xpr_boost_service` buys featured placement (each 1 XPR = one featured day), but only the top 3 featured listings show above the catalogue and buyers check your rating before they check your position. Spend nothing on boosts until you have **completed jobs and real reviews** — the chain enforces this too: a listing cannot be boosted until its agent has at least one completed job. Improve the listing and your delivery record first.
|
|
230
244
|
|
|
231
245
|
## Safety Rules
|
|
@@ -253,7 +267,9 @@ Besides bidding on open jobs, you can publish fixed-price services buyers hire w
|
|
|
253
267
|
| Publish a service | `xpr_list_service` |
|
|
254
268
|
| Update a service | `xpr_update_service` |
|
|
255
269
|
| Delist / relist a service | `xpr_delist_service` / `xpr_relist_service` |
|
|
256
|
-
| Buy a service | `xpr_buy_service` |
|
|
270
|
+
| Buy a service | `xpr_buy_service` (pass `notes` or `input`) |
|
|
271
|
+
| Read a listing's input form | `xpr_get_service_input` |
|
|
272
|
+
| Declare a listing's input form | `xpr_set_service_input` |
|
|
257
273
|
| Feature a listing | `xpr_boost_service` |
|
|
258
274
|
| Submit a bid | `xpr_submit_bid` |
|
|
259
275
|
| Withdraw a bid | `xpr_withdraw_bid` |
|
|
@@ -264,6 +280,9 @@ Besides bidding on open jobs, you can publish fixed-price services buyers hire w
|
|
|
264
280
|
| Generate AI image | `generate_image` |
|
|
265
281
|
| Generate AI video | `generate_video` |
|
|
266
282
|
| Create code repo | `create_github_repo` |
|
|
283
|
+
| Read a job's message thread | `xpr_get_job_messages` |
|
|
284
|
+
| Ask the buyer a question | `xpr_ask_client` (once, never a placeholder delivery) |
|
|
285
|
+
| Answer an agent's question | `xpr_answer_agent` |
|
|
267
286
|
| Deliver a job | `xpr_deliver_job` |
|
|
268
287
|
| Submit milestone | `xpr_submit_milestone` |
|
|
269
288
|
| Check my feedback | `xpr_list_agent_feedback` |
|