New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

agentenact

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install
Malware was recently detected in this package.

Affected versions:

0.2.0

agentenact

Provable agent outputs: signed, payment-bound Enactment Records, chain-agnostic. The execution layer agents are created with and run on.

latest
npmnpm
Version
0.1.1
Version published
Weekly downloads
515
10200%
Maintainers
1
Weekly downloads
 
Created
Source

agentenact

Provable agent outputs. An Enactment Record is a signed document binding sha256(input), sha256(output), the settlement transaction that paid for the work, and the agent's on-chain identity. Anyone can verify it from public data.

It proves attribution, payment binding and non-repudiation. It does not prove the output is correct, and uses no zero-knowledge machinery — correctness is what reputation (AgentKarma) scores.

Testnet first. 0.1.1 is proven end to end on Stellar testnet: paid x402 calls, agent-to-agent hires, records verified from public data, and agent birth with sponsor, owner-wallet and passkey owners. The mainnet networks are in the table and their read paths (identity, settlement) are checked against mainnet, but no transaction has been sent to a mainnet from this code yet. Build and test on stellar:testnet; treat mainnet as unproven.

Install

bun add agentenact

Everything chain- or rail-specific is an optional peer, pulled only by the subpath that needs it:

subpathneeds
agentenact (record + verifier)nothing
agentenact/adapters/stellar@stellar/stellar-sdk
agentenact/adapters/evmviem
agentenact/serve, agentenact/hire@x402/core @x402/stellar @x402/fetch (v2, same major)
agentenact/create@stellar/stellar-sdk

Serve — an x402-gated agent that proves every paid output

import { createEnactServer } from 'agentenact/serve';
import { createStellarAdapter, stellarSecretToBytes } from 'agentenact/adapters/stellar';

const handler = await createEnactServer({
  network: 'stellar:testnet',
  agent: { address: AGENT_G_ADDRESS, agentId: '31', secret: stellarSecretToBytes(process.env.AGENT_SECRET!) },
  adapter: createStellarAdapter({ network: 'testnet' }),
  price: '$0.001',
  facilitator: { url: 'https://channels.openzeppelin.com/x402/testnet', apiKey: process.env.OZ_API_KEY! },
  resource: 'https://agent.example/summarise',
  handler: async (input) => ({ body: await summarise(input), mediaType: 'application/json' }),
});

Bun.serve({ port: 3000, fetch: handler }); // or any Request → Response host

Order inside the handler is the invariant: verify → run in memory → settle → sign → send. A handler error means nothing is settled. A settlement failure means the output is discarded. A success returns the body plus PAYMENT-RESPONSE and Enactment-Record headers bound to the real tx hash.

A Stellar x402 auth entry expires maxTimeoutSeconds after the client signs it (x402 default 300 s), so the handler is bounded by handlerTimeoutMs (default 45 s, a wide margin inside that window): a slow handler returns 504 before settlement rather than settling into an expired entry. Nothing that can fail on the agent's side runs after money has moved; if the record step itself fails, the response is a 500 that still carries PAYMENT-RESPONSE and the transaction hash.

Hire — pay, receive, verify

import { hire } from 'agentenact/hire';
import { createStellarAdapter } from 'agentenact/adapters/stellar';

const r = await hire('https://agent.example/summarise', { method: 'POST', body: text }, {
  adapter: createStellarAdapter({ network: 'testnet' }),
  payer: { secret: process.env.PAYER_SECRET!, network: 'stellar:testnet' },
  maxAmount: '0.05', // required with a payer: the most this call may pay, in USDC
});
// r.output, r.settlement.transaction, r.paid, r.record, r.verification.checks, r.consistent, r.problems

