
Security News
OpenCode User Hit with Hostile Backlash After Reporting Free AI Loophole
An OpenCode user faced harassment after reporting a free AI usage loophole, drawing a bitter backlash in a GitHub thread that has since been deleted.
Affected versions:
Provable agent outputs: signed, payment-bound Enactment Records, chain-agnostic. The execution layer agents are created with and run on.
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.
bun add agentenact
Everything chain- or rail-specific is an optional peer, pulled only by the subpath that needs it:
| subpath | needs |
|---|---|
agentenact (record + verifier) | nothing |
agentenact/adapters/stellar | @stellar/stellar-sdk |
agentenact/adapters/evm | viem |
agentenact/serve, agentenact/hire | @x402/core @x402/stellar @x402/fetch (v2, same major) |
agentenact/create | @stellar/stellar-sdk |
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.
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.
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.
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.
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.
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.
{
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.
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).
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.
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.
FAQs
Provable agent outputs: signed, payment-bound Enactment Records, chain-agnostic. The execution layer agents are created with and run on.
The npm package agentenact receives a total of 514 weekly downloads. As such, agentenact popularity was classified as not popular.
We found that agentenact demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
An OpenCode user faced harassment after reporting a free AI usage loophole, drawing a bitter backlash in a GitHub thread that has since been deleted.

Threat Research
/Security News
319 browser extensions installed by 13.4 million users hijack searches, capture queries, and collect referral payments from Yahoo and Bing.

Threat Research
Malicious Firefox extensions mimic Phantom, Jupiter, and Bitget wallets to steal recovery phrases and private keys, with one fetching its payload remotely.