{"_id":"@agent-pulse/middleware","_rev":"2-118b5d85b32899c32a5cb136422f8a1c","name":"@agent-pulse/middleware","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@agent-pulse/middleware","version":"1.0.0","keywords":["agent-pulse","liveness","pulse","base","ethereum","middleware","express","langchain","ai-agents","filter","on-chain"],"author":{"name":"Agent Pulse Team"},"license":"MIT","_id":"@agent-pulse/middleware@1.0.0","maintainers":[{"name":"consensuscli","email":"openclaw@consensus.run"}],"homepage":"https://agentpulse.xyz","bugs":{"url":"https://github.com/consensus-hq/agent-pulse/issues"},"dist":{"shasum":"e59b12715ff372093fb6fdd2ed595146329e53a6","tarball":"https://registry.npmjs.org/@agent-pulse/middleware/-/middleware-1.0.0.tgz","fileCount":7,"integrity":"sha512-LNBlJsltxRj6CLoovFOn3CfjUChZmes8yvTIdAUeWRvS8MW2wEK3L5NZaFFM8BSjeWM+0mwPb3Uh5DJU6ByscA==","signatures":[{"sig":"MEUCIH6QtyTIUQePRY1dWHK0KZfhie5Ztvims4THRkQH19cfAiEA+mhgxxgnbIkqlH7eR823zbEv/BHtqE9LRefFcVWPy9E=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":32607},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./middleware":{"types":"./dist/middleware.d.ts","import":"./dist/middleware.js"}},"gitHead":"d8db49ccc4ea143444344308a9a33d72c13009d3","scripts":{"dev":"tsc --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsc","clean":"rm -rf dist","test:live":"PULSE_LIVE_TEST=1 vitest run","test:watch":"vitest","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"consensuscli","email":"openclaw@consensus.run"},"repository":{"url":"git+https://github.com/consensus-hq/agent-pulse.git","type":"git","directory":"packages/pulse-filter"},"_npmVersion":"10.9.4","description":"Lightweight liveness filter for Agent Pulse — check if on-chain agents are alive using just fetch()","directories":{},"sideEffects":false,"_nodeVersion":"22.22.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^1.0.0","typescript":"^5.3.0"},"_npmOperationalInternal":{"tmp":"tmp/middleware_1.0.0_1770421227509_0.9388668908496798","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@agent-pulse/middleware","version":"1.1.0","description":"Lightweight liveness filter for Agent Pulse — check if on-chain agents are alive using just fetch()","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./middleware":{"types":"./dist/middleware.d.ts","import":"./dist/middleware.js","require":"./dist/middleware.cjs"}},"scripts":{"build:esm":"tsc -p tsconfig.json","build:cjs":"tsc -p tsconfig.cjs.json","build":"npm run build:esm && npm run build:cjs && node scripts/cjs-to-dist.mjs","dev":"tsc -p tsconfig.json --watch","test":"vitest run","test:watch":"vitest","test:live":"PULSE_LIVE_TEST=1 vitest run","lint":"tsc -p tsconfig.json --noEmit","clean":"rm -rf dist dist-cjs","prepublishOnly":"npm run clean && npm run build"},"devDependencies":{"typescript":"^5.3.0","vitest":"^1.0.0"},"keywords":["agent-pulse","liveness","pulse","base","ethereum","middleware","express","langchain","ai-agents","filter","on-chain"],"author":{"name":"Agent Pulse Team"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/consensus-hq/agent-pulse.git","directory":"packages/pulse-filter"},"homepage":"https://agentpulse.xyz","bugs":{"url":"https://github.com/consensus-hq/agent-pulse/issues"},"engines":{"node":">=18.0.0"},"sideEffects":false,"_id":"@agent-pulse/middleware@1.1.0","gitHead":"4d6d40479f9f59403791056bbca103e539331b2d","_nodeVersion":"22.22.0","_npmVersion":"10.9.4","dist":{"integrity":"sha512-yCBLkCOdyuyradKMD2R/7JkteYJDWldw/HvR7QkV5cmfvdHvteLN86kjKLQ3wjeVY/D1v02Uk+DmcFTLWgfj/A==","shasum":"3c0ad9a35103108a20b0cc69446b95dde2c08f91","tarball":"https://registry.npmjs.org/@agent-pulse/middleware/-/middleware-1.1.0.tgz","fileCount":9,"unpackedSize":50453,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDtJmTZ5JIQS2nF94nHw/tj3DcaPCHs7mf2AZsQiXM3HgIgIKWWVPUz0fWc24+X/FGv48QFT84uNDAZ3xCLfBTeCIg="}]},"_npmUser":{"name":"consensuscli","email":"openclaw@consensus.run"},"directories":{},"maintainers":[{"name":"consensuscli","email":"openclaw@consensus.run"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/middleware_1.1.0_1770427888029_0.4541896286780809"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-06T23:40:27.423Z","modified":"2026-02-07T01:31:28.353Z","1.0.0":"2026-02-06T23:40:27.680Z","1.1.0":"2026-02-07T01:31:28.180Z"},"bugs":{"url":"https://github.com/consensus-hq/agent-pulse/issues"},"author":{"name":"Agent Pulse Team"},"license":"MIT","homepage":"https://agentpulse.xyz","keywords":["agent-pulse","liveness","pulse","base","ethereum","middleware","express","langchain","ai-agents","filter","on-chain"],"repository":{"type":"git","url":"git+https://github.com/consensus-hq/agent-pulse.git","directory":"packages/pulse-filter"},"description":"Lightweight liveness filter for Agent Pulse — check if on-chain agents are alive using just fetch()","maintainers":[{"name":"consensuscli","email":"openclaw@consensus.run"}],"readme":"# @agent-pulse/middleware\n\n[![npm version](https://img.shields.io/npm/v/@agent-pulse/middleware.svg)](https://www.npmjs.com/package/@agent-pulse/middleware)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n[![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)\n\n**Zero-dependency liveness filter for [Agent Pulse](https://agentpulse.xyz)** — check if on-chain agents are alive using just `fetch()`.\n\nWorks in Node.js 18+, Bun, Deno, Cloudflare Workers, and any edge runtime.\n\n## Quick Start\n\n```bash\nnpm install @agent-pulse/middleware\n```\n\n```ts\nimport { filterAlive } from \"@agent-pulse/middleware\";\n\nconst alive = await filterAlive([\n  \"0xAgentA...\",\n  \"0xAgentB...\",\n  \"0xAgentC...\",\n]);\n// → [\"0xAgentA...\"] — only the alive ones\n```\n\nThat's it. Three lines. No ethers, no viem, no RPC node required.\n\n## What is Agent Pulse?\n\nAgent Pulse is an on-chain liveness attestation protocol on [Base](https://base.org). Autonomous AI agents periodically burn PULSE tokens to prove they're still running. This package lets you **query that liveness data** via the Agent Pulse API — so you can route work to alive agents, gate API access, or build trust scores.\n\n- **PULSE Token**: `0x21111B39A502335aC7e45c4574Dd083A69258b07` (Base mainnet)\n- **PulseRegistryV2**: `0xe61C615743A02983A46aFF66Db035297e8a43846`\n- **API**: `https://agent-pulse-nine.vercel.app/api/v2/agent/{address}/alive`\n\n## API\n\n### `isAlive(address, options?)`\n\nCheck if a single agent is alive.\n\n```ts\nimport { isAlive } from \"@agent-pulse/middleware\";\n\nconst alive = await isAlive(\"0x1234567890abcdef1234567890abcdef12345678\");\n// → true or false\n```\n\n### `filterAlive(agents, options?)`\n\nFilter a list of addresses to only the alive ones. Returns `string[]`.\n\n```ts\nimport { filterAlive } from \"@agent-pulse/middleware\";\n\nconst alive = await filterAlive([\"0xAbc...\", \"0xDef...\", \"0x123...\"]);\n// → [\"0xAbc...\"]\n```\n\n### `filterAliveDetailed(agents, options?)`\n\nLike `filterAlive` but returns full details, including per-agent status and errors.\n\n```ts\nimport { filterAliveDetailed } from \"@agent-pulse/middleware\";\n\nconst result = await filterAliveDetailed([\"0xAbc...\", \"0xDef...\"]);\n// result.alive    → [\"0xAbc...\"]\n// result.details  → [{ address, isAlive, streak, staleness, ... }, ...]\n// result.errors   → [{ address: \"0xBad\", reason: \"Invalid address\" }]\n// result.checkedAt → \"2026-02-06T21:49:21.595Z\"\n```\n\n### `PulseFilter` class\n\nReusable instance with pre-configured options — avoids passing options on every call.\n\n```ts\nimport { PulseFilter } from \"@agent-pulse/middleware\";\n\nconst pulse = new PulseFilter({\n  threshold: 3600,  // 1 hour\n  timeoutMs: 5000,\n  retries: 3,\n});\n\nawait pulse.isAlive(\"0xAbc...\");                     // → boolean\nawait pulse.filterAlive([\"0xAbc...\", \"0xDef...\"]);   // → string[]\nawait pulse.filterAliveDetailed([\"0xAbc...\"]);       // → FilterResult\nawait pulse.getStatus(\"0xAbc...\");                   // → AliveResponse\n```\n\n### Options\n\nAll functions and the `PulseFilter` constructor accept `PulseFilterOptions`:\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `apiUrl` | `string` | `\"https://agent-pulse-nine.vercel.app\"` | Base URL for the Agent Pulse API |\n| `threshold` | `number` | — | Max staleness in seconds. Agents older than this are dead even if the chain says alive |\n| `timeoutMs` | `number` | `10000` | Fetch timeout in milliseconds |\n| `retries` | `number` | `2` | Retries on 5xx / network errors (exponential backoff) |\n| `retryDelayMs` | `number` | `500` | Base delay between retries (caps at 4× this value) |\n\n## Express Middleware\n\nProtect your Express routes — only alive agents get through.\n\n```bash\nnpm install @agent-pulse/middleware\n```\n\n```ts\nimport express from \"express\";\nimport { pulseGuard } from \"@agent-pulse/middleware/middleware\";\n\nconst app = express();\napp.use(express.json());\n\n// Protect a route — reads agent address from header, query, or body\napp.use(\"/api/agents\", pulseGuard());\n\n// Custom config\napp.use(\"/api/protected\", pulseGuard({\n  headerName: \"x-agent-id\",   // Custom header name\n  threshold: 3600,             // 1 hour max staleness\n  allowMissing: false,         // 400 if no address provided\n  timeoutMs: 5000,\n}));\n\napp.post(\"/api/agents/task\", (req, res) => {\n  // req.pulseStatus has the full liveness data\n  res.json({ message: \"Task accepted\", agent: req.headers[\"x-agent-address\"] });\n});\n\napp.listen(3000);\n```\n\n### How it works\n\n1. Extracts the agent address from (in order): **header** → **query param** → **body field**\n2. Calls the Agent Pulse API to check liveness\n3. **Alive** → attaches `pulseStatus` to `req` and calls `next()`\n4. **Dead** → responds with `403` and a \"Missing Link\" payload:\n   `{\n     error: \"AGENT_HAS_NO_PULSE\",\n     message: \"...\",\n     address: \"0x...\",\n     fix: {\n       install: \"npm install @agent-pulse/middleware\",\n       npm: \"https://www.npmjs.com/package/@agent-pulse/middleware\",\n       github: \"https://github.com/consensus-hq/agent-pulse\",\n       docs: \"https://agentpulse.xyz\"\n     }\n   }`\n5. **API error** → fails **open** (calls `next()` with `X-Pulse-Warning` header)\n\n### Middleware Options\n\nExtends `PulseFilterOptions` with:\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `headerName` | `string` | `\"x-agent-address\"` | Header to read address from |\n| `queryParam` | `string` | `\"agent\"` | Query parameter name |\n| `bodyField` | `string` | `\"agentAddress\"` | JSON body field name |\n| `allowMissing` | `boolean` | `false` | If `true`, pass through when no address found (instead of 400) |\n| `onRejected` | `function` | — | Custom handler for dead agents (receives `req, res, next, { address, status }`) |\n| `onAlert` | `function` | — | Fire-and-forget callback invoked on every rejection (use for logging/webhooks/alerting). Errors are swallowed. |\n\n## LangChain Tool Example\n\nUse Agent Pulse as a LangChain tool to let your AI agents verify liveness:\n\n```ts\nimport { DynamicTool } from \"@langchain/core/tools\";\nimport { PulseFilter } from \"@agent-pulse/middleware\";\n\nconst pulse = new PulseFilter({ threshold: 3600 });\n\nconst pulseFilterTool = new DynamicTool({\n  name: \"pulse_filter\",\n  description:\n    \"Check if an Ethereum agent address is alive on the Agent Pulse protocol. \" +\n    \"Input: a single 0x Ethereum address. Output: JSON with alive status.\",\n  func: async (address: string) => {\n    try {\n      const status = await pulse.getStatus(address);\n      return JSON.stringify({\n        address: status.address,\n        alive: status.isAlive,\n        streak: status.streak,\n        staleness: status.staleness,\n        lastPulse: status.lastPulseTimestamp > 0\n          ? new Date(status.lastPulseTimestamp * 1000).toISOString()\n          : \"never\",\n      });\n    } catch (err) {\n      return JSON.stringify({ error: String(err) });\n    }\n  },\n});\n\n// Use in a LangChain agent\n// agent.tools = [pulseFilterTool, ...otherTools];\n```\n\n### Batch Filter Tool\n\n```ts\nconst batchFilterTool = new DynamicTool({\n  name: \"pulse_batch_filter\",\n  description:\n    \"Filter a JSON array of Ethereum addresses to only those that are alive. \" +\n    \"Input: JSON array of 0x addresses. Output: JSON with alive addresses.\",\n  func: async (input: string) => {\n    const addresses = JSON.parse(input) as string[];\n    const result = await pulse.filterAliveDetailed(addresses);\n    return JSON.stringify({\n      alive: result.alive,\n      aliveCount: result.alive.length,\n      totalChecked: addresses.length,\n      errors: result.errors,\n    });\n  },\n});\n```\n\n## Types\n\nAll types are exported and available for TypeScript consumers:\n\n```ts\nimport type {\n  PulseFilterOptions,\n  AliveResponse,\n  FilterResult,\n  PulseGuardOptions,\n  PulseGuardRejection,\n} from \"@agent-pulse/middleware\";\n```\n\n### `AliveResponse`\n\n```ts\ninterface AliveResponse {\n  address: string;\n  isAlive: boolean;\n  lastPulseTimestamp: number;\n  streak: number;\n  staleness: number | null;\n  ttl: number;\n  checkedAt: string;\n}\n```\n\n### `FilterResult`\n\n```ts\ninterface FilterResult {\n  alive: string[];\n  details: AliveResponse[];\n  errors: Array<{ address: string; reason: string }>;\n  checkedAt: string;\n}\n```\n\n## Advanced Usage\n\n### Custom API URL\n\nPoint to a different API (e.g., your own relay or staging):\n\n```ts\nconst pulse = new PulseFilter({\n  apiUrl: \"https://my-pulse-relay.example.com\",\n});\n```\n\n### Strict Threshold\n\nOnly consider agents alive if they pulsed within the last 5 minutes:\n\n```ts\nconst alive = await filterAlive(agents, { threshold: 300 });\n```\n\n### Error-Resilient Batch Processing\n\n```ts\nconst result = await filterAliveDetailed(hundredsOfAgents, {\n  retries: 3,\n  timeoutMs: 15_000,\n});\n\nconsole.log(`Alive: ${result.alive.length}`);\nconsole.log(`Errors: ${result.errors.length}`);\n\n// Retry the failed ones\nif (result.errors.length > 0) {\n  const retryAddresses = result.errors.map(e => e.address);\n  const retry = await filterAliveDetailed(retryAddresses);\n  result.alive.push(...retry.alive);\n}\n```\n\n### Webhook Handler\n\n```ts\napp.post(\"/webhook/route-to-alive\", async (req, res) => {\n  const { agents, payload } = req.body;\n  const alive = await filterAlive(agents, { threshold: 300 });\n\n  // Route work only to alive agents\n  await Promise.all(alive.map(agent => sendTask(agent, payload)));\n\n  res.json({ routed: alive.length, total: agents.length });\n});\n```\n\n## Design Principles\n\n- **Zero dependencies** — only uses native `fetch()`, works everywhere\n- **Edge-ready** — runs on Cloudflare Workers, Vercel Edge, Deno Deploy\n- **Fail-open middleware** — API errors don't block your traffic\n- **Typed end-to-end** — strict TypeScript, all exports typed\n- **Retries built-in** — exponential backoff for 5xx and network failures\n\n## License\n\nMIT © Agent Pulse Team\n","readmeFilename":"README.md"}