hire pays whatever the seller's 402 asks, so it checks the offer first and refuses before anything is signed or sent (it throws an error with code: 'payment_refused' and a reason). With a payer, maxAmount is required. The offer must be the network's pinned USDC (or the asset you pin, as CODE:ISSUER), exact scheme, x402 v2, a valid payTo that is not the payer (or the payTo you pin), an amount at most maxAmount, and an authorization lifetime of at most 300 s. One offer is approved and paid once; a second payment request is refused. onPay(offer) runs right before the signed payment leaves (reserve a spend there; throw to refuse, nothing is sent). createPaymentGuard and checkPaymentRequired are exported from agentenact/hire for callers that wrap their own fetch.

hire never throws on a bad record. consistent is true only when the record verifies from public data and its input hash, output hash and settlement reference match what this client sent, received and paid.

Wallets — keys outside the sandbox, spending inside a policy

An agent can own wallets on several networks (any network in NETWORKS: Stellar, Celo, Arc). The keys never enter the agent's sandbox. They live in a policy signer on the operator's infra, which signs on its own, with no human per transaction, whenever a request fits the spend policy the owner set once:

// outside the sandbox (operator infra)
import { createPolicySigner, createSignerHandler, generateWalletKey } from 'agentenact/signer';

const evm = await generateWalletKey('eip155:5042002'); // store evm.secret here; give the agent only the address
const signer = await createPolicySigner({
  keys: [{ network: 'stellar:testnet', secret: process.env.AGENT_STELLAR_SECRET! }, evm],
  policy: {
    rules: [
      { network: 'stellar:testnet', asset: 'USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5', perTx: '0.05', perWindow: '5' },
      { network: 'eip155:5042002', asset: '0x3600000000000000000000000000000000000000', perTx: '0.05', perWindow: '5', payTo: ['0x…'] },
    ],
    expiresAt: '2026-12-31T00:00:00Z',
  },
  // ledger: a persistent SpendLedger; the default is in memory and forgets the window on restart
});
Bun.serve({ port: 7402, fetch: createSignerHandler(signer, { token: process.env.SIGNER_TOKEN! }) });

// inside the sandbox (the agent)
import { httpSignerTransport, listWallets, stellarPaySigner, evmPaySigner } from 'agentenact/wallet';
const transport = httpSignerTransport({ url: process.env.SIGNER_URL!, token: process.env.SIGNER_TOKEN! });
const [stellar] = await listWallets(transport);
await hire(url, init, { adapter, payer: { signer: stellarPaySigner(transport, stellar), network: 'stellar:testnet' }, maxAmount: '0.05' });

The signer never trusts the agent's description of a payment. It decodes the exact payload it signs: on Stellar, the auth-entry preimage (only a single pinned-SAC transfer from this wallet, on this network, no sub-invocations); on EVM, EIP-3009 TransferWithAuthorization typed data (canonical fields, this chain id, a pinned token, from this wallet, valid at most 300 s). The amount, asset and payee checked against the policy are the ones in that payload. Check, count and sign run under one lock. A spend is counted when it is signed, whether or not it settles. Anything else is refused with { ok: false, reason } (no_rule, over_per_tx, over_window, pay_to_not_allowed, policy_expired, unsupported_payload, …).

httpSignerTransport POSTs only the request and the bearer token to the URL you give it (serve the handler where only the sandbox can reach it); a refusal comes back as { ok: false, reason }, a network failure or timeout throws. stellarPaySigner and evmPaySigner have the shapes @x402/stellar and @x402/evm clients expect. hire pays on Stellar only for now; an EVM wallet works with @x402/fetch and ExactEvmScheme directly.

Create — an agent born with an account, a trustline and an identity

import { createStellarAgent } from 'agentenact/create';

const born = await createStellarAgent({
  network: 'stellar:testnet',
  sponsorSecret: process.env.SPONSOR_SECRET!, // your own funded account; never stored
  agentUri: 'https://agent.example/.well-known/agent.json',
});
// born.publicKey, born.secret (returned once), born.agentId, born.txs.{account,register}

Sponsored reserves (CAP-33) open the account and USDC trustline; the registration is fee-bumped (CAP-15). The agent holds zero XLM at every step.

Or let an owner wallet hold the identity. The owner is any signer (Freighter, a hardware wallet, a local key); its secret never enters the package:

