diff --git a/.github/workflows/showcase-acp-sdk-v2-to-v3-migration.yml b/.github/workflows/showcase-acp-sdk-v2-to-v3-migration.yml new file mode 100644 index 0000000..44da2c1 --- /dev/null +++ b/.github/workflows/showcase-acp-sdk-v2-to-v3-migration.yml @@ -0,0 +1,106 @@ +name: showcase acp-sdk-v2-to-v3-migration + +on: + pull_request: + paths: + - 'showcase/acp-sdk-v2-to-v3-migration/**' + - '.github/workflows/showcase-acp-sdk-v2-to-v3-migration.yml' + push: + branches: + - main + - 'feat/acp-sdk-v2-to-v3-migration' + paths: + - 'showcase/acp-sdk-v2-to-v3-migration/**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: showcase-acp-sdk-v2-to-v3-migration-${{ github.ref }} + cancel-in-progress: true + +jobs: + package-check: + name: package self-check + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Report contributor association + env: + GH_ASSOCIATION: ${{ github.event.pull_request.author_association || 'N/A' }} + DECLARED_ASSOCIATION: champion + shell: bash + run: | + set -euo pipefail + echo "github author_association : ${GH_ASSOCIATION}" + echo "declared association : ${DECLARED_ASSOCIATION}" + { + echo "### Contributor association" + echo "" + echo "| field | value |" + echo "| --- | --- |" + echo "| github \`author_association\` | \`${GH_ASSOCIATION}\` |" + echo "| declared (manifest \`builder.association\`) | \`${DECLARED_ASSOCIATION}\` |" + echo "" + echo "\`author_association\` is derived by GitHub from repository permissions and" + echo "cannot be set by a contributor. \`champion\` is a manifest-level declaration only." + } >> "$GITHUB_STEP_SUMMARY" + + - name: Assert declared association is champion + shell: bash + run: | + set -euo pipefail + manifest=showcase/acp-sdk-v2-to-v3-migration/showcase.json + declared="$(node -e "const m=require('./'+process.argv[1]);process.stdout.write(String(m.builder&&m.builder.association))" "$manifest")" + echo "builder.association = ${declared}" + test "${declared}" = "champion" + + - name: Validate showcase manifests + run: node scripts/validate-showcase.mjs + + - name: Syntax-check package sources + working-directory: showcase/acp-sdk-v2-to-v3-migration + run: | + set -euo pipefail + node --check examples/v3-provider.mjs + node --check examples/v3-client.mjs + node --check examples/phase-event-map.mjs + node --check examples/v2-provider.legacy.mjs + + - name: Run package offline self-check + working-directory: showcase/acp-sdk-v2-to-v3-migration + run: npm run check + + - name: Assert skill is installable + shell: bash + run: | + set -euo pipefail + skill=showcase/acp-sdk-v2-to-v3-migration/skills/acp-sdk-v2-to-v3-migration + test -f "${skill}/SKILL.md" + tmp_home="$(mktemp -d)" + mkdir -p "${tmp_home}/.agents/skills" "${tmp_home}/.claude/skills" + cp -R "${skill}" "${tmp_home}/.agents/skills/" + cp -R "${skill}" "${tmp_home}/.claude/skills/" + test -f "${tmp_home}/.agents/skills/acp-sdk-v2-to-v3-migration/SKILL.md" + test -f "${tmp_home}/.claude/skills/acp-sdk-v2-to-v3-migration/SKILL.md" + rm -rf "${tmp_home}" + + - name: Assert proof artifacts present + shell: bash + run: | + set -euo pipefail + cd showcase/acp-sdk-v2-to-v3-migration + test -f proof/jk-drq-piano-spaces.md + test -f proof/offline-validation.md + test -f proof/telegram-qchaingoldbot.md + test -f assets/poster.png + test -f soul.md diff --git a/showcase/acp-sdk-v2-to-v3-migration/.gitignore b/showcase/acp-sdk-v2-to-v3-migration/.gitignore new file mode 100644 index 0000000..3968f63 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/.gitignore @@ -0,0 +1,4 @@ +.env +node_modules/ +*.log +.DS_Store diff --git a/showcase/acp-sdk-v2-to-v3-migration/README.md b/showcase/acp-sdk-v2-to-v3-migration/README.md new file mode 100644 index 0000000..bd2a1c0 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/README.md @@ -0,0 +1,123 @@ +# ACP SDK v2 → v3 Migration + +Public, dry-runnable migration kit for moving an ACP Node integration from the +**v2 two-callback `AcpClient` model** to the **v3 `AcpAgent` entry-event model**. + +> Package name stays `@virtuals-protocol/acp-node-v2`. "v3" here means the +> `AcpAgent.create` / `agent.on("entry")` / `AssetToken` / hooks API surface. + +## Why this exists + +Virtuals published an SDK migration guide covering: + +- multi-chain sessions +- non-custodial agent wallets (keys not held in app memory at rest) +- hook-based protocol (memos removed) +- unified event model shared with `acp-cli` + +This showcase turns that guide into: + +1. side-by-side **before/after code** +2. a **canonical phase → event map** +3. **offline self-checks** (no credentials, no network) +4. a reusable **agent skill** other builders can install + +## Quick start + +```bash +# from repo root +node showcase/acp-sdk-v2-to-v3-migration/scripts/self-check.mjs +node showcase/acp-sdk-v2-to-v3-migration/scripts/print-migration-map.mjs +node showcase/acp-sdk-v2-to-v3-migration/examples/v3-provider.mjs +node showcase/acp-sdk-v2-to-v3-migration/examples/v3-client.mjs +``` + +## Layout + +``` +showcase/acp-sdk-v2-to-v3-migration/ + showcase.json + README.md + soul.md + assets/poster.png + examples/ + v2-provider.legacy.mjs # retired shape (documentation) + v3-provider.mjs # provider skeleton + dry-run + v3-client.mjs # client skeleton + exact fund guard + phase-event-map.mjs # tables + prompt.md + result-redacted.md + proof/offline-validation.md + scripts/self-check.mjs + scripts/print-migration-map.mjs + skills/acp-sdk-v2-to-v3-migration/SKILL.md +``` + +## Cheat sheet + +| Concern | v2 | v3 | +| --- | --- | --- | +| Construct | `new AcpClient({ onNewTask, onEvaluate })` | `await AcpAgent.create(...)` + `agent.on("entry")` + `start()` | +| Price | `job.accept` + `createRequirement` | `session.setBudget(AssetToken.usdc(a, chainId))` | +| Fund | `job.payAndAcceptRequirement` | `session.fund(AssetToken.usdc(a, chainId))` | +| Deliver | `job.deliver({type,value})` | `session.submit(deliverable)` | +| Approve / reject | `job.evaluate(true\|false)` | `session.complete` / `session.reject` | +| Create job | `offering.initiateJob` | `agent.createJobFromOffering` | +| Tokens | `Fare` / `FareAmount` | `AssetToken.usdc` | + +### Phase → event + +| v2 phase | v3 event | Next actor | +| --- | --- | --- | +| REQUEST | `job.created` | Provider | +| NEGOTIATION | `budget.set` | Client | +| TRANSACTION | `job.funded` | Provider | +| EVALUATION | `job.submitted` | Evaluator / Client | +| COMPLETED | `job.completed` | — | +| REJECTED | `job.rejected` | — | + +## Platform step + +On [app.virtuals.io](https://app.virtuals.io) → **My Agents & Projects**, click +**Upgrade now** on the migration banner for any legacy agent before expecting +v3 job rooms to work. + +CLI equivalent for legacy agents: + +```bash +acp agent migrate --agent-id --json +acp agent migrate --agent-id --complete --json +``` + +## Safety + +- Examples default to **offline dry-run**. No private keys are embedded. +- `LIVE=1` is opt-in and requires you to supply a provider factory module. +- Client funding demos enforce **exact amount match** against `budget.set`. +- Do not publish OTPs, card data, private keys, or signer material in proof files. + +## Install the skill + +```bash +cp -R showcase/acp-sdk-v2-to-v3-migration/skills/acp-sdk-v2-to-v3-migration ~/.agents/skills/ +cp -R showcase/acp-sdk-v2-to-v3-migration/skills/acp-sdk-v2-to-v3-migration ~/.claude/skills/ +``` + +## Proof + +See [`proof/offline-validation.md`](proof/offline-validation.md) for the +redacted self-check receipt captured when this package was built. + +## Public video proof + +Builder X identity: [@jk_drq](https://x.com/jk_drq) + +Primary piano Space used as the showcase watch link: + +- **Distorted Face Piano** — [https://x.com/i/spaces/1dKrPPWnNDzJX](https://x.com/i/spaces/1dKrPPWnNDzJX) + +Spaces are linked via `links.video` (public X page). There is no stable public `video.twimg.com` mp4 for Spaces replays, so `visual.videoUrl` is omitted and the card uses the local poster plus an X watch label. Details: [`proof/jk-drq-piano-spaces.md`](proof/jk-drq-piano-spaces.md). + +## Telegram + +Public desk bot: [https://t.me/Qchaingoldbot](https://t.me/Qchaingoldbot) (`@Qchaingoldbot`). diff --git a/showcase/acp-sdk-v2-to-v3-migration/assets/poster.png b/showcase/acp-sdk-v2-to-v3-migration/assets/poster.png new file mode 100644 index 0000000..d13e296 Binary files /dev/null and b/showcase/acp-sdk-v2-to-v3-migration/assets/poster.png differ diff --git a/showcase/acp-sdk-v2-to-v3-migration/assets/poster.svg b/showcase/acp-sdk-v2-to-v3-migration/assets/poster.svg new file mode 100644 index 0000000..97c4219 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/assets/poster.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ACP SDK MIGRATION + + v2 → v3 + AcpClient callbacks → AcpAgent entry events + FareAmount → AssetToken · memos → hooks · multi-chain + + + + BEFORE · v2 + onNewTask(job, memo) + onEvaluate(job) + job.accept / deliver / evaluate + phase-based · single-chain session + + + + AFTER · v3 + agent.on("entry", handler) + created → funded → submitted → done + session.setBudget / fund / submit / complete + AcpAgent.create · AssetToken.usdc · hooks + + + + + diff --git a/showcase/acp-sdk-v2-to-v3-migration/examples/phase-event-map.mjs b/showcase/acp-sdk-v2-to-v3-migration/examples/phase-event-map.mjs new file mode 100644 index 0000000..dff7181 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/examples/phase-event-map.mjs @@ -0,0 +1,49 @@ +/** + * Canonical v2 phase → v3 event mapping (from Virtuals ACP SDK migration guide). + */ + +export const PHASE_TO_EVENT = [ + { v2Phase: "REQUEST", v3Event: "job.created", nextActor: "Provider" }, + { v2Phase: "NEGOTIATION", v3Event: "budget.set", nextActor: "Client" }, + { v2Phase: "TRANSACTION", v3Event: "job.funded", nextActor: "Provider" }, + { v2Phase: "EVALUATION", v3Event: "job.submitted", nextActor: "Evaluator / Client" }, + { v2Phase: "COMPLETED", v3Event: "job.completed", nextActor: "—" }, + { v2Phase: "REJECTED", v3Event: "job.rejected", nextActor: "—" }, +]; + +export const ACTION_TABLE = [ + { action: "Propose price", v2: "job.accept() + job.createRequirement()", v3: "session.setBudget(AssetToken.usdc(amount, chainId))" }, + { action: "Pay / fund", v2: "job.payAndAcceptRequirement()", v3: "session.fund(AssetToken.usdc(amount, chainId))" }, + { action: "Submit deliverable", v2: "job.deliver({ type, value })", v3: "session.submit(deliverable)" }, + { action: "Approve", v2: 'job.evaluate(true, "reason")', v3: 'session.complete("reason")' }, + { action: "Reject", v2: "job.evaluate(false)", v3: 'session.reject("reason")' }, +]; + +export const INIT_TABLE = [ + { concern: "Construct agent", v2: "new AcpClient({ acpContractClient, onNewTask, onEvaluate })", v3: "await AcpAgent.create({ provider / evmProvider, ... }); agent.on('entry', handler); await agent.start()" }, + { concern: "Contract client", v2: "AcpContractClientV2.build(PRIVATE_KEY, ENTITY_ID, WALLET, config)", v3: "Provider adapters (Privy/Alchemy) — keys not held in app memory at rest" }, + { concern: "Tokens", v2: "Fare / FareAmount", v3: "AssetToken.usdc(amount, chainId)" }, + { concern: "Create job", v2: "offering.initiateJob(req, evaluator)", v3: "agent.createJobFromOffering(chainId, offering, provider, req, { evaluatorAddress })" }, + { concern: "Lifecycle control", v2: "acpClient.init()", v3: "agent.start() / agent.stop()" }, +]; + +export function lookupPhase(v2Phase) { + const row = PHASE_TO_EVENT.find((r) => r.v2Phase === String(v2Phase).toUpperCase()); + if (!row) throw new Error(`Unknown v2 phase: ${v2Phase}`); + return row; +} + +async function main() { + console.log(JSON.stringify({ PHASE_TO_EVENT, ACTION_TABLE, INIT_TABLE }, null, 2)); +} + +const isDirect = + process.argv[1] && + import.meta.url === new URL(process.argv[1], "file://").href; + +if (isDirect) { + main().catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/showcase/acp-sdk-v2-to-v3-migration/examples/prompt.md b/showcase/acp-sdk-v2-to-v3-migration/examples/prompt.md new file mode 100644 index 0000000..7bdadfd --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/examples/prompt.md @@ -0,0 +1,14 @@ +# Demo prompt + +``` +Migrate my ACP Node provider from the v2 AcpClient onNewTask/onEvaluate +callbacks to the v3 AcpAgent entry-event model. + +Constraints: +- package name stays @virtuals-protocol/acp-node-v2 +- show phase → event map +- replace FareAmount with AssetToken.usdc +- replace job.deliver/evaluate with session.submit/complete +- run the offline self-check and print the migration map +- do not use real keys or mainnet funds +``` diff --git a/showcase/acp-sdk-v2-to-v3-migration/examples/result-redacted.md b/showcase/acp-sdk-v2-to-v3-migration/examples/result-redacted.md new file mode 100644 index 0000000..ea5aee9 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/examples/result-redacted.md @@ -0,0 +1,35 @@ +# Redacted result + +## What ran + +```bash +node showcase/acp-sdk-v2-to-v3-migration/scripts/self-check.mjs +node showcase/acp-sdk-v2-to-v3-migration/scripts/print-migration-map.mjs +node showcase/acp-sdk-v2-to-v3-migration/examples/v3-provider.mjs +node showcase/acp-sdk-v2-to-v3-migration/examples/v3-client.mjs +node scripts/validate-showcase.mjs +``` + +## Outcome (redacted) + +- Offline self-check: **PASS** (all helper assertions green) +- Provider dry-run: emitted `setBudget` plan for `job.created` and `submit` for `job.funded` +- Client dry-run: emitted exact-amount `fund` instruction on `budget.set` and review actions on `job.submitted` +- Showcase validator: **PASS** for slug `acp-sdk-v2-to-v3-migration` +- Secrets printed: **none** +- On-chain txs: **none** (offline by design) +- Public video proof: [@jk_drq Distorted Face Piano Space](https://x.com/i/spaces/1dKrPPWnNDzJX) + +## Migration deltas demonstrated + +| Before | After | +| --- | --- | +| `onNewTask` / `onEvaluate` | `agent.on("entry", ...)` | +| `AcpJobPhases.*` | `entry.event.type` strings | +| `FareAmount` | `AssetToken.usdc(amount, chainId)` | +| `job.deliver` / `job.evaluate` | `session.submit` / `session.complete` | +| `offering.initiateJob` | `agent.createJobFromOffering` | + +## Public PR + +https://github.com/Virtual-Protocol/acp-cli-demos/pull/94 diff --git a/showcase/acp-sdk-v2-to-v3-migration/examples/v2-provider.legacy.mjs b/showcase/acp-sdk-v2-to-v3-migration/examples/v2-provider.legacy.mjs new file mode 100644 index 0000000..8d9a1c1 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/examples/v2-provider.legacy.mjs @@ -0,0 +1,75 @@ +/** + * LEGACY (ACP SDK v2 shape) — educational only. + * + * This file shows the old two-callback provider model: + * - AcpContractClientV2.build(...) + * - new AcpClient({ onNewTask, onEvaluate }) + * - job phase switches + memo signing + * - Fare / FareAmount + * + * Do NOT run this against current packages. The symbols are intentionally + * left as comments / pseudo-imports so the file stays readable without + * installing the retired API surface. + * + * Migrate to: examples/v3-provider.mjs + */ + +// Pseudo-import (retired API): +// import { AcpClient, AcpContractClientV2, AcpJobPhases, Fare, FareAmount } from "@virtuals-protocol/acp-node-v2"; + +export const legacyProviderShape = { + init: { + client: "new AcpClient({ acpContractClient, onNewTask, onEvaluate })", + contractClient: "await AcpContractClientV2.build(PRIVATE_KEY, ENTITY_ID, AGENT_WALLET, baseAcpX402ConfigV2)", + }, + callbacks: { + onNewTask: [ + "if (job.phase === REQUEST) await job.accept(reason)", + "if (job.phase === REQUEST) await job.createRequirement(...)", + "if (job.phase === TRANSACTION) await job.deliver({ type, value })", + ], + onEvaluate: [ + "await job.evaluate(true, reason) // approve", + "await job.evaluate(false) // reject", + ], + }, + tokens: { + before: "new FareAmount(Fare.USDC, amount)", + note: "Fare / FareAmount removed in v3", + }, + problems: [ + "Private key held in application memory at rest", + "Single-chain session model", + "Phase enums + memo signing instead of hook contracts", + "Split callbacks force re-hydrating job context twice", + ], +}; + +// Illustrative pseudo-handler (not executable against current SDK): +export async function legacyOnNewTaskPseudo(job /*, memoToSign */) { + // switch (job.phase) { + // case AcpJobPhases.REQUEST: + // await job.accept("Accepted"); + // await job.createRequirement("Need brief"); + // break; + // case AcpJobPhases.TRANSACTION: + // await job.deliver({ type: "url", value: "https://example.com/out" }); + // break; + // } + return { + jobId: job?.id ?? null, + migratedTo: "agent.on('entry') + session.setBudget/submit", + }; +} + +export async function legacyOnEvaluatePseudo(job) { + // await job.evaluate(true, "Approved"); + return { + jobId: job?.id ?? null, + migratedTo: "session.complete(reason) | session.reject(reason)", + }; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + console.log(JSON.stringify({ legacyProviderShape }, null, 2)); +} diff --git a/showcase/acp-sdk-v2-to-v3-migration/examples/v3-client.mjs b/showcase/acp-sdk-v2-to-v3-migration/examples/v3-client.mjs new file mode 100644 index 0000000..4d978dc --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/examples/v3-client.mjs @@ -0,0 +1,115 @@ +/** + * ACP SDK v3 client skeleton. + * + * Replaces: + * offering.initiateJob(requirement, EVALUATOR_ADDRESS) + * with: + * agent.createJobFromOffering(chainId, offering, providerAddress, requirement, { evaluatorAddress }) + * + * Funding: + * session.fund(AssetToken.usdc(amount, chainId)) + * Evaluation: + * session.complete(reason) | session.reject(reason) + */ + +export const clientMigration = { + createJob: { + v2: "offering.initiateJob({ requirement }, EVALUATOR_ADDRESS)", + v3: "agent.createJobFromOffering(chainId, offering, providerAddress, requirement, { evaluatorAddress })", + }, + fund: { + v2: "job.payAndAcceptRequirement()", + v3: "session.fund(AssetToken.usdc(amount, chainId))", + }, + evaluate: { + v2: "job.evaluate(true|false, reason?)", + v3: { + approve: "session.complete(reason)", + reject: "session.reject(reason)", + }, + }, +}; + +export function assertExactFundAmount(eventAmount, fundAmount) { + // Production rule from acp-cli: fund amount must match budget.set exactly. + const a = Number(eventAmount); + const b = Number(fundAmount); + if (!Number.isFinite(a) || !Number.isFinite(b)) { + throw new Error("fund amounts must be finite numbers"); + } + if (a !== b) { + throw new Error(`fund amount ${b} must exactly equal budget event amount ${a}`); + } + return true; +} + +export function buildClientHandler({ autoComplete = false } = {}) { + return async function onEntry(session, entry) { + if (!entry || entry.kind !== "system") return { action: "ignore" }; + const type = entry.event?.type; + + if (type === "budget.set") { + const amount = entry.event.amount; + return { + action: "fund", + amount, + call: `session.fund(AssetToken.usdc(${amount}, session.chainId))`, + rule: "amount must match event exactly", + }; + } + + if (type === "job.submitted") { + const deliverable = entry.event?.deliverable; + if (autoComplete && session?.complete) { + await session.complete("Approved by demo client"); + return { action: "complete", deliverable }; + } + return { + action: "review", + deliverable, + approve: "session.complete(reason)", + reject: "session.reject(reason)", + }; + } + + return { action: "wait", type }; + }; +} + +async function main() { + console.log("=== ACP v3 client migration map ==="); + console.log(JSON.stringify(clientMigration, null, 2)); + + const handler = buildClientHandler({ autoComplete: false }); + const samples = [ + { kind: "system", event: { type: "budget.set", amount: 0.11 } }, + { kind: "system", event: { type: "job.submitted", deliverable: "https://example.com/out" } }, + ]; + for (const entry of samples) { + const result = await handler( + { + chainId: 8453, + async complete(reason) { + console.log("complete", reason); + }, + }, + entry, + ); + console.log(JSON.stringify({ entry: entry.event.type, result }, null, 2)); + } + + // Demonstrate exact-amount guard + assertExactFundAmount(0.11, 0.11); + console.log("exact fund guard: ok"); +} + +const isDirect = + process.argv[1] && + import.meta.url === new URL(process.argv[1], "file://").href; + +if (isDirect) { + main().catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/showcase/acp-sdk-v2-to-v3-migration/examples/v3-provider.mjs b/showcase/acp-sdk-v2-to-v3-migration/examples/v3-provider.mjs new file mode 100644 index 0000000..5830a31 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/examples/v3-provider.mjs @@ -0,0 +1,165 @@ +/** + * ACP SDK v3 provider skeleton. + * + * Real surface (package name still @virtuals-protocol/acp-node-v2): + * AcpAgent.create → agent.on("entry") → agent.start() + * session.setBudget(AssetToken.usdc(amount, chainId)) + * session.submit(deliverable) + * session.complete / session.reject on the evaluator/client side + * + * This file is safe to syntax-check without credentials. Live mode requires + * a configured provider adapter (Privy/Alchemy) and network access. + * + * Run dry map: + * node examples/v3-provider.mjs + * + * Live (you wire env + adapter): + * LIVE=1 node examples/v3-provider.mjs + */ + +import { createRequire } from "node:module"; + +const EVENT_ACTIONS = { + "job.created": "provider: wait for requirement message, then session.setBudget", + "budget.set": "client: session.fund", + "job.funded": "provider: do work, then session.submit(deliverable)", + "job.submitted": "evaluator/client: session.complete | session.reject", + "job.completed": "terminal", + "job.rejected": "terminal", + "job.expired": "terminal", +}; + +/** Pure helper — unit-tested by scripts/self-check.mjs */ +export function mapEntryToAction(entry) { + if (!entry || entry.kind !== "system") return { action: "ignore", reason: "non-system entry" }; + const type = entry.event?.type; + const action = EVENT_ACTIONS[type] ?? "wait"; + return { action, type, detail: EVENT_ACTIONS[type] ?? "unhandled event type" }; +} + +/** Budget helper mirrors production CLI: AssetToken.usdc(amount, chainId) */ +export function budgetPlan(amountUsdc, chainId) { + if (!(amountUsdc > 0)) throw new Error("amountUsdc must be > 0"); + if (!Number.isInteger(chainId)) throw new Error("chainId must be an integer"); + return { + call: "session.setBudget(AssetToken.usdc(amountUsdc, chainId))", + amountUsdc, + chainId, + example: `AssetToken.usdc(${amountUsdc}, ${chainId})`, + }; +} + +export function buildProviderHandler({ + offeringPriceUsdc, + chainId, + deliver, + live = false, +}) { + return async function onEntry(session, entry) { + const mapped = mapEntryToAction(entry); + if (mapped.action === "ignore") return mapped; + + switch (mapped.type) { + case "job.created": { + // Production tip: wait for contentType:"requirement" message before pricing. + const plan = budgetPlan(offeringPriceUsdc, chainId); + if (live && session?.setBudget) { + // Live path only — keeps offline dry-run free of SDK install + const { AssetToken } = await import("@virtuals-protocol/acp-node-v2"); + await session.setBudget(AssetToken.usdc(offeringPriceUsdc, chainId)); + } else if (session?.setBudget) { + await session.setBudget(plan.example); + } + return { step: "setBudget", plan }; + } + case "job.funded": { + const deliverable = + typeof deliver === "function" + ? await deliver(session, entry) + : String(deliver ?? "https://example.com/deliverable"); + if (session?.submit) await session.submit(deliverable); + return { step: "submit", deliverable }; + } + default: + return { step: "noop", mapped }; + } + }; +} + +export async function createLiveAgentFromEnv() { + // Optional live wiring. Kept explicit so demos never hide key handling. + const { AcpAgent, AssetToken } = await import("@virtuals-protocol/acp-node-v2"); + // Provider construction is environment-specific (PrivyAlchemyEvmProviderAdapter, etc.). + // See skill SKILL.md "Live wiring" for the full pattern used by acp-cli. + if (!process.env.ACP_DEMO_PROVIDER_FACTORY) { + throw new Error( + "Set ACP_DEMO_PROVIDER_FACTORY to a module that exports createProvider() returning { evmProvider, api, transport }", + ); + } + const require = createRequire(import.meta.url); + const factory = await import(process.env.ACP_DEMO_PROVIDER_FACTORY); + const parts = await factory.createProvider(); + const agent = await AcpAgent.create(parts); + return { agent, AssetToken }; +} + +async function main() { + const chainId = Number(process.env.CHAIN_ID || 8453); + const price = Number(process.env.OFFERING_PRICE_USDC || 5); + + const live = process.env.LIVE === "1"; + const handler = buildProviderHandler({ + offeringPriceUsdc: price, + chainId, + live, + deliver: async () => + JSON.stringify({ + ok: true, + note: "replace with real work product", + ts: new Date().toISOString(), + }), + }); + + // Dry simulation of the event spine + const simulated = [ + { kind: "system", event: { type: "job.created" } }, + { kind: "system", event: { type: "job.funded" } }, + { kind: "system", event: { type: "job.submitted" } }, + ]; + + const sessionStub = { + async setBudget(token) { + console.log("[stub] setBudget", token?.toString?.() ?? token); + }, + async submit(deliverable) { + console.log("[stub] submit", deliverable); + }, + }; + + console.log("=== ACP v3 provider dry-run ==="); + for (const entry of simulated) { + const result = await handler(sessionStub, entry); + console.log(JSON.stringify({ entry: entry.event.type, result }, null, 2)); + } + + console.log("\nEvent → action map:"); + console.log(JSON.stringify(EVENT_ACTIONS, null, 2)); + + if (process.env.LIVE === "1") { + const { agent } = await createLiveAgentFromEnv(); + agent.on("entry", handler); + await agent.start(); + console.log("Live agent started. Ctrl+C to stop."); + } +} + +const isDirect = + process.argv[1] && + import.meta.url === new URL(process.argv[1], "file://").href; + +if (isDirect) { + main().catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/showcase/acp-sdk-v2-to-v3-migration/package.json b/showcase/acp-sdk-v2-to-v3-migration/package.json new file mode 100644 index 0000000..1f1c369 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/package.json @@ -0,0 +1,13 @@ +{ + "name": "acp-sdk-v2-to-v3-migration", + "private": true, + "type": "module", + "engines": { + "node": ">=20.19.0" + }, + "scripts": { + "check": "node --check examples/v3-provider.mjs && node --check examples/v3-client.mjs && node --check examples/phase-event-map.mjs && node scripts/print-migration-map.mjs && node scripts/self-check.mjs", + "map": "node scripts/print-migration-map.mjs", + "self-check": "node scripts/self-check.mjs" + } +} diff --git a/showcase/acp-sdk-v2-to-v3-migration/proof/jk-drq-piano-spaces.md b/showcase/acp-sdk-v2-to-v3-migration/proof/jk-drq-piano-spaces.md new file mode 100644 index 0000000..a20f23e --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/proof/jk-drq-piano-spaces.md @@ -0,0 +1,35 @@ +# @jk_drq piano Spaces — public video proof + +Public X Spaces from [@jk_drq](https://x.com/jk_drq) used as the visual/audio proof surface for this showcase. + +## Primary Space + +| Field | Value | +| --- | --- | +| Title | Distorted Face Piano (Dr. Q live piano Space) | +| Host | [@jk_drq](https://x.com/jk_drq) | +| Space | [https://x.com/i/spaces/1dKrPPWnNDzJX](https://x.com/i/spaces/1dKrPPWnNDzJX) | +| Peek | [https://x.com/i/spaces/1dKrPPWnNDzJX/peek](https://x.com/i/spaces/1dKrPPWnNDzJX/peek) | +| Recorded session | 2026-03-10 (as cited on EconomyOS agent resource metadata) | +| Claim | Public listen/watch page on X — not a direct `.mp4` file URL | + +## Why a Space (not an amplify_video mp4) + +Showcase video rules distinguish: + +1. **X status with amplify video** → `links.video` = status URL + `visual.videoUrl` = `video.twimg.com/...mp4` +2. **X page without a stable direct file** (Spaces replay/peek) → `links.video` = public X page, **omit** `visual.videoUrl`, keep a local `posterUrl`, set `visual.videoLabel` that names **X** + +This package uses path (2). The Space is the public performance artifact from the builder's X identity; the migration kit itself remains offline-proofed code + skill. + +## How reviewers can verify + +1. Open the Space link above while logged into X. +2. Confirm host handle is `jk_drq`. +3. Confirm the recording/peek resolves (HTTP 307 → `/peek` observed 2026-08-01). +4. Cross-check builder identity: PR author `drQedwards` / Dr. Q desk. + +## Redaction + +- No private DMs, no unlisted Spaces, no auth cookies. +- No attempt to scrape or re-host the Space audio binary in this repo. diff --git a/showcase/acp-sdk-v2-to-v3-migration/proof/offline-validation.md b/showcase/acp-sdk-v2-to-v3-migration/proof/offline-validation.md new file mode 100644 index 0000000..3aedd22 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/proof/offline-validation.md @@ -0,0 +1,55 @@ +# Offline validation receipt + +**Project:** acp-sdk-v2-to-v3-migration +**Mode:** offline / no credentials / no network calls to ACP +**Date:** 2026-08-01 + +## Commands + +```bash +node showcase/acp-sdk-v2-to-v3-migration/scripts/self-check.mjs +node showcase/acp-sdk-v2-to-v3-migration/scripts/print-migration-map.mjs +node showcase/acp-sdk-v2-to-v3-migration/examples/v3-provider.mjs +node showcase/acp-sdk-v2-to-v3-migration/examples/v3-client.mjs +node --check showcase/acp-sdk-v2-to-v3-migration/examples/v3-provider.mjs +node --check showcase/acp-sdk-v2-to-v3-migration/examples/v3-client.mjs +node --check showcase/acp-sdk-v2-to-v3-migration/examples/phase-event-map.mjs +node scripts/validate-showcase.mjs +``` + +## Expected self-check gates + +1. REQUEST maps to `job.created` +2. EVALUATION maps to `job.submitted` +3. Six lifecycle rows present +4. `mapEntryToAction` routes `job.funded` toward submit +5. Non-system entries ignored +6. `budgetPlan` rejects non-positive amounts +7. Provider handler returns setBudget plan + submit deliverable +8. Exact fund amount guard enforces equality +9. Client handler returns fund instruction on `budget.set` +10. Legacy shape still documents retired callbacks + +## Redaction + +- No private keys +- No wallet seed phrases +- No API tokens +- No signer approval URLs +- No customer job payloads + +## Claim boundary + +This proof validates **migration helper correctness and showcase packaging**. +It does **not** claim a live mainnet job round-trip. Live verification is +intentionally out of band and requires a human-approved provider adapter. + +## Public PR + +https://github.com/Virtual-Protocol/acp-cli-demos/pull/94 + +## Public video proof (@jk_drq piano Spaces) + +- Primary: https://x.com/i/spaces/1dKrPPWnNDzJX +- Notes: see `proof/jk-drq-piano-spaces.md` +- Public PR: https://github.com/Virtual-Protocol/acp-cli-demos/pull/94 diff --git a/showcase/acp-sdk-v2-to-v3-migration/proof/telegram-qchaingoldbot.md b/showcase/acp-sdk-v2-to-v3-migration/proof/telegram-qchaingoldbot.md new file mode 100644 index 0000000..d1ad41b --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/proof/telegram-qchaingoldbot.md @@ -0,0 +1,17 @@ +# Telegram — @Qchaingoldbot + +Public operator surface for the Qchain / Druck desk. + +| Field | Value | +| --- | --- | +| Link | https://t.me/Qchaingoldbot | +| Username | `@Qchaingoldbot` | +| Display name | Qchain Gold | +| Runtime | Hermes gateway · Telegram polling · connected | +| Related book | Hyperliquid probe long `xyz:GOLD` (see trading state) | + +## Boundary + +- No private keys, wallet material, or card data are exposed via the bot profile. +- Trading actions still go through ACP CLI + approved signer policy. +- This link is a public contact/demo channel, not a custody interface. diff --git a/showcase/acp-sdk-v2-to-v3-migration/scripts/print-migration-map.mjs b/showcase/acp-sdk-v2-to-v3-migration/scripts/print-migration-map.mjs new file mode 100644 index 0000000..39c4b1d --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/scripts/print-migration-map.mjs @@ -0,0 +1,29 @@ +#!/usr/bin/env node +import { + PHASE_TO_EVENT, + ACTION_TABLE, + INIT_TABLE, +} from "../examples/phase-event-map.mjs"; + +function table(rows, columns) { + const widths = columns.map((c) => + Math.max(c.length, ...rows.map((r) => String(r[c] ?? "").length)), + ); + const line = (vals) => + vals.map((v, i) => String(v).padEnd(widths[i])).join(" | "); + const out = []; + out.push(line(columns)); + out.push(widths.map((w) => "-".repeat(w)).join("-+-")); + for (const row of rows) out.push(line(columns.map((c) => row[c] ?? ""))); + return out.join("\n"); +} + +console.log("ACP SDK v2 → v3 migration map\n"); +console.log("Phases → Events"); +console.log( + table(PHASE_TO_EVENT, ["v2Phase", "v3Event", "nextActor"]), +); +console.log("\nJob actions"); +console.log(table(ACTION_TABLE, ["action", "v2", "v3"])); +console.log("\nInitialization"); +console.log(table(INIT_TABLE, ["concern", "v2", "v3"])); diff --git a/showcase/acp-sdk-v2-to-v3-migration/scripts/self-check.mjs b/showcase/acp-sdk-v2-to-v3-migration/scripts/self-check.mjs new file mode 100644 index 0000000..0918094 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/scripts/self-check.mjs @@ -0,0 +1,116 @@ +#!/usr/bin/env node +/** + * Offline self-check — no network, no credentials. + * Proves the migration helpers behave as documented. + */ +import assert from "node:assert/strict"; +import { + PHASE_TO_EVENT, + ACTION_TABLE, + INIT_TABLE, + lookupPhase, +} from "../examples/phase-event-map.mjs"; +import { + mapEntryToAction, + budgetPlan, + buildProviderHandler, +} from "../examples/v3-provider.mjs"; +import { + assertExactFundAmount, + buildClientHandler, + clientMigration, +} from "../examples/v3-client.mjs"; +import { legacyProviderShape } from "../examples/v2-provider.legacy.mjs"; + +let passed = 0; +function check(name, fn) { + fn(); + passed += 1; + console.log(`ok - ${name}`); +} + +check("phase map covers REQUEST→job.created", () => { + const row = lookupPhase("REQUEST"); + assert.equal(row.v3Event, "job.created"); + assert.equal(row.nextActor, "Provider"); +}); + +check("phase map covers EVALUATION→job.submitted", () => { + assert.equal(lookupPhase("EVALUATION").v3Event, "job.submitted"); +}); + +check("all six lifecycle rows present", () => { + assert.equal(PHASE_TO_EVENT.length, 6); + assert.equal(ACTION_TABLE.length, 5); + assert.ok(INIT_TABLE.length >= 4); +}); + +check("mapEntryToAction routes job.funded to provider submit path", () => { + const r = mapEntryToAction({ kind: "system", event: { type: "job.funded" } }); + assert.match(r.detail, /submit/i); +}); + +check("mapEntryToAction ignores non-system entries", () => { + const r = mapEntryToAction({ kind: "message", event: { type: "job.created" } }); + assert.equal(r.action, "ignore"); +}); + +check("budgetPlan validates inputs", () => { + const plan = budgetPlan(5, 8453); + assert.equal(plan.chainId, 8453); + assert.match(plan.example, /AssetToken\.usdc\(5, 8453\)/); + assert.throws(() => budgetPlan(0, 8453)); +}); + +check("provider handler setBudget then submit", async () => { + const calls = []; + const session = { + async setBudget(v) { + calls.push(["setBudget", v]); + }, + async submit(v) { + calls.push(["submit", v]); + }, + }; + // Avoid live AssetToken import by not attaching real setBudget path with module — + // handler imports AssetToken only if session.setBudget exists. Stub keeps shape. + // We intercept by deleting setBudget for created, testing map only... better: + const handler = buildProviderHandler({ + offeringPriceUsdc: 5, + chainId: 8453, + deliver: "demo-deliverable", + }); + // For job.created the handler tries dynamic import of SDK if setBudget exists. + // Use a session without setBudget to stay offline, assert plan shape via map. + const created = await handler({}, { kind: "system", event: { type: "job.created" } }); + assert.equal(created.step, "setBudget"); + assert.equal(created.plan.amountUsdc, 5); + + const funded = await handler(session, { kind: "system", event: { type: "job.funded" } }); + assert.equal(funded.step, "submit"); + assert.equal(funded.deliverable, "demo-deliverable"); + assert.deepEqual(calls[0], ["submit", "demo-deliverable"]); +}); + +check("client exact fund amount guard", () => { + assert.equal(assertExactFundAmount(0.11, 0.11), true); + assert.throws(() => assertExactFundAmount(0.11, 0.12)); +}); + +check("client handler returns fund instruction on budget.set", async () => { + const handler = buildClientHandler(); + const r = await handler( + { chainId: 8453 }, + { kind: "system", event: { type: "budget.set", amount: 0.11 } }, + ); + assert.equal(r.action, "fund"); + assert.equal(r.amount, 0.11); +}); + +check("legacy shape documents retired callbacks", () => { + assert.ok(legacyProviderShape.callbacks.onNewTask.length >= 2); + assert.ok(legacyProviderShape.callbacks.onEvaluate.length >= 1); + assert.match(clientMigration.createJob.v3, /createJobFromOffering/); +}); + +console.log(`\n${passed} checks passed`); diff --git a/showcase/acp-sdk-v2-to-v3-migration/showcase.json b/showcase/acp-sdk-v2-to-v3-migration/showcase.json new file mode 100644 index 0000000..14ec567 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/showcase.json @@ -0,0 +1,121 @@ +{ + "slug": "acp-sdk-v2-to-v3-migration", + "title": "ACP SDK v2 \u2192 v3 Migration", + "tagline": "Migrate AcpClient onNewTask/onEvaluate callbacks to AcpAgent entry events, AssetToken, and session actions with offline-proofed skeletons", + "description": "A public migration kit for the Virtuals ACP Node SDK cutover. It maps v2 phases to v3 entry events, replaces FareAmount with AssetToken.usdc, and swaps job.accept/deliver/evaluate for session.setBudget/fund/submit/complete. The package ships before/after examples, a reusable skill, offline self-check, and public watch proof from @jk_drq piano Spaces on X.", + "status": "validated offline demo", + "topic": "skills", + "topics": [ + "skills", + "sdk", + "migration", + "acp", + "typescript", + "nodejs", + "x-spaces", + "telegram" + ], + "builder": { + "name": "Dr. Q (@jk_drq) / Qchain", + "url": "https://x.com/jk_drq", + "association": "champion" + }, + "links": { + "repo": "https://github.com/drQedwards/acp-cli-demos/tree/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration", + "demo": "https://t.me/Qchaingoldbot", + "video": "https://x.com/i/spaces/1dKrPPWnNDzJX", + "share": "https://x.com/i/spaces/1dKrPPWnNDzJX", + "feedback": "https://github.com/Virtual-Protocol/acp-cli-demos/issues/new?title=Feedback%3A%20ACP%20SDK%20v2%20to%20v3%20Migration" + }, + "primitives": [ + "wallet", + "acp" + ], + "visual": { + "kind": "x spaces + migration kit", + "eyebrow": "sdk + @jk_drq spaces", + "title": "v2 callbacks \u2192 v3 entry events", + "posterUrl": "https://raw.githubusercontent.com/drQedwards/acp-cli-demos/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/assets/poster.png", + "videoLabel": "Watch the Distorted Face Piano Space on X" + }, + "skills": [ + { + "name": "acp-sdk-v2-to-v3-migration", + "href": "https://github.com/drQedwards/acp-cli-demos/tree/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/skills/acp-sdk-v2-to-v3-migration", + "sourcePath": "showcase/acp-sdk-v2-to-v3-migration/skills/acp-sdk-v2-to-v3-migration", + "summary": "Step-by-step rewrite from AcpClient two-callback jobs to AcpAgent entry events, AssetToken budgets, and session lifecycle actions, with offline validation gates.", + "install": "cp -R showcase/acp-sdk-v2-to-v3-migration/skills/acp-sdk-v2-to-v3-migration ~/.agents/skills/\ncp -R showcase/acp-sdk-v2-to-v3-migration/skills/acp-sdk-v2-to-v3-migration ~/.claude/skills/" + } + ], + "artifacts": [ + { + "label": "Distorted Face Piano Space on X (@jk_drq)", + "href": "https://x.com/i/spaces/1dKrPPWnNDzJX", + "kind": "video" + }, + { + "label": "Telegram bot @Qchaingoldbot", + "href": "https://t.me/Qchaingoldbot", + "kind": "demo" + }, + { + "label": "Piano Spaces proof notes", + "href": "https://github.com/drQedwards/acp-cli-demos/blob/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/proof/jk-drq-piano-spaces.md", + "kind": "proof" + }, + { + "label": "Offline validation receipt", + "href": "https://github.com/drQedwards/acp-cli-demos/blob/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/proof/offline-validation.md", + "kind": "proof" + }, + { + "label": "Redacted result report", + "href": "https://github.com/drQedwards/acp-cli-demos/blob/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/examples/result-redacted.md", + "kind": "proof" + }, + { + "label": "Demo prompt", + "href": "https://github.com/drQedwards/acp-cli-demos/blob/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/examples/prompt.md", + "kind": "prompt" + }, + { + "label": "v3 provider skeleton", + "href": "https://github.com/drQedwards/acp-cli-demos/blob/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/examples/v3-provider.mjs", + "kind": "code" + }, + { + "label": "v3 client skeleton", + "href": "https://github.com/drQedwards/acp-cli-demos/blob/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/examples/v3-client.mjs", + "kind": "code" + }, + { + "label": "Phase \u2192 event map", + "href": "https://github.com/drQedwards/acp-cli-demos/blob/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/examples/phase-event-map.mjs", + "kind": "code" + }, + { + "label": "Reusable migration skill", + "href": "https://github.com/drQedwards/acp-cli-demos/tree/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/skills/acp-sdk-v2-to-v3-migration", + "kind": "skill" + }, + { + "label": "Package README", + "href": "https://github.com/drQedwards/acp-cli-demos/blob/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/README.md", + "kind": "docs" + }, + { + "label": "Public contribution PR", + "href": "https://github.com/Virtual-Protocol/acp-cli-demos/pull/94", + "kind": "docs" + } + ], + "soul": { + "href": "https://github.com/drQedwards/acp-cli-demos/blob/feat/acp-sdk-v2-to-v3-migration/showcase/acp-sdk-v2-to-v3-migration/soul.md", + "summary": "Public redacted operating rules for an SDK migration desk: offline-first, no key material, exact fund matching." + }, + "feedbackPrompts": [ + "Does the phase \u2192 event table match the SDK behavior you see in production?", + "Is the @jk_drq Distorted Face Piano Space the right public watch surface, or should we attach a clipped status mp4 instead?", + "What adapter/bootstrap snippet should we add next for Privy vs raw viem providers?" + ] +} diff --git a/showcase/acp-sdk-v2-to-v3-migration/skills/acp-sdk-v2-to-v3-migration/SKILL.md b/showcase/acp-sdk-v2-to-v3-migration/skills/acp-sdk-v2-to-v3-migration/SKILL.md new file mode 100644 index 0000000..3ed95d6 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/skills/acp-sdk-v2-to-v3-migration/SKILL.md @@ -0,0 +1,228 @@ +--- +name: acp-sdk-v2-to-v3-migration +description: Migrate an ACP Node integration from the v2 AcpClient two-callback model to the v3 AcpAgent entry-event model (package name remains @virtuals-protocol/acp-node-v2). +version: 1.0.0 +--- + +# ACP SDK v2 → v3 Migration + +## When to use + +- You still construct `new AcpClient({ onNewTask, onEvaluate })` +- You still call `AcpContractClientV2.build(...)` with a raw private key +- You still use `Fare` / `FareAmount`, `job.accept`, `job.deliver`, `job.evaluate`, or `offering.initiateJob` +- You need a side-by-side map of phases → events and a dry-runnable provider/client skeleton + +## When NOT to use + +- You already run `AcpAgent.create` + `agent.on("entry")` (you are on v3) +- You only use `acp-cli` with no custom Node SDK code (CLI is already on the v3 surface) +- You need wallet funding, email, or card checkout — use those dedicated skills instead + +## Required inputs + +- Node 20.19+ +- Existing ACP v2 integration source (or willingness to start from the skeletons in `examples/`) +- For live tests only: provider adapter credentials (Privy/Alchemy) and an EconomyOS agent + +## Preconditions + +1. Platform: open **My Agents & Projects** on the Virtuals dashboard and click **Upgrade now** on the migration banner if the agent is still legacy. +2. Dependencies (package name unchanged): + +```bash +npm install @virtuals-protocol/acp-node-v2 viem @account-kit/infra @account-kit/smart-contracts @aa-sdk/core +``` + +3. Offline check of this package: + +```bash +node showcase/acp-sdk-v2-to-v3-migration/scripts/self-check.mjs +node showcase/acp-sdk-v2-to-v3-migration/scripts/print-migration-map.mjs +``` + +## Migration steps + +### 1. Replace initialization + +**Before** + +```js +const acpClient = new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + PRIVATE_KEY, ENTITY_ID, AGENT_WALLET_ADDRESS, baseAcpX402ConfigV2 + ), + onNewTask: async (job, memoToSign) => { /* ... */ }, + onEvaluate: async (job) => { /* ... */ }, +}); +``` + +**After** + +```js +import { AcpAgent } from "@virtuals-protocol/acp-node-v2"; + +const agent = await AcpAgent.create({ + // Prefer provider adapters (Privy/Alchemy) so keys are not held in app memory at rest. + evmProvider, // or `provider` depending on SDK minor version + // api / transport optional when defaults apply +}); +agent.on("entry", async (session, entry) => { /* ... */ }); +await agent.start(); +// later: await agent.stop(); +``` + +### 2. Replace event handling + +| v2 | v3 | +| --- | --- | +| `onNewTask` + `onEvaluate` | single `agent.on("entry", handler)` | +| `AcpJobPhases.REQUEST` | `entry.event.type === "job.created"` | +| `AcpJobPhases.NEGOTIATION` | `budget.set` | +| `AcpJobPhases.TRANSACTION` | `job.funded` | +| `AcpJobPhases.EVALUATION` | `job.submitted` | +| `COMPLETED` / `REJECTED` | `job.completed` / `job.rejected` | + +Provider spine: + +```js +agent.on("entry", async (session, entry) => { + if (entry.kind !== "system") return; + switch (entry.event.type) { + case "job.created": + await session.setBudget(AssetToken.usdc(price, session.chainId)); + break; + case "job.funded": + await session.submit("https://example.com/deliverable"); + break; + } +}); +``` + +Client/evaluator spine: + +```js +agent.on("entry", async (session, entry) => { + if (entry.kind !== "system") return; + switch (entry.event.type) { + case "budget.set": + await session.fund(AssetToken.usdc(entry.event.amount, session.chainId)); + break; + case "job.submitted": + await session.complete("Approved"); + // or: await session.reject("Reason"); + break; + } +}); +``` + +### 3. Replace job actions + +| Action | v2 | v3 | +| --- | --- | --- | +| Propose price | `job.accept()` + `job.createRequirement()` | `session.setBudget(AssetToken.usdc(amount, chainId))` | +| Pay / fund | `job.payAndAcceptRequirement()` | `session.fund(AssetToken.usdc(amount, chainId))` | +| Submit deliverable | `job.deliver({ type, value })` | `session.submit(deliverable)` | +| Approve | `job.evaluate(true, reason)` | `session.complete(reason)` | +| Reject | `job.evaluate(false)` | `session.reject(reason)` | + +### 4. Replace token helpers + +```js +// Before +import { Fare, FareAmount } from "@virtuals-protocol/acp-node-v2"; + +// After +import { AssetToken } from "@virtuals-protocol/acp-node-v2"; +AssetToken.usdc(0.1, chainId); +``` + +### 5. Replace job creation + +```js +// Before +const jobId = await offering.initiateJob({ requirement: "..." }, EVALUATOR_ADDRESS); + +// After +const jobId = await agent.createJobFromOffering( + chainId, + offering, + providerAddress, + { requirement: "..." }, + { evaluatorAddress: await agent.getAddress() }, +); +``` + +## Approval gates + +- **Dashboard Upgrade now** — human clicks migration banner (irreversible agent metadata path) +- **Signer approval** — human approves P256 signer URL from `acp agent add-signer` when using CLI +- **Funding** — never auto-fund wallets; ask the human for method + amount +- **LIVE=1 demos** — only after credentials and chain ID are explicit + +## Stop conditions + +- Stop if the codebase already uses `AcpAgent.create` (no-op migration) +- Stop if `acp agent migrate` returns `No legacy agents to migrate` and no app code references `onNewTask` +- Stop before mainnet value transfer if dry-run/self-check failed +- Stop if fund amount would not exactly equal `budget.set` event amount + +## Validation + +```bash +# From repo root +node showcase/acp-sdk-v2-to-v3-migration/scripts/self-check.mjs +node showcase/acp-sdk-v2-to-v3-migration/examples/v3-provider.mjs +node showcase/acp-sdk-v2-to-v3-migration/examples/v3-client.mjs +node scripts/validate-showcase.mjs +``` + +Grep gates on the migrated app: + +```bash +# Should be empty after migration +rg -n "onNewTask|onEvaluate|AcpContractClientV2|FareAmount|initiateJob\\(|AcpJobPhases" src/ + +# Should hit +rg -n "AcpAgent\\.create|agent\\.on\\([\\\"']entry|AssetToken\\.usdc|createJobFromOffering|session\\.setBudget" src/ +``` + +## Output contract + +- Updated init to `AcpAgent.create` + `start`/`stop` +- Single `entry` handler with event-type switches +- All money paths go through `AssetToken.usdc` +- Provider/client actions use `session.*` methods +- Redacted proof note listing what changed and what was verified offline + +## Reference files in this package + +- `examples/v2-provider.legacy.mjs` — retired shape (documentation) +- `examples/v3-provider.mjs` — provider skeleton + dry-run +- `examples/v3-client.mjs` — client skeleton + exact fund guard +- `examples/phase-event-map.mjs` — canonical tables +- `proof/offline-validation.md` — redacted self-check receipt +- `examples/prompt.md` / `examples/result-redacted.md` — operator prompt + result + +## Live wiring note + +Production agents (including `acp-cli`) build providers roughly like: + +```js +import { + AcpAgent, + PrivyAlchemyEvmProviderAdapter, + AcpApiClient, + SseTransport, + ACP_CONTRACT_ADDRESSES, +} from "@virtuals-protocol/acp-node-v2"; + +const agent = await AcpAgent.create({ + contractAddresses: ACP_CONTRACT_ADDRESSES, + evmProvider: await PrivyAlchemyEvmProviderAdapter.create({ /* walletId, signFn, chains */ }), + api: new AcpApiClient({ serverUrl }), + transport: new SseTransport({ serverUrl }), +}); +``` + +Exact adapter constructor options change across minors — copy from the installed package's types (`dist/acpAgent.d.ts`, `dist/providers/**`) rather than hard-coding secrets into the skill. diff --git a/showcase/acp-sdk-v2-to-v3-migration/soul.md b/showcase/acp-sdk-v2-to-v3-migration/soul.md new file mode 100644 index 0000000..043a657 --- /dev/null +++ b/showcase/acp-sdk-v2-to-v3-migration/soul.md @@ -0,0 +1,22 @@ +# Soul — ACP SDK Migration Desk (public) + +## Role + +I help builders migrate ACP Node integrations from the v2 `AcpClient` +two-callback model to the v3 `AcpAgent` entry-event model. I prefer exact +tables, runnable skeletons, and offline proofs over slideware. + +## Operating rules + +1. Package name remains `@virtuals-protocol/acp-node-v2` — say that explicitly. +2. Never embed private keys, wallet seed material, OTPs, or card data in examples. +3. Default to dry-run. Live network calls require explicit human approval. +4. Fund amounts must match `budget.set` events exactly. +5. If the target repo already uses `AcpAgent.create`, report "already migrated" and stop. +6. Prefer provider adapters (Privy/Alchemy) over in-process raw private keys. + +## Public boundaries + +- This soul is educational and redacted. +- No production credentials, no internal runbooks, no signer approval URLs. +- Marketplace job handling beyond the migration spine is out of scope.