Skip to content
Star3

Quickstart

This guide adds hashspan to an agent that sends transactions with viem. You need Node.js 22.3 or later.

  • Adding it to your own agent? Follow the steps below.
  • Starting from scratch? Jump to the complete example: one file, a local chain, no API key.
  • Just want to see a trace? The local lab runs the repository’s example agent.
  1. Terminal window
    npm install @hashspan/viem @opentelemetry/api viem
  2. hashspan only needs @opentelemetry/api. Any SDK setup works. If you don’t have one yet, this exports spans over OTLP:

    Terminal window
    npm install \
    @opentelemetry/sdk-trace-node \
    @opentelemetry/sdk-trace-base \
    @opentelemetry/exporter-trace-otlp-proto \
    @opentelemetry/resources \
    @opentelemetry/semantic-conventions
    telemetry.ts
    import { OTLPTraceExporter } from
    '@opentelemetry/exporter-trace-otlp-proto';
    import { resourceFromAttributes } from '@opentelemetry/resources';
    import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
    import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
    import { ATTR_SERVICE_NAME } from
    '@opentelemetry/semantic-conventions';
    export const provider = new NodeTracerProvider({
    resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'my-agent',
    }),
    // OTLPTraceExporter honours OTEL_EXPORTER_OTLP_ENDPOINT
    spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter())],
    });
    provider.register();

    Import it first in your entry file, before anything that sends transactions: import { provider } from './telemetry.js';.

  3. import { createPublicClient, createWalletClient, http } from 'viem';
    import { baseSepolia } from 'viem/chains';
    import { withHashspan } from '@hashspan/viem';
    const hashspan = withHashspan();
    const chain = baseSepolia;
    // account: any viem Account, e.g. privateKeyToAccount(process.env.PRIVATE_KEY)
    const wallet = createWalletClient({ account, chain, transport: http() })
    .extend(hashspan);
    const reader = createPublicClient({ chain, transport: http() })
    .extend(hashspan);
    // Inside an agent tool, while the framework's span is active:
    const hash = await wallet.sendTransaction({ to, value }); // send span
    await reader.waitForTransactionReceipt({ hash }); // confirm span

    The confirm span links back to the send span. Reuse the same withHashspan() result for every client of one agent, so confirmations link to their sends.

  4. A confirm span can end after your code moves on, for example while a revert reason is decoded. Short-lived scripts and serverless functions should flush first:

    await hashspan.flush();
    await provider.shutdown();
  5. Find your service in your tracing backend and expand a tool span. No backend yet? Start Jaeger locally:

    Terminal window
    docker run --rm -p 16686:16686 -p 4318:4318 \
    jaegertracing/jaeger:2.21.0

    Then open localhost:16686.

One file that sends a transaction on a local Anvil chain and traces it. Put it next to telemetry.ts from step 2, in a project with the packages from steps 1 and 2 and "type": "module" in its package.json:

agent.ts
import { provider } from './telemetry.js';
import { trace } from '@opentelemetry/api';
import { createPublicClient, createWalletClient, http, parseEther } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { foundry } from 'viem/chains';
import { withHashspan } from '@hashspan/viem';
// Anvil's first test account. Its key is public: never use it on a real chain.
const account = privateKeyToAccount(
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
);
const to = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'; // Anvil's second account
const hashspan = withHashspan({ agent: { name: 'my-agent' } });
const wallet = createWalletClient({ account, chain: foundry, transport: http() })
.extend(hashspan);
const reader = createPublicClient({ chain: foundry, transport: http() })
.extend(hashspan);
// Stands in for your agent framework's tool span.
await trace.getTracer('my-agent').startActiveSpan('execute_tool pay_vendor', async (tool) => {
const hash = await wallet.sendTransaction({ to, value: parseEther('0.01') });
await reader.waitForTransactionReceipt({ hash });
tool.end();
});
await hashspan.flush();
await provider.shutdown();

Start a chain and Jaeger, then run it:

Terminal window
anvil # terminal 1: local chain on :8545
docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:2.21.0 # terminal 2
npx tsx agent.ts # terminal 3

In Jaeger, pick my-agent: execute_tool pay_vendor has a send 31337 and a confirm 31337 span with blockchain.tx.status = success.

Each transaction shows up as a send and a confirm span under the tool that sent it. A reverted transaction is marked as an error and carries the decoded reason:

Jaeger showing a confirm 31337 span marked as an error, with blockchain.tx.status reverted, the fee, gas used and the decoded revert reason WithdrawalLimitExceeded
A reverted confirm span in Jaeger, from the example agent in the local lab.