From f983d1ef7d5b6385f54729f1ccf63885e4d1e3fb Mon Sep 17 00:00:00 2001 From: Soheima M Date: Fri, 24 Jul 2026 17:42:17 +0200 Subject: [PATCH 1/3] updated vibenet skill --- .gitignore | 4 +- README.md | 3 +- package-lock.json | 10 + skills/vibenet/SKILL.md | 106 ++++++++++ skills/vibenet/references/eip8130-accounts.md | 182 ++++++++++++++++++ .../vibenet/references/payer-sponsorship.md | 69 +++++++ .../references/session-keys-and-policies.md | 95 +++++++++ 7 files changed, 467 insertions(+), 2 deletions(-) create mode 100644 package-lock.json create mode 100644 skills/vibenet/SKILL.md create mode 100644 skills/vibenet/references/eip8130-accounts.md create mode 100644 skills/vibenet/references/payer-sponsorship.md create mode 100644 skills/vibenet/references/session-keys-and-policies.md diff --git a/.gitignore b/.gitignore index 2ec9fd9..b9236bb 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,6 @@ vite.config.js.timestamp-* vite.config.ts.timestamp-* .DS_Store -.DS_Store \ No newline at end of file +.DS_Store +# Skill eval workspaces (local scratch, not shipped) +skills/*-workspace/ diff --git a/README.md b/README.md index 3d9cfba..103bd4e 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,13 @@ ## Recommended Skills -Two consolidated skills that cover the most common use cases. Each uses progressive reference loading — the skill loads a single entry point and pulls in detailed references only when needed. +Consolidated skills that cover the most common use cases. Each uses progressive reference loading — the skill loads a single entry point and pulls in detailed references only when needed. | Skill | Install | Description | | ----- | ------- | ----------- | | [build-on-base](./skills/build-on-base/SKILL.md) | `npx skills add base/skills --skill build-on-base` | Complete Base development playbook: network, contracts, wallet auth, payments, attribution, and migrations. Consolidates all individual skills into one. | | [base-mcp](./skills/base-mcp/SKILL.md) | `npx skills add base/skills --skill base-mcp` | Base MCP server — gives your AI assistant a wallet via mcp.base.org. Sending, swapping, signing, batched calls, balances, and partner plugins for lending, swaps, and more. | +| [vibenet](./skills/vibenet/SKILL.md) | `npx skills add base/skills --skill vibenet` | Build on vibenet, Base's devnet for native account abstraction (EIP-8130) with viem: smart accounts, batched calls, session keys and policies, and ERC-8168 payer gas sponsorship. | ## Installation diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6d32e89 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "skills", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "skills" + } + } +} diff --git a/skills/vibenet/SKILL.md b/skills/vibenet/SKILL.md new file mode 100644 index 0000000..f0ea5d8 --- /dev/null +++ b/skills/vibenet/SKILL.md @@ -0,0 +1,106 @@ +--- +name: vibenet +description: >- + Build on vibenet — Base's devnet for native account abstraction (EIP-8130) + and payer gas sponsorship (ERC-8168) using the viem experimental module. Use + whenever the user mentions vibenet, EIP-8130, ERC-8168, 8130 accounts, native + account abstraction, session keys, actors, policies, payers, or gas + sponsorship on Base — or is writing code that creates or operates 8130 smart + accounts, authorizes session-key actors, sends batched calls, sponsors gas + with a payer, or wires a frontend/script against the vibenet devnet or Base + Sepolia. +--- + +# Vibenet + +Vibenet is Base's devnet for **EIP-8130 native account abstraction**: account +abstraction in the protocol itself. Accounts are portable across EVM chains, +support multiple signer types (secp256k1, P-256, WebAuthn), key rotation +without changing address, scoped session-key actors, on-chain policies, and +native **ERC-8168** gas sponsorship. The tooling lives in viem's +`experimental/eip8130` module (fork branch — not yet in npm `viem`). + +## Network + +| Endpoint | Value | +|----------|-------| +| Chain ID | `84538453` | +| Public execution RPC (Node/scripts) | `https://rpc.vibes.base.org` — 8130-capable (`AA_TX_TYPE` / `0x79`) | +| Browser RPC proxy (CORS-safe) | `https://vibes.base.org/api/vibenet/account/rpc` — passes through all `eth_*`, including `0x79` broadcasts and receipt polling | +| Hosted payer (ERC-8168) | `https://vibes.base.org/api/vibenet/account/payer` | +| Faucet | `POST https://vibes.base.org/api/vibenet/faucet/drip` with `{ "address": "0x…" }` | +| Base Sepolia (also 8130-enabled) | `https://sepolia.base.org`, chain id `84532` | + +Install viem from the fork branch, then import from +`viem/experimental/eip8130` (and `viem/experimental/eip8168` for payers): + +```bash +bun add "viem@github:chunter-cb/viem#feat/eip-8130" +``` + +**npm cannot install this branch directly from git** — the fork's workspace +uses pnpm's `catalog:` protocol, so `npm install "viem@github:…"` fails, and +even the bun git install yields an unbuilt monorepo. The reliable path for +npm projects is clone → build → depend on the built package (which lives in +the fork's `src/` directory): + +```bash +git clone -b feat/eip-8130 https://github.com/chunter-cb/viem viem-fork +cd viem-fork && npx pnpm install --ignore-scripts && npx pnpm run build +# then in your app: npm install "viem@file:../viem-fork/src" +``` + +In TypeScript projects, set `"target": "ES2020"` (or later) in +`tsconfig.json` — the 8130 module uses BigInt literals, which the common +ES2017 default rejects at build time. + +## Safety Guardrails + +- **Never commit private keys** — generate throwaway keys for devnet scripts, + read real ones from env vars. +- **`key.k1(...)` builds an actor identity, not a signer** — passing it (or a + raw private-key hex) as `signer` fails with an opaque `pad()` TypeError. Use + `privateKeyToAccount(pk)`. +- **Verify config changes by on-chain read-back** (`isActor8130` / + `getConfigSequence8130`), never by receipt logs or `status: success` — a + skipped authorize is silent. +- **Read the live config sequence right before signing** — a hardcoded + sequence causes silent no-ops. + +## Task Routing + +Read the reference for your task: + +| Task | When to Use | Reference | +|------|-------------|-----------| +| **Accounts & transactions** | Create an 8130 smart account, send batched calls, attribution metadata, gas estimation, reading account state, locking, gotchas | [references/eip8130-accounts.md](references/eip8130-accounts.md) | +| **Session keys & policies** | Authorize/revoke actors, scopes, SessionPolicy spend limits, config sequences, verifying "silent" changes | [references/session-keys-and-policies.md](references/session-keys-and-policies.md) | +| **Gas sponsorship** | Sponsor gas with a payer (ERC-8168), gasless onboarding, `send` vs `sign` modes | [references/payer-sponsorship.md](references/payer-sponsorship.md) | + +## Operating Procedure + +1. **Classify the task** using the table above and read the relevant reference + before implementing. +2. **Pick the right RPC**: `rpc.vibes.base.org` from Node/scripts; the + `account/rpc` browser proxy from web UIs (same chain, CORS-safe). +3. **Implement** with explicit chain id, the fork-branch install, and read-back + verification for any account-config change. +4. **Deliver** runnable code, install commands, and any manual steps (env + vars, faucet funding). + +## For Edge Cases and Latest API Changes + +- **EIP-8130 spec**: [eip.tools/eip/8130](https://eip.tools/eip/8130) + (payer standard: [eip.tools/eip/8168](https://eip.tools/eip/8168)) +- **viem fork**: `github.com/chunter-cb/viem`, branch `feat/eip-8130` + (API surface: `src/experimental/eip8130/index.ts`; docs: + `site/pages/experimental/eip8130`) +- **Deep guide (chaptered)**: `github.com/chunter-cb/eip-8130-web` (`/guide/*`) +- **Session-key walkthrough**: + [gist.github.com/chunter-cb/bf70c53a5ab6d8361ce7f4215b776114](https://gist.github.com/chunter-cb/bf70c53a5ab6d8361ce7f4215b776114) + +## Installation + +```bash +npx skills add base/skills --skill vibenet +``` diff --git a/skills/vibenet/references/eip8130-accounts.md b/skills/vibenet/references/eip8130-accounts.md new file mode 100644 index 0000000..6a90325 --- /dev/null +++ b/skills/vibenet/references/eip8130-accounts.md @@ -0,0 +1,182 @@ +# EIP-8130 accounts and transactions (viem) + +Creating 8130 smart accounts and sending batched calls on vibenet / Base +Sepolia with viem's `experimental/eip8130` module. For network endpoints and +install, see the [skill root](../SKILL.md). + +## Core concepts + +- **Account** — a viem-style account object with `.address` (deterministic, + CREATE2-derived), `.create()` / `.createChange` (first-tx deploy change), and + `signTransaction`. Create via `newSmartAccount8130` / `to8130Account` / + `toEoa8130Account`. +- **Signer** — a signing object that can produce `sender_auth`. For K1, use + `privateKeyToAccount(pk)` (a viem `LocalAccount`). For P-256 / WebAuthn use + `toP256Signer` / `toWebAuthnSigner`. +- **Actor** — an on-chain identity (`{ actorId, authenticator }`), built with + `key.k1(address)` / `key.p256(...)` / `key.webAuthn(...)`. Used for + `initialActors`, `authorizeActor`, `revokeActor` — **not** as the `signer` + passed to `newSmartAccount8130`. +- **Scope** (`actorScope`) — `scopeUnrestricted` (0x00) is admin. Bits: + `sender` `policy` `nonce` `selfPayer` `sponsorPayer`. A policy-bearing actor + must be restricted (non-zero scope), or `authorizeActor` throws. +- **Nonce mode** — admin (`0x00`) or an actor with the `nonce` bit + (`SCOPE_NONCE`) may use **ordered** (sequenced, expiry-free) *or* nonce-free + (expiring) nonces; sends default to ordered. Only a restricted actor + **without** `SCOPE_NONCE` is confined to nonce-free. +- **Policy** — on-chain rules (e.g. `SessionPolicy`: per-token spend limits + + call scopes). See + [session-keys-and-policies.md](session-keys-and-policies.md). +- **Calls** — a batch of `{ to, value?, data? }` executed atomically, with + optional signed top-level `metadata` (set via `dataSuffix` on send). +- **Payer (ERC-8168)** — a service that co-signs `payer_auth` to pay gas. See + [payer-sponsorship.md](payer-sponsorship.md). + +## Minimal end-to-end: create a smart account and send a batch + +```ts +import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; +import { + createPublicClient, http, parseEther, toHex, + newSmartAccount8130, sendCalls8130, estimateGas8130, encodeWalletCalls, + waitForTransactionReceipt8130, allPhasesSucceeded, +} from "viem/experimental/eip8130"; + +const chainId = 84538453; // vibenet (Base Sepolia = 84532) +const RPC_URL = "https://rpc.vibes.base.org"; // 8130-capable public RPC +const chain = { + id: chainId, + name: "vibenet", + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: [RPC_URL] } }, +}; +const client = createPublicClient({ chain, transport: http(RPC_URL) }); +// Browser apps: use https://vibes.base.org/api/vibenet/account/rpc instead +// (same chain, CORS-safe proxy). + +// 1) Signer = LocalAccount (NOT key.k1 — that builds an actor identity). +const signer = privateKeyToAccount(generatePrivateKey()); +// P-256: toP256Signer({ privateKey }) · WebAuthn: toWebAuthnSigner(...) + +// 2) Deterministic account — address exists before any tx. The owner is an +// admin actor (scope 0x00), so sends default to ORDERED (sequenced) nonces, +// which are expiry-free. (Nonce-free/expiring mode is opt-in via +// `nonceKey: nonceKeyMax` — see Gotchas for its current known bug.) +// key.k1(signer.address) is what newSmartAccount8130 uses internally for +// the primary actor — you only call key.* when authorizing extra actors. +const account = newSmartAccount8130({ signer }); // synchronous — no await +// (The fork's TS types may require casting a K1 LocalAccount when passing +// it as `signer`.) + +// 3) Fund account.address (faucet), then estimate + send. The drip responds +// with { tx_hash, amount_wei, to } and grants 0.1 ETH, usually landing in +// ~2s — but treat the shape as unstable: confirm funding by polling +// eth_getBalance until non-zero (allow ~60s and handle rate limits). +await fetch("https://vibes.base.org/api/vibenet/faucet/drip", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ address: account.address }), +}); + +const calls = [{ to: "0x…recipient", value: parseEther("0.001") }]; +const wire = encodeWalletCalls({ account: account.address, calls: [calls] }); +const gas = await estimateGas8130(client, { + sender: account.address, + accountChanges: [account.createChange], + calls: wire, +}); + +const hash = await sendCalls8130(client, { + account, + accountChanges: [account.createChange], // omit on subsequent txs + calls, + dataSuffix: toHex("invoice #4242"), // maps to signed metadata + gas: (gas * 120n) / 100n, +}); + +// 4) Wait and check every phase succeeded. +const receipt = await waitForTransactionReceipt8130(client, { hash }); +if (!allPhasesSucceeded(receipt)) throw new Error("a phase reverted"); +``` + +An 8130 receipt executes in **phases** (one per call batch). `phaseStatuses` +on the receipt reports per-phase success for CALL phases only — +account-change application is not covered (see +[session-keys-and-policies.md](session-keys-and-policies.md) for why config +changes need a read-back instead). UIs that display per-phase results should +render `phaseStatuses` and treat `allPhasesSucceeded(receipt)` as the overall +verdict; don't rely on `receipt.status` alone. For the exact field shape, +check `src/experimental/eip8130` on the fork branch — it is experimental and +may shift. + +## Canonical deployment + +Canonical contract addresses per chain come from `getEip8130Deployment(chainId)` +(or `canonicalEip8130Deployment`): `accountConfiguration`, `accounts.*`, +`authenticators.*`, `policies.{manager,sessionPolicy}`. + +The current canonical deployment uses AccountConfiguration +`0x53648Cf00356fbAA1F2B531715c6B64AaBDE1555`, DefaultAccount +`0x58da469ef71Dd4B092B010CdA37DE124C926EebD`, PolicyManager +`0x6e9E627770C1c90371A2E4CB9474A7Af577a4306`, and SessionPolicy +`0x58ef2d572a1bC528f0B9121d686B2618809604Dc`. + +## Account creation modes + +- `newSmartAccount8130({ signer })` — new CREATE2 smart account (most common). + `signer` must be a signing account (`privateKeyToAccount(pk)`, `toP256Signer`, + or `toWebAuthnSigner`) — **not** `key.k1(...)`. +- `to8130Account({ signer, userSalt, code, initialActors, authenticator, accountConfigAddress })` + — full control over salt / initial actors. +- `to8130Account({ signer, address, authenticator })` — a configured (non-default) + actor on an existing/delegated account. +- `toEoa8130Account(signer)` — an EOA acting as its own default K1 actor + (raw 65-byte sig, EIP-7702 delegation via `account.delegate(impl)`). + +## Reading account state (viem actions) + +All take `(client, params)` and read the on-chain `AccountConfiguration`: +`getActorConfig8130`, `isActor8130`, `getPolicy8130`, `getSessionSpend8130`, +`getLockStatus8130` / `isLocked8130`, `getConfigSequence8130`, +`getTransactionCount8130`, `getTransaction8130`, `getTransactionReceipt8130`, +`waitForTransactionReceipt8130`. + +## Locking + +`lockCall` / `initiateUnlockCall` build `applySignedLockChanges` calls; hash the +change to sign with `hashLockChange8130` (`lockChangeTypehash`). `lockCall` +requires `unlockDelay >= 1`. + +## Gotchas + +- **`key.k1` ≠ signer.** `key.k1(address)` builds an actor id for authorize / + `initialActors`. Passing it (or a raw private-key hex) as `signer` to + `newSmartAccount8130` fails with an opaque `pad()` TypeError — use + `privateKeyToAccount(pk)`. +- Fund `account.address` **before** a self-paid first tx — the deploy+batch pays + gas from the account (unless a payer sponsors it). +- Only the **first** tx includes `account.createChange`; later txs omit it. +- After a successful create, `eth_getCode` on the account returns an EIP-1167 + minimal proxy delegating to the canonical DefaultAccount — but the read + lags ~1 block behind the receipt, so an immediate getCode can return `0x` + on a tx that succeeded. Poll before concluding the deploy failed (same lag + as config-change read-backs). +- Attribution goes in `dataSuffix` on `sendCalls8130` (maps to signed `metadata`). +- **Always pass explicit `gas`** to `sendCalls8130` (estimate with + `estimateGas8130`, add ~20% headroom, as in the example above). Omitting it + gets the tx rejected with the misleading error + `transaction type not supported` — live-confirmed, and easy to misread as + an RPC capability problem. +- `rpc.vibes.base.org` **is** fine for `0x79` broadcasts (Node/scripts). The + `account/rpc` URL is only required in the browser (CORS proxy to the same + chain). +- **Nonce-free (expiring) sends have a known bug (planned fix July 22, 2026):** + the node can intermittently reject a valid `0x79` with a misleading + `transaction type not supported` (the short `expiry` lapses before + validation). Until then, prefer **ordered (expiry-free)** sends — the default + for admin and `SCOPE_NONCE` actors — which are unaffected. +- Contract addresses are bytecode-derived — the deployed system must be compiled + with the same solc as `canonicalEip8130Deployment`, or account creation fails + with "create address mismatch". +- Chains must be 8130-aware: use `register8130Chains` / `is8130Enabled` when + operating outside the built-in `eip8130ChainIds`. diff --git a/skills/vibenet/references/payer-sponsorship.md b/skills/vibenet/references/payer-sponsorship.md new file mode 100644 index 0000000..d552891 --- /dev/null +++ b/skills/vibenet/references/payer-sponsorship.md @@ -0,0 +1,69 @@ +# Payer gas sponsorship (ERC-8168) + +Sponsoring gas for 8130 accounts with a payer service — the path to gasless +onboarding (account creation + first transaction with zero user ETH). For +account creation and core concepts, read +[eip8130-accounts.md](eip8130-accounts.md) first. + +A **payer** is a service that co-signs `payer_auth` to pay gas (sponsored or +in ERC-20). The hosted vibenet payer lives at +`https://vibes.base.org/api/vibenet/account/payer` and supports **both** +ERC-8168 modes: + +- `mode: "send"` (default) — payer co-signs and submits (`payer_sendTransaction`) +- `mode: "sign"` — payer co-signs only; you broadcast with `eth_sendRawTransaction` + +## Sponsor a first transaction (gasless onboarding) + +```ts +import { createPayerClient, sendSponsoredCalls } from "viem/experimental/eip8168"; +import { waitForTransactionReceipt8130 } from "viem/experimental/eip8130"; + +const payerClient = createPayerClient({ + url: "https://vibes.base.org/api/vibenet/account/payer", +}); + +// Default mode:"send" — payer co-signs and broadcasts. Returns the tx HASH +// directly (SendTransactionReturnType) — do NOT destructure { transactionHash }. +const hash = await sendSponsoredCalls(client, { + account, + payerClient, + accountChanges: [account.createChange], // deploy rides in the first sponsored tx + calls: [{ to: account.address, value: 0n, data: "0x" }], + context: { flow: "transact" }, // budgets free grants per (sender, flow) +}); +const receipt = await waitForTransactionReceipt8130(client, { hash }); +``` + +No faucet call and no user ETH is needed anywhere in this flow — the payer's +`payer_auth` makes the protocol debit gas from the payer, not the sender. + +For self-submit (e.g. custom RPC / retry control), pass `mode: "sign"` — the +call then resolves with the signed raw transaction +(`SignTransactionReturnType`), which you broadcast yourself with +`eth_sendRawTransaction`. Both modes return a plain hex string, not an +object: the tx hash in `send` mode, the signed tx in `sign` mode. + +## Wire protocol (JSON-RPC over the payer URL) + +Under the hood `sendSponsoredCalls` runs the ERC-8168 flow: fetch terms with +`payer_getTerms`, build and sign `sender_auth` with `payer` set and +`payer_auth` empty, then hand off via `payer_sendTransaction` (send mode) or +`payer_signTransaction` (sign mode). `payer_getTerms` + +`payer_sendTransaction` are the required pair every payer implements; +`payer_signTransaction` is optional (advertised in the terms' `methods`). +Rejections come back as JSON-RPC error `-32000` with a +`data: { code, reason }` envelope — branch on the string `code` (e.g. +`BUDGET_EXHAUSTED`, `SENDER_LIMIT_REACHED`, whose context includes a +`validFor` retry hint). Full types: `src/experimental/eip8168/types.ts` on +the fork branch. + +## Notes + +- Subsequent sponsored txs omit `account.createChange` — only the first tx + carries it. +- `context.flow` lets the hosted payer budget free grants per + `(sender, flow)` pair; pick a stable string per product surface + (e.g. `"onboarding"`, `"transact"`). +- Payer standard: https://eip.tools/eip/8168 (viem module: + `src/experimental/eip8168` on the fork branch). diff --git a/skills/vibenet/references/session-keys-and-policies.md b/skills/vibenet/references/session-keys-and-policies.md new file mode 100644 index 0000000..f5c162e --- /dev/null +++ b/skills/vibenet/references/session-keys-and-policies.md @@ -0,0 +1,95 @@ +# Session keys, actors, and policies (EIP-8130) + +Authorizing scoped session-key actors with on-chain policies, and verifying +config changes correctly. For account creation and core concepts, read +[eip8130-accounts.md](eip8130-accounts.md) first. + +## Authorize a policy-gated session actor + +```ts +import { + key, authorizeActor, actorScope, + defineSessionPolicy, encodeSessionPolicyConfig, getEip8130Deployment, +} from "viem/experimental/eip8130"; + +const dep = getEip8130Deployment(chainId); +const policyConfig = encodeSessionPolicyConfig({ + tokenLimits: [{ token: usdv, limit: 100_000_000n, period: 604_800n }], // 100 USDV / week + callScopes: [{ target: usdv, selectorRules: [{ selector: "0xa9059cbb" }] }], // transfer only +}); +const session = defineSessionPolicy({ + account: account.address, policy: dep.policies.sessionPolicy, + policyConfig, manager: dep.policies.manager, validUntil: 1_900_000_000n, +}); + +// There is no install step. Every execute carries the full PolicyBinding and +// the manager recomputes its authorized commitment. +const call = session.executeCall({ target, value: 0n, data }); + +// Actor identity for authorize (not a LocalAccount). SCOPE_POLICY is set +// automatically when `policy` is present. +const sessionActor = key.p256({ x: "0x…", y: "0x…" }); +const change = authorizeActor(sessionActor, { + scope: actorScope.sender, + expiry: 1_900_000_000n, + policy: session.actorPolicy, +}); +// Include `change` in accountChanges of a sendCalls8130 signed by an admin actor. +``` + +A policy actor must have a non-zero scope; admin (scope 0) + policy is +rejected by `authorizeActor`. + +## Verifying a config change (and why it can look "silent") + +Account changes (authorize/revoke) do **not** surface success the way calls do. +Check them by **reading back on-chain state**, not by the receipt: + +- **`receipt.logs` is empty even on a successful authorize.** `ActorAuthorized` + is not surfaced as a normal EVM log here — "no events" is NOT a failure. Do + not gate success on scanning logs. +- **`allPhasesSucceeded` / `phaseStatuses` only cover CALL phases**, not the + account-change application. A skipped authorize marks no failed phase, throws + nothing, and the tx still reports `status: success`. +- **The only reliable success check is a read-back:** `isActor8130`, + `getActorConfig8130`, or a bumped `getConfigSequence8130`. +- **Reads lag ~1 block (~2s)** behind the receipt — poll the read-back. + +## Sequence correctness + +The usual cause of a silent no-op: the digest binds +`(account, chainId, sequence)`. Always read the **live** counter for the channel +right before signing — never hardcode it. Session-key auth uses the **local** +channel (`chainId = chain.id`); owner changes use the **multichain** channel +(`chainId = 0`). The first-authorize sequence depends on the deploy path: + +| Deploy path | First local sequence | +|---|---| +| Smart wallet (`createAccount`) | `1` (create bumps local 0→1) | +| Explicit `importAccount` | `1` | +| Bare 7702 delegation (`account.delegate`) | `0` (delegation does not initialize state) | + +```ts +import { getConfigSequence8130, isActor8130 } from "viem/experimental/eip8130"; + +const { local } = await getConfigSequence8130(client, { + accountConfiguration: dep.accountConfiguration, + account: account.address, +}); // read live — do not assume 0 or 1 +const change = await account.change([authorizeActor(/* … */)], { + chainId, // local channel for session keys + sequence: Number(local), +}); +// … send the tx, then verify by read-back (polled for ~1 block of lag): +const bound = await isActor8130(client, { + account: account.address, actorId, accountConfiguration: dep.accountConfiguration, +}); +if (!bound) throw new Error("authorize was skipped — check sequence/channel"); +``` + +## Reference + +- Session-key end-to-end walkthrough (create → register PolicyManager + + session key → drive a call through the session key, with read-back + verification at each step): + https://gist.github.com/chunter-cb/bf70c53a5ab6d8361ce7f4215b776114 From a9d93018b2c9bb630c6a1fe382c896f3ec1d1ee7 Mon Sep 17 00:00:00 2001 From: Soheima M Date: Mon, 27 Jul 2026 07:40:00 -0400 Subject: [PATCH 2/3] vibenet: correct endpoints, add account lifecycle, fix verified-wrong guidance Verified live against the devnet by building a Next.js app end to end. Endpoints: API host moved vibes.base.org -> api.vibes.base.org (the bare host 302s to an HTML page, which viem reports as "Unrecognized token '<'"). Add faucet/status, chain-health and explorer. rpc.vibes.base.org is CORS-enabled, so the browser proxy is optional rather than required. Lifecycle: new section covering counterfactual -> deployed. There is no deploy step; the first transaction creates the account. Sponsored is the shortest path (no faucet). Read deployment from eth_getCode, never optimistic local state. Corrections, all reproduced live: - sendSponsoredCalls resolves with { transactionHash }; the previous text said the opposite and the declared return type is wrong. - A bad config sequence is rejected at broadcast, not silently applied. The real trap is the inverse: a change can apply on a tx reporting status 0x0. - The end-to-end example imported core viem helpers from the 8130 module, which does not export them, so it could not compile. - npm file: installs need --install-links or Turbopack cannot resolve viem. New gotchas: payer validation lags ~1 block after deploy ("actor is not bound"); "no backend is currently healthy" means the devnet is halted; a value-bearing call to a never-funded address reverts. --- skills/vibenet/SKILL.md | 72 ++++++++++---- skills/vibenet/references/eip8130-accounts.md | 95 +++++++++++++++++-- .../vibenet/references/payer-sponsorship.md | 37 ++++++-- .../references/session-keys-and-policies.md | 22 ++++- 4 files changed, 182 insertions(+), 44 deletions(-) diff --git a/skills/vibenet/SKILL.md b/skills/vibenet/SKILL.md index f0ea5d8..a87509d 100644 --- a/skills/vibenet/SKILL.md +++ b/skills/vibenet/SKILL.md @@ -25,34 +25,63 @@ native **ERC-8168** gas sponsorship. The tooling lives in viem's | Endpoint | Value | |----------|-------| | Chain ID | `84538453` | -| Public execution RPC (Node/scripts) | `https://rpc.vibes.base.org` — 8130-capable (`AA_TX_TYPE` / `0x79`) | -| Browser RPC proxy (CORS-safe) | `https://vibes.base.org/api/vibenet/account/rpc` — passes through all `eth_*`, including `0x79` broadcasts and receipt polling | -| Hosted payer (ERC-8168) | `https://vibes.base.org/api/vibenet/account/payer` | -| Faucet | `POST https://vibes.base.org/api/vibenet/faucet/drip` with `{ "address": "0x…" }` | +| Public execution RPC | `https://rpc.vibes.base.org` — 8130-capable (`AA_TX_TYPE` / `0x79`), serves `access-control-allow-origin: *` | +| Browser RPC proxy | `https://api.vibes.base.org/api/vibenet/account/rpc` — passes through all `eth_*`, including `0x79` broadcasts and receipt polling | +| Hosted payer (ERC-8168) | `https://api.vibes.base.org/api/vibenet/account/payer` | +| Faucet | `POST https://api.vibes.base.org/api/vibenet/faucet/drip` with `{ "address": "0x…" }` | +| Faucet status | `GET https://api.vibes.base.org/api/vibenet/faucet/status` — drip size, cooldowns, USDV/NFV token addresses | +| Chain health | `GET https://api.vibes.base.org/api/vibenet/chain-health` — `{ healthy, head, headAgeSecs, … }` | +| Landing page / explorer | `https://chain.base.org/vibenet`, `https://chain.base.org/vibenet/explorer` | | Base Sepolia (also 8130-enabled) | `https://sepolia.base.org`, chain id `84532` | -Install viem from the fork branch, then import from -`viem/experimental/eip8130` (and `viem/experimental/eip8168` for payers): +**The API host is `api.vibes.base.org`, not `vibes.base.org`.** The bare host +302-redirects to the `chain.base.org/vibenet` HTML page; viem's HTTP transport +then tries to parse that as JSON and throws `Unrecognized token '<'`, which +reads like a code bug rather than a wrong URL. -```bash -bun add "viem@github:chunter-cb/viem#feat/eip-8130" -``` +All `api.vibes.base.org` endpoints (RPC proxy, payer, faucet) send permissive +CORS headers, and so does `rpc.vibes.base.org` — so browser apps can talk to +either. Prefer `rpc.vibes.base.org` for execution and reserve the `account/rpc` +proxy for when you specifically want the hosted path. -**npm cannot install this branch directly from git** — the fork's workspace -uses pnpm's `catalog:` protocol, so `npm install "viem@github:…"` fails, and -even the bun git install yields an unbuilt monorepo. The reliable path for -npm projects is clone → build → depend on the built package (which lives in -the fork's `src/` directory): +The tooling is **not published to npm** and cannot be installed from git +directly: the fork's workspace uses pnpm's `catalog:` protocol, so +`npm install "viem@github:…"` fails outright, and `bun add "viem@github:…"` +"succeeds" but leaves you an unbuilt monorepo with no `exports` field. Clone, +build, then depend on the built package (which lives in the fork's `src/`): ```bash git clone -b feat/eip-8130 https://github.com/chunter-cb/viem viem-fork cd viem-fork && npx pnpm install --ignore-scripts && npx pnpm run build -# then in your app: npm install "viem@file:../viem-fork/src" + +# then in your app — --install-links is required: +npm install --install-links "viem@file:../viem-fork/src" ``` -In TypeScript projects, set `"target": "ES2020"` (or later) in -`tsconfig.json` — the 8130 module uses BigInt literals, which the common -ES2017 default rejects at build time. +Then import from `viem/experimental/eip8130` (and `viem/experimental/eip8168` +for payers). Core helpers like `createPublicClient` / `parseEther` come from +plain `viem` — the 8130 module does not re-export them. + +**Use `--install-links`.** Without it npm symlinks `node_modules/viem` to a path +outside the project root, and Turbopack/Next.js then fails with +`Module not found: Can't resolve 'viem'` for a package that is plainly there +(`tsc` resolves it fine, which makes it look like a bundler bug). + +If your TypeScript build rejects the module's BigInt literals, set +`"target": "ES2020"` or later in `tsconfig.json`. Next.js 16's generated config +already works as-is, since its `lib` includes `esnext`. + +## Accounts Have No Deploy Step + +Creating an account derives a CREATE2 address locally — synchronous, zero RPC, +`eth_getCode` still `0x`. It becomes real as a **side effect of its first +transaction**, which carries `account.createChange` alongside your actual calls. +There is nothing else to call. The shortest path from nothing to a deployed +account is a *sponsored* first tx (no faucet, no funding); the self-paid route +needs the address funded first. Read deployment state from `eth_getCode`, never +from optimistic local state — it decides whether the next tx carries +`createChange`. Full lifecycle: +[references/eip8130-accounts.md](references/eip8130-accounts.md). ## Safety Guardrails @@ -73,7 +102,7 @@ Read the reference for your task: | Task | When to Use | Reference | |------|-------------|-----------| -| **Accounts & transactions** | Create an 8130 smart account, send batched calls, attribution metadata, gas estimation, reading account state, locking, gotchas | [references/eip8130-accounts.md](references/eip8130-accounts.md) | +| **Accounts & transactions** | Create an 8130 smart account, the counterfactual→deployed lifecycle, send batched calls, attribution metadata, gas estimation, reading account state, locking, gotchas | [references/eip8130-accounts.md](references/eip8130-accounts.md) | | **Session keys & policies** | Authorize/revoke actors, scopes, SessionPolicy spend limits, config sequences, verifying "silent" changes | [references/session-keys-and-policies.md](references/session-keys-and-policies.md) | | **Gas sponsorship** | Sponsor gas with a payer (ERC-8168), gasless onboarding, `send` vs `sign` modes | [references/payer-sponsorship.md](references/payer-sponsorship.md) | @@ -81,8 +110,9 @@ Read the reference for your task: 1. **Classify the task** using the table above and read the relevant reference before implementing. -2. **Pick the right RPC**: `rpc.vibes.base.org` from Node/scripts; the - `account/rpc` browser proxy from web UIs (same chain, CORS-safe). +2. **Pick the right RPC**: `rpc.vibes.base.org` works from both Node and the + browser; `api.vibes.base.org/api/vibenet/account/rpc` is the hosted proxy to + the same chain. Never `vibes.base.org` — that host is not an API. 3. **Implement** with explicit chain id, the fork-branch install, and read-back verification for any account-config change. 4. **Deliver** runnable code, install commands, and any manual steps (env diff --git a/skills/vibenet/references/eip8130-accounts.md b/skills/vibenet/references/eip8130-accounts.md index 6a90325..32ed002 100644 --- a/skills/vibenet/references/eip8130-accounts.md +++ b/skills/vibenet/references/eip8130-accounts.md @@ -9,7 +9,8 @@ install, see the [skill root](../SKILL.md). - **Account** — a viem-style account object with `.address` (deterministic, CREATE2-derived), `.create()` / `.createChange` (first-tx deploy change), and `signTransaction`. Create via `newSmartAccount8130` / `to8130Account` / - `toEoa8130Account`. + `toEoa8130Account`. Creating one puts it in a *counterfactual* state — see + [Account lifecycle](#account-lifecycle-there-is-no-deploy-step). - **Signer** — a signing object that can produce `sender_auth`. For K1, use `privateKeyToAccount(pk)` (a viem `LocalAccount`). For P-256 / WebAuthn use `toP256Signer` / `toWebAuthnSigner`. @@ -32,12 +33,62 @@ install, see the [skill root](../SKILL.md). - **Payer (ERC-8168)** — a service that co-signs `payer_auth` to pay gas. See [payer-sponsorship.md](payer-sponsorship.md). +## Account lifecycle: there is no deploy step + +**The single most common point of confusion.** An 8130 account has two states, +and nothing you call moves it between them directly. + +``` +newSmartAccount8130({ signer }) → COUNTERFACTUAL + address derived locally (CREATE2), synchronous, zero RPC calls. + eth_getCode returns 0x. The account does not exist on-chain. + +first transaction (carries account.createChange) → DEPLOYED + eth_getCode returns an EIP-1167 minimal proxy delegating to the + canonical DefaultAccount. +``` + +There is **no `deploy()` and no `create()` transaction to send.** The account is +brought into existence as a side effect of its first transaction, which carries +`account.createChange` alongside whatever calls you actually wanted to make. +Creation costs no extra round-trip: deploy and first batch are one tx. + +That leaves exactly two routes from counterfactual to deployed: + +| Route | Needs funding first? | How | +|---|---|---| +| **Sponsored** (shortest path) | No | `sendSponsoredCalls` with `accountChanges: [account.createChange]`. The payer pays gas, so this works at a zero balance — no faucet, no cooldown. See [payer-sponsorship.md](payer-sponsorship.md). | +| **Self-paid** | Yes | Faucet-fund `account.address`, then `sendCalls8130` with `accountChanges: [account.createChange]`. The account pays its own deploy+batch gas. | + +Reach for the sponsored route when onboarding a user or writing a first +example — it removes the faucet from the critical path entirely. + +### Checking whether an account is deployed + +Read it from the chain; never track it as local/optimistic UI state. It is not +cosmetic — it decides whether the next transaction attaches `createChange`, so a +stale `true` produces a malformed tx and a stale `false` re-sends a create. + +```ts +const code = await client.getCode({ address: account.address }); +const deployed = Boolean(code && code !== "0x"); +``` + +`getCode` lags ~1 block (~2s) behind the receipt, so immediately after a +successful create it can still return `0x` on a transaction whose every phase +succeeded. **Poll it** — don't conclude the deploy failed from a single read. +The same lag hits the payer: sponsoring right after a self-paid deploy fails +with `actor is not bound` until the config propagates. + +Once deployed, **omit `accountChanges` on every subsequent transaction.** + ## Minimal end-to-end: create a smart account and send a batch ```ts +// Core helpers come from `viem` itself — the 8130 module does NOT re-export them. +import { createPublicClient, http, parseEther, toHex } from "viem"; import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; import { - createPublicClient, http, parseEther, toHex, newSmartAccount8130, sendCalls8130, estimateGas8130, encodeWalletCalls, waitForTransactionReceipt8130, allPhasesSucceeded, } from "viem/experimental/eip8130"; @@ -51,8 +102,9 @@ const chain = { rpcUrls: { default: { http: [RPC_URL] } }, }; const client = createPublicClient({ chain, transport: http(RPC_URL) }); -// Browser apps: use https://vibes.base.org/api/vibenet/account/rpc instead -// (same chain, CORS-safe proxy). +// This RPC is CORS-enabled, so it works from the browser too. The hosted +// proxy at https://api.vibes.base.org/api/vibenet/account/rpc is an +// alternative path to the same chain. // 1) Signer = LocalAccount (NOT key.k1 — that builds an actor identity). const signer = privateKeyToAccount(generatePrivateKey()); @@ -71,8 +123,10 @@ const account = newSmartAccount8130({ signer }); // synchronous — no await // 3) Fund account.address (faucet), then estimate + send. The drip responds // with { tx_hash, amount_wei, to } and grants 0.1 ETH, usually landing in // ~2s — but treat the shape as unstable: confirm funding by polling -// eth_getBalance until non-zero (allow ~60s and handle rate limits). -await fetch("https://vibes.base.org/api/vibenet/faucet/drip", { +// eth_getBalance until non-zero (allow ~60s). Cooldown is ~10s per address +// and per IP; GET /api/vibenet/faucet/status returns the live values. +// The endpoint is CORS-enabled, so a browser can call it directly. +await fetch("https://api.vibes.base.org/api/vibenet/faucet/drip", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ address: account.address }), @@ -161,15 +215,38 @@ requires `unlockDelay >= 1`. lags ~1 block behind the receipt, so an immediate getCode can return `0x` on a tx that succeeded. Poll before concluding the deploy failed (same lag as config-change read-backs). +- **A value-bearing call to a brand-new address reverts.** Sending `value` to an + address that has never held a balance comes back `status: 0x0` with + `phaseStatuses: ["0x0"]` (reproduced 4/4 on vibenet). The same call to an + address that already has a balance succeeds and moves the exact amount, and a + zero-value call to a fresh address is fine — so it is specific to funding an + untouched address, not to `value` in general. Cause unconfirmed; if you are + onboarding a fresh recipient, fund it from the faucet first or expect the + revert. - Attribution goes in `dataSuffix` on `sendCalls8130` (maps to signed `metadata`). - **Always pass explicit `gas`** to `sendCalls8130` (estimate with `estimateGas8130`, add ~20% headroom, as in the example above). Omitting it gets the tx rejected with the misleading error `transaction type not supported` — live-confirmed, and easy to misread as an RPC capability problem. -- `rpc.vibes.base.org` **is** fine for `0x79` broadcasts (Node/scripts). The - `account/rpc` URL is only required in the browser (CORS proxy to the same - chain). +- `rpc.vibes.base.org` **is** fine for `0x79` broadcasts, from Node and from the + browser (it serves `access-control-allow-origin: *`). The + `api.vibes.base.org/api/vibenet/account/rpc` proxy is an alternative route to + the same chain, not a requirement. +- **The API host is `api.vibes.base.org`.** A bare `vibes.base.org/api/…` URL + 302s to an HTML page, and viem surfaces that as + `JSON Parse error: Unrecognized token '<'` — easy to misread as a bug in your + code. +- **`no backend is currently healthy to serve traffic` means the devnet is + halted, not that your code or RPC URL is wrong.** vibenet is an ephemeral + devnet and does stall. Reads (`eth_chainId`, `eth_blockNumber`) keep answering + from the last block while `eth_sendRawTransaction` fails, which makes it look + like a transaction-shaped problem. Confirm with + `GET https://api.vibes.base.org/api/vibenet/chain-health`, which reports + `{ healthy, reason, detail, head, headAgeSecs, stuckSecs }` — a halted chain + returns `healthy: false, reason: "halted"` with a rising `headAgeSecs`. Wait + for it to recover; there is nothing to fix client-side. Worth surfacing in any + UI or script that sends transactions. - **Nonce-free (expiring) sends have a known bug (planned fix July 22, 2026):** the node can intermittently reject a valid `0x79` with a misleading `transaction type not supported` (the short `expiry` lapses before diff --git a/skills/vibenet/references/payer-sponsorship.md b/skills/vibenet/references/payer-sponsorship.md index d552891..299078b 100644 --- a/skills/vibenet/references/payer-sponsorship.md +++ b/skills/vibenet/references/payer-sponsorship.md @@ -7,7 +7,7 @@ account creation and core concepts, read A **payer** is a service that co-signs `payer_auth` to pay gas (sponsored or in ERC-20). The hosted vibenet payer lives at -`https://vibes.base.org/api/vibenet/account/payer` and supports **both** +`https://api.vibes.base.org/api/vibenet/account/payer` and supports **both** ERC-8168 modes: - `mode: "send"` (default) — payer co-signs and submits (`payer_sendTransaction`) @@ -16,22 +16,24 @@ ERC-8168 modes: ## Sponsor a first transaction (gasless onboarding) ```ts +import type { Hex } from "viem"; import { createPayerClient, sendSponsoredCalls } from "viem/experimental/eip8168"; import { waitForTransactionReceipt8130 } from "viem/experimental/eip8130"; const payerClient = createPayerClient({ - url: "https://vibes.base.org/api/vibenet/account/payer", + url: "https://api.vibes.base.org/api/vibenet/account/payer", }); -// Default mode:"send" — payer co-signs and broadcasts. Returns the tx HASH -// directly (SendTransactionReturnType) — do NOT destructure { transactionHash }. -const hash = await sendSponsoredCalls(client, { +// Default mode:"send" — payer co-signs and broadcasts, resolving with an +// OBJECT: `{ transactionHash }`. Destructure it. (The declared return type says +// otherwise — see the note below — so TypeScript needs a cast.) +const { transactionHash: hash } = (await sendSponsoredCalls(client, { account, payerClient, accountChanges: [account.createChange], // deploy rides in the first sponsored tx calls: [{ to: account.address, value: 0n, data: "0x" }], context: { flow: "transact" }, // budgets free grants per (sender, flow) -}); +})) as unknown as { transactionHash: Hex }; const receipt = await waitForTransactionReceipt8130(client, { hash }); ``` @@ -39,10 +41,16 @@ No faucet call and no user ETH is needed anywhere in this flow — the payer's `payer_auth` makes the protocol debit gas from the payer, not the sender. For self-submit (e.g. custom RPC / retry control), pass `mode: "sign"` — the -call then resolves with the signed raw transaction -(`SignTransactionReturnType`), which you broadcast yourself with -`eth_sendRawTransaction`. Both modes return a plain hex string, not an -object: the tx hash in `send` mode, the signed tx in `sign` mode. +call then resolves with the signed raw transaction, which you broadcast yourself +with `eth_sendRawTransaction`. + +**The declared return type is wrong.** `SendSponsoredCallsReturnType` is typed +as a union of hex strings (`SendTransactionReturnType | SignTransactionReturnType`), +but `mode: "send"` resolves with `{ transactionHash }` at runtime — +live-confirmed against the hosted payer. Passing the raw result into +`waitForTransactionReceipt8130` fails at the RPC layer with +`invalid type: map, expected 32 bytes`, which points nowhere near the cause. The +union also isn't assignable to `Hex`, so narrowing needs `as unknown as`. ## Wire protocol (JSON-RPC over the payer URL) @@ -62,6 +70,15 @@ the fork branch. - Subsequent sponsored txs omit `account.createChange` — only the first tx carries it. +- **Don't sponsor immediately after a self-paid deploy.** Account config + propagates ~1 block behind the receipt (the same lag as `eth_getCode` and + config read-backs), and the payer validates against the lagging state — so a + sponsored tx sent right after a successful deploy is rejected with + `EIP-8130 validation failed: actor is not bound`, surfaced as viem's + `InvalidInputRpcError: Missing or invalid parameters`. Neither message points + at timing. Retry on `actor is not bound` (a few seconds is enough) or wait for + the account's code read-back before sponsoring. Live-confirmed: the same call + fails immediately after deploy and succeeds ~6s later. - `context.flow` lets the hosted payer budget free grants per `(sender, flow)` pair; pick a stable string per product surface (e.g. `"onboarding"`, `"transact"`). diff --git a/skills/vibenet/references/session-keys-and-policies.md b/skills/vibenet/references/session-keys-and-policies.md index f5c162e..a29a7b2 100644 --- a/skills/vibenet/references/session-keys-and-policies.md +++ b/skills/vibenet/references/session-keys-and-policies.md @@ -49,10 +49,24 @@ Check them by **reading back on-chain state**, not by the receipt: is not surfaced as a normal EVM log here — "no events" is NOT a failure. Do not gate success on scanning logs. - **`allPhasesSucceeded` / `phaseStatuses` only cover CALL phases**, not the - account-change application. A skipped authorize marks no failed phase, throws - nothing, and the tx still reports `status: success`. -- **The only reliable success check is a read-back:** `isActor8130`, - `getActorConfig8130`, or a bumped `getConfigSequence8130`. + account-change application. On a change-only transaction (no calls) + `phaseStatuses` is absent entirely and `allPhasesSucceeded` returns `true` + vacuously — it is reporting on nothing. +- **A wrong sequence is rejected at broadcast, not silently applied.** Signing a + change over a stale *or* future sequence makes `eth_sendRawTransaction` fail + with `EIP-8130 validation failed: config change sequence mismatch` (surfaced + by viem as `InvalidInputRpcError: Missing or invalid parameters`). The tx + never lands, so there is no receipt to inspect — the failure is loud, but the + error text names neither the sequence you used nor the one expected. +- **The real trap is the inverse: a config change can apply on a transaction + that reports failure.** Live-confirmed — a tx carrying an authorize plus a + reverting call came back `status: 0x0` with `phaseStatuses: ["0x0"]`, yet the + actor was bound and the config sequence had bumped. Account changes are not + atomic with the calls they ride along with, in either direction. **Never infer + config state from `receipt.status`.** +- **The only reliable check is a read-back:** `isActor8130`, + `getActorConfig8130`, or a bumped `getConfigSequence8130` — after a failed tx + as much as a successful one. - **Reads lag ~1 block (~2s)** behind the receipt — poll the read-back. ## Sequence correctness From 37df1d9ec09f34caff034ff503976817bec62008 Mon Sep 17 00:00:00 2001 From: Soheima M Date: Mon, 27 Jul 2026 09:27:05 -0400 Subject: [PATCH 3/3] docs: frame vibenet as Base Vibes in README, add example usage Adds vibenet prompt examples alongside the existing ones, plus a worked gasless-onboarding snippet showing the counterfactual-to-deployed flow with an ERC-8168 payer. The snippet was compiled and run live against the devnet (deployed at a zero balance, status 0x1). --- README.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 103bd4e..9a451e4 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Consolidated skills that cover the most common use cases. Each uses progressive | ----- | ------- | ----------- | | [build-on-base](./skills/build-on-base/SKILL.md) | `npx skills add base/skills --skill build-on-base` | Complete Base development playbook: network, contracts, wallet auth, payments, attribution, and migrations. Consolidates all individual skills into one. | | [base-mcp](./skills/base-mcp/SKILL.md) | `npx skills add base/skills --skill base-mcp` | Base MCP server — gives your AI assistant a wallet via mcp.base.org. Sending, swapping, signing, batched calls, balances, and partner plugins for lending, swaps, and more. | -| [vibenet](./skills/vibenet/SKILL.md) | `npx skills add base/skills --skill vibenet` | Build on vibenet, Base's devnet for native account abstraction (EIP-8130) with viem: smart accounts, batched calls, session keys and policies, and ERC-8168 payer gas sponsorship. | +| [vibenet](./skills/vibenet/SKILL.md) | `npx skills add base/skills --skill vibenet` | Build on [vibenet](https://chain.base.org/vibenet), the Base Vibes devnet for native account abstraction (EIP-8130) with viem: smart accounts, batched calls, session keys and policies, and ERC-8168 payer gas sponsorship. | ## Installation @@ -67,6 +67,68 @@ Convert my existing Farcaster miniapp to a standard app on Base Register my trading bot and add builder code attribution to its transactions ``` +```text +Create an EIP-8130 smart account on vibenet and fund it from the faucet +``` + +```text +Deploy a smart account on vibenet with sponsored gas, so the user needs no ETH +``` + +```text +Authorize a session key on my 8130 account with a weekly USDC spend limit +``` + +### Example: gasless onboarding on vibenet + +A worked example of what the `vibenet` skill produces — creating an EIP-8130 +smart account and deploying it with zero user funds, via an ERC-8168 payer: + +```ts +import type { Hex } from "viem"; +import { createPublicClient, http } from "viem"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { + allPhasesSucceeded, + newSmartAccount8130, + waitForTransactionReceipt8130, +} from "viem/experimental/eip8130"; +import { createPayerClient, sendSponsoredCalls } from "viem/experimental/eip8168"; + +const chain = { + id: 84538453, + name: "vibenet", + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: ["https://rpc.vibes.base.org"] } }, +}; +const client = createPublicClient({ chain, transport: http() }); + +// The address is derived locally — synchronous, no RPC, nothing on-chain yet. +// (The fork types `signer` for P-256 keys, so a K1 account needs a cast.) +const signer = privateKeyToAccount(generatePrivateKey()); +const account = newSmartAccount8130({ signer: signer as never }); + +// There is no deploy step: the account is created by its first transaction. +// The payer covers gas, so this works at a zero balance — no faucet needed. +const payerClient = createPayerClient({ + url: "https://api.vibes.base.org/api/vibenet/account/payer", +}); +const { transactionHash: hash } = (await sendSponsoredCalls(client, { + account, + payerClient, + accountChanges: [account.createChange], // only on the first tx + calls: [{ to: account.address, value: 0n, data: "0x" }], + context: { flow: "onboarding" }, +})) as unknown as { transactionHash: Hex }; + +const receipt = await waitForTransactionReceipt8130(client, { hash }); +if (!allPhasesSucceeded(receipt)) throw new Error("a phase reverted"); +``` + +See the [vibenet skill](./skills/vibenet/SKILL.md) for the install steps (the +8130 tooling ships on a viem fork branch), the account lifecycle, session keys, +and the devnet's sharper edges. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.