const born = await createStellarAgent({
  network: 'stellar:testnet',
  owner: { publicKey, signTransaction: (xdr, networkPassphrase) => wallet.sign(xdr, networkPassphrase) },
  agentUri,
});
// born.owner, born.agentId, born.txs.{account,register,bind}

The owner sponsors the agent account, registers the identity, and binds the agent as its wallet (set_agent_wallet, authorised by both). If a step fails after the account opened, the thrown error carries err.partial with the agent's secret, so nothing on chain is orphaned.

An owner that should never touch XLM (a passkey smart wallet, C…) can sign auth entries only, while a separate sponsor sources and pays for every tx:

const born = await createStellarAgent({
  network: 'stellar:testnet',
  owner: { address: 'C…', signAuthEntry: (entryXdr, validUntilLedger, passphrase) => wallet.signAuthEntry(entryXdr, validUntilLedger, passphrase) },
  sponsor: { publicKey, signTransaction },
  agentUri,
  keypair, // optional: bring the agent key you already store
});

signAuthEntry must return the whole signed SorobanAuthorizationEntry (base64 XDR): what a passkey kit's signAuthEntry or authorizeEntry (for a G… owner) returns. Freighter's method of the same name returns a bare signature and cannot be passed through as-is.

Concurrent births from one sponsor must not interleave their sponsor sequence. Pass sponsorLock: (work) => myMutex.run(work) (sponsor and auth-entry owner modes) and each sponsor-sourced tx runs in one call: from reading the sponsor account through building, re-simulating, signing and submitting, until the submit returned or failed. The lock is never nested and never held while the owner signs an auth entry; the recording simulation (never submitted) reads the sponsor account outside it. In sponsor mode only tx 1 is sponsor-sourced; the fee-bump uses the agent's sequence. Your lock must release when work throws. Without the option work just runs. Wallet-owner mode rejects the option.

The registry records the C… address as owner. Each registry call is re-simulated with the signed entries before submit, so a bad owner signature fails before anything is sent.

If an auth-entry birth stopped part-way (the owner never approved), pass the same keypair with resume: { account: true, agentId? } to continue from what is on chain instead of starting over. The library checks the account, its USDC trustline and (with agentId) the registry's owner and wallet itself, throws code: 'resume_mismatch' before sending anything if a fact does not hold, and returns txs with only the transactions this call sent.

Careful: { account: true } without agentId always sends register_with_uri. Use it only when you know no registration was sent for this key (the owner never signed it). If a register may have landed (confirm failed, unknown outcome), pass agentId if you know it, or do not resume. When the identity is already bound to the key, resume returns success after checking owner and wallet only; it does not compare the registered URI.

Templates — start an agent from a ready-made draft

import { fromTemplate, getTemplate, TEMPLATES } from 'agentenact/templates';

const agent = fromTemplate(getTemplate('translate')!, {
  params: { to: 'English' },
  name: 'TR→EN translator', // optional; the template has defaults for name, description, price
  model: async ({ instructions, input }) => myLlm(instructions, input), // your model, your key
});

await createStellarAgent({ network: 'stellar:testnet', owner, agentUri: agent.agentUri });
const handler = await createEnactServer({ network, agent: keys, adapter, facilitator, resource, ...agent.serve });

A template is plain data (summarize, translate, extract, classify ship built in; defineTemplate validates your own): name, description and price defaults, the input/output media types, a byte limit and the model instructions with {{param}} slots. fromTemplate returns the ERC-8004 registration document inline as a data: URI and the price, description and handler for serve. The package ships no model and calls nothing; the handler only shapes the call to yours. Empty, oversized or non-UTF-8 input and a JSON template's non-JSON answer throw inside the handler, so serve answers 500 before settlement and the payer is not charged.

Verify — anyone

import { decodeRecordHeader, verifyRecord } from 'agentenact';
import { createStellarAdapter } from 'agentenact/adapters/stellar';

const result = await verifyRecord(decodeRecordHeader(headerValue), createStellarAdapter({ network: 'testnet' }));
// { ok, checks: { signature, settlement, timing, identity }, failure?, settlement? }

Checks, in order: signature over the canonical digest · the referenced tx pays the agent the stated amount and asset from the stated payer · issuedAt is after settlement and within 24h (windowSeconds) · if agentId is set, the ERC-8004 identity registry binds it to the signing address.

A missing settlement is not_found, unless the record was issued before the oldest ledger the Horizon still serves: then it is history_unavailable (testnet resets and pruned nodes remove old payments). Either way it does not verify; the second only means the record can no longer be checked.

From a terminal (needs @stellar/stellar-sdk next to the package):

npx agentenact verify record.json --input in.txt --output out.json
# MATCH signature / MATCH settlement / MATCH timing / MATCH identity
# MATCH input sha256 … / MATCH output sha256 …
# VERIFIED: signer, payment and timing check out. This says nothing about whether the output is right.

<record> is a file, an https URL, or an Enactment-Record header value. --network pubnet|testnet (default testnet), --horizon <url> to read from a Horizon of your choice, --json for the raw result. Exit 0 verified, 1 not, 2 could not check.

Conformance vectors in tests/vectors/ pin the verdict for 15 records (real testnet records, tampered copies, and records signed with a published test-only key), replayed offline from recorded Horizon and Soroban answers. A verifier in another language can run the same files.

Record shape (v0.1, v0.2)

{
  v: '0.1' | '0.2',
  agent: { chain, address, agentId? },
  input:  { sha256, mediaType? },
  output: { sha256, mediaType?, bytes? },
  skills: [{ id, sha256 }], // v0.2 only, required there; sorted by id, may be empty
  settlement: { chain, ref, payer, amount, asset },
  issuedAt, // RFC 3339 UTC
  nonce,    // 16 random bytes, hex
  sig: { alg: 'ed25519' | 'secp256k1-eip191', value }
}

settlement.asset is the chain's unspoofable asset identity: CODE:ISSUER on Stellar (STELLAR_USDC is Circle's), the lowercase token address on EVM chains. A bare "USDC" never verifies. The Stellar adapter reads classic payments and Soroban invoke_host_function transfers (how x402 and MPP settlements appear).

v0.2 adds skills: the approved skills (agentenact/skills content hashes) the agent says it used for this output. serve issues v0.2 only when the handler returns skills (e.g. recordSkills(kit.used())); otherwise v0.1, byte for byte as before. It is the agent's signed statement, not proof the skills ran.

Canonical bytes are fixed-order JSON without whitespace and without sig; the signed digest is sha256(canonical). Input and output bytes never leave the parties — only their hashes are in the record.

Networks

stellar:testnet (proven end to end), stellar:pubnet, eip155:42220 (Celo), eip155:5042002 (Arc testnet). Unknown networks and assets throw; nothing is guessed. On stellar:pubnet only reads have been exercised so far; registering an identity there costs real XLM (simulate first).

Invariants

Keys are generated and held by the operator; nothing here transmits one. Agents serve directly — no proxy, escrow or relay. No token, no competing registry, no competing manifest format. Enforced by tests/invariants.test.ts.

Status

0.1.1 (first working npm release, testnet-proven): record v0.1 and v0.2, verifier and the agentenact verify CLI, Stellar + EVM adapters, serve, hire, create (sponsor, owner-wallet and auth-entry owners, resume, sponsorLock), templates, skills, and the wallet / signer policy signer. Offline suite green; testnet x402 serve → hire → verify and agent birth proven end to end; Stellar and Celo identity reads and a real Soroban USDC transfer verified against mainnet (read-only). The Stellar testnet x402 loop (tests/e2e/stellar-testnet.test.ts) is opt-in and needs a funded payer plus an OpenZeppelin Channels key — see scripts/testnet-setup.ts.

Keywords

agentenact

FAQs

Package last updated on 27 Oct 2026

Related posts