The GOAT Network counterpart to Coinbase AgentKit — a TypeScript SDK enabling AI Agents to autonomously execute on-chain operations on the GOAT chain.
agentkit/
├── core/ # Runtime engine (policy, validation, idempotency, retry, metrics, timeout, hooks)
├── plugins/ # 16 feature modules (129 Actions)
├── adapters/ # 5 AI framework adapters
├── providers/ # Action registry + tool manifest generation
├── networks/ # GOAT chain adapter layer (mainnet / testnet)
├── packages/ # Independent packages (create-goat-agent CLI)
├── bin/ # End-user CLI binaries (agentkit-gns, agentkit-giftcard, agentkit-paykit)
├── examples/ # Minimal runnable examples
├── tests/ # Unit + integration tests
└── docs/ # Design documents
| Layer | Responsibility | Key File |
|---|---|---|
| Core | Runtime engine: Policy → Validation → Idempotency → Retry → Metrics → Timeout → Hooks | core/runtime/execution-runtime.ts |
| Plugins | Concrete implementations of on-chain operations (each plugin is a group of Actions) | plugins/*/actions/*.ts |
| Adapters | Convert Actions into tool formats for each AI framework | adapters/*/tools.ts |
| Providers | Action registration, discovery, and JSON Schema tool manifest generation | providers/action-provider.ts |
npm create goat-agent
# Follow prompts: project name → preset (minimal/defi/full) → network
cd my-agent && pnpm start# GOAT Name Service — register / renew / lookup .goat names
npx -p @goatnetwork/agentkit agentkit-gns --help
# x402 Giftcard purchase — browse brands, pay cross-chain, track orders
npx -p @goatnetwork/agentkit agentkit-giftcard --help
# Public GOAT Flow PayKit — crypto, products, MPP, card links and recovery
npx -p @goatnetwork/agentkit agentkit-paykit --helpThe GNS and giftcard CLIs read configuration from environment variables (GOAT_PRIVATE_KEY, GNS_API_BASE_URL, GIFTCARD_API_BASE_URL, DEMO_MOCK, …) and ship interactive doctor subcommands for env diagnosis.
npm install @goatnetwork/agentkitimport { ActionProvider } from '@goatnetwork/agentkit/providers';
import { PolicyEngine, ExecutionRuntime } from '@goatnetwork/agentkit/core';
import { NoopWalletProvider } from '@goatnetwork/agentkit/core';
import { walletBalanceAction, transferErc20Action, NoopWalletReadAdapter } from '@goatnetwork/agentkit/plugins';
const wallet = new NoopWalletProvider(); // Replace with EvmWalletProvider or ViemWalletProvider for production
const provider = new ActionProvider();
provider.register(walletBalanceAction(new NoopWalletReadAdapter()));
provider.register(transferErc20Action(wallet));
const policy = new PolicyEngine({
allowedNetworks: ['goat-testnet'],
maxRiskWithoutConfirm: 'low',
writeEnabled: true,
});
const runtime = new ExecutionRuntime(policy, { maxRetries: 2, retryDelayMs: 200 });
const result = await runtime.run(
provider.get('wallet.balance'),
{ traceId: 'trace-1', network: 'goat-testnet', now: Date.now() },
{ address: '0xabc...' },
);
console.log(result.ok ? result.output : result.error);provider.openAITools(); // OpenAI Function Calling
provider.langChainToolDefs(); // LangChain Tools
provider.mcpTools(); // Model Context Protocol
provider.vercelAITools(); // Vercel AI SDK
provider.openAIAgentsTools(); // OpenAI Agents SDK| Plugin | Actions | Functionality |
|---|---|---|
| wallet | 10 | ERC20 transfer / approve / balance / contract read & write / deploy / token symbol resolution |
| bridge | 7 | Bridge.sol real contract: withdraw / cancel / refund / replace-by-fee / deposit-status / withdrawal-status / get-params |
| dex | 7 | OKU (Uniswap V3): swap / quote / get-pool / add-liquidity / remove-liquidity / collect-fees / get-position |
| x402 | 8 | Agent payment protocol: payment.create / submit-signature / transfer / status / cancel / native order / hosted checkout / payment proof |
| paykit | 7 | Public GOAT Flow discovery / custom and product ERC-20 payments / session status / MPP pay and resume / hosted card links |
| giftcard | 8 | x402 giftcard purchase: list-brands / get-brand / list-categories / list-supported-tokens / create-order / pay-order / get-order / list-orders |
| Plugin | Actions | Functionality |
|---|---|---|
| x402-merchant | 31 | Merchant portal management: auth (register / login / refresh / invite) / dashboard / profile / orders / balance / addresses / callback-contracts / API keys / webhooks / invite-codes / audit-logs |
| erc8004 | 9 | ERC-8004 Trustless Agents: register-agent / set-agent-uri / get-metadata / set-metadata / get-agent-wallet / give-feedback / revoke-feedback / get-reputation / get-clients |
| gns | 15 | GOAT Name Service (.goat names): check-availability / estimate-price / commit / wait-for-commitment / register / renew / get-name-details / get-my-names / set-address / set-profile-records / set-primary-name + cross-chain x402 register flow (create-order / submit-signature / pay-order / get-order-status) |
GOAT Testnet3 ERC-8004 upgrade note: the goat-testnet Identity Registry now uses
0x54B8d8E2455946f2A5B8982283f2359812e815ce, replacing
0x556089008Fc0a60cD09390Eca93477ca254A5522 from versions 0.1.2–0.2.3. It pairs with
the unchanged Reputation Registry 0xd9140951d8aE6E5F625a02F5908535e16e3af964.
Mainnet addresses, ABIs and action arguments are unchanged. Existing registrations
are not migrated: agent IDs are scoped to their registry, and an old numeric ID
may identify another agent or no agent under the new default. Keep historical
registrations associated with the old contract; register in the new Identity
Registry before using the corrected pair. New registration files use
eip155:48816:0x54B8d8E2455946f2A5B8982283f2359812e815ce as agentRegistry.
| Plugin | Actions | Functionality |
|---|---|---|
| layerzero | 3 | LayerZero V2 OFT cross-chain: quote-send / send / quote-oft |
| bitvm2 | 10 | BitVM2 BTC bridge + staking: bridge.deposit / bridge.withdraw / bridge.status / stake.register-pubkey / stake.approve / stake.stake / stake.lock / pegbtc.balance / pegin.request / pegout.initiate |
| erc721 | 3 | NFT: mint / transfer / balance |
| wgbtc | 3 | Wrapped GBTC: wrap / unwrap / balance |
| goat-token | 3 | Governance: delegate / get-votes / get-delegates |
| faucet | 2 | Testnet tokens: request-funds / get-chains |
| bitcoin | 3 | On-chain BTC light client: block-hash / latest-height / network-name |
Total: 129 Actions across 16 plugins + customActionProvider() for unlimited custom extensions.
| Adapter | Target Framework |
|---|---|
openai/tools.ts |
OpenAI Function Calling |
langchain/tools.ts |
LangChain Tools |
mcp/tools.ts |
Model Context Protocol |
vercel-ai/tools.ts |
Vercel AI SDK |
openai-agents/tools.ts |
OpenAI Agents SDK |
Define an Action once, automatically available across all five frameworks.
The core differentiating capability of AgentKit. Four complementary plugin sets:
Payer side (x402 — 8 actions): The Agent acts as the "payer", completing payments with merchant gateways via EIP-712 signatures:
HttpMerchantGatewayAdapter— interfaces with merchant APIsEvmPayerWalletAdapter— local signing and authorization- Full EIP-712 signing flow example (
examples/x402-payment-flow/)
Merchant side (x402-merchant — 31 actions): Full merchant portal management via MerchantPortalClient HTTP adapter — auth, dashboard, orders, balance, webhooks, API keys, callback contracts, invite codes, and audit logs. Per-request token isolation via ActionContext.accessToken with sensitiveOutputFields redaction for hook/log safety.
Public payment links (paykit — 7 actions): Merchant discovery, custom and product ERC-20 payments, session status, GOAT Flow MPP payment and verification recovery, and human-completed hosted card checkout. See the PayKit guide below.
Real-world consumer flow (giftcard — 8 actions): A turnkey reference for "Agent pays cross-chain → user receives off-chain good". Browse brands and categories, create a giftcard order, pay the order via x402 from a source chain wallet (USDC/USDT on Polygon/Base/Arbitrum/Optimism/BSC/Metis), and poll until FULFILLED. Includes an EIP-712 auth signer with Redis-backed single-flight to avoid duplicate token requests under concurrent load. Backed by the agentkit-giftcard CLI for end users.
This is the implementation of the Coinbase x402 protocol on GOAT Network, enabling Agents to complete on-chain payments without a human account.
The gns plugin (15 actions) provides a complete .goat namespace stack:
- Read paths:
checkAvailability,getNameDetails,getMyNames,estimatePrice - Two-phase ENS-style registration:
commit→waitForCommitment(min/max commitment age verified on-chain) →register(with permit or pre-approve) →renew - Profile management:
setAddress,setProfileRecords(text + addr multicall),setPrimaryName(reverse record) - Cross-chain x402 registration: pay for a
.goatregistration in USDC/USDT on Polygon / Base / Arbitrum / Optimism / BSC / Metis; the GNS backend settles on goat-mainnet after payment confirmation. Actions:x402.createOrder→x402.submitSignature→x402.payOrder→x402.getOrderStatus. The on-chain commit binds the GOAT-side stablecoin address resolved by canonical symbol (via the SDK'sGNS_PAYMENT_TOKENStable, mirroring the backend'spaymentTokenAddressFromSymbol) and the pricedtotalWeifrom/names/quote;createOrderforwards the same value ascommitMaxAmountWeiso the backend's strict-equality re-derivation passes. Theagentkit-gns x402-registerCLI orchestrates the full sequence:x402.configpreflight →estimatePrice→commit→waitForCommitment(polls untilblock.timestamp ≥ revealAt; the on-chainminCommitmentAgeis 60s and the CLI's overall wait deadline defaults to 180s) →createOrder→submitSignature→payOrder→ pollgetOrderStatusuntilINVOICED(the adaptor callback executed on GOAT and the name is actually registered —PAYMENT_CONFIRMEDonly means the source-chain transfer landed).payOrderdefense-in-depth: the action takes payment routing fields (chain id, source-chain token contract, payToAddress, amount, expiry, calldataSignRequest) from the createOrder response as inputs and cross-checks them against three trust anchors before broadcasting — the status-endpointOrderProof, the backend's signedcalldataSignRequest.message.{owner, payer}(mandatory; refuse if absent), and the merchant'sx402Configtoken allowlist for the source chain. The status gate requires explicitCHECKOUT_VERIFIED(backend lifecycle never emitsPAYMENT_PENDING) and fail-closes on terminal failure states. 3-way payer binding enforceswallet ≡ payer ≡ signed message.payerso a mis-wired payer adapter cannot broadcast from a different signer than the chain-validated wallet.
The commit action returns the 32-byte secret as a top-level sensitiveOutputFields value, so default hook/log paths redact it and --reveal-secrets is required to emit it (the CLI uses the dedicated print-secret action for that). The register action re-derives the commitment hash on-chain and rejects any (secret / token / amount / years / owner / resolver / data / reverseRecord / referrer) divergence vs the original commit, preventing parameter drift between phases. Backed by the agentkit-gns CLI for end users.
Execution pipeline: Policy Gate → Schema Validation (Zod) → Idempotency → Retry → Timeout → Metrics → Hooks
- Policy Engine: Risk-gated action execution by risk level
- Idempotency: Dual-mode memory / Redis, with Lua script atomic lock release for Redis
- Metrics: Built-in Prometheus export (
/metrics), aggregated by action labels - ExecutionHooks:
onActionStart/onActionSuccess/onActionError/onPolicyBlockedobservation callbacks - Timeout:
Promise.raceimplementation, supporting per-action and global defaults
EvmWalletProvider(ethers.js) — full-featured, includingwriteContract/deployContract,getLatestBlockTimestamp()for on-chain readiness checksViemWalletProvider(viem) — modern EVM clientNoopWalletProvider— development/testing placeholder; defaults tochainId 48816(GOAT Testnet3)
All three implement an optional getChainId() so chain-bound actions (e.g. giftcard.payOrder, gns.x402.payOrder) can verify the wallet is on the source chain the order expects, refusing to broadcast on mismatch.
networks/goat/tokens.ts maintains a GOAT chain token mapping table. The wallet.resolve_token action supports operating with symbols (e.g., USDC) directly, without manually looking up contract addresses.
Four independent CLI entry points:
-
npm create goat-agent— interactive project scaffolder (packages/create-goat-agent/), three presets:- minimal — wallet plugin only (10 actions)
- defi — wallet + wgbtc + bridge + bitcoin (27 actions)
- full — the bundled wallet, DeFi, merchant, and identity plugins
-
agentkit-gns(bin/gns.mjs) — register / renew / lookup.goatnames, with--reveal-secrets, region selection, env-driven wallet (GOAT_PRIVATE_KEY), and adoctorsubcommand for env diagnostics. Thex402-registersubcommand additionally accepts--wait-timeout <sec>(default180) and--poll-interval <sec>(default10) for the post-commit reveal-window poll, and preflight-validates(--pay-chain, --pay-token, --pay-token-contract)against the merchant's x402 config before any GOAT-chain spend. -
agentkit-giftcard(bin/giftcard.mjs) — browse brands, place orders, pay cross-chain, poll for fulfillment. Hard-refuses to useNoopWalletProviderfor real payments outsideDEMO_MOCK=true. -
agentkit-paykit— public PayKit discovery, payments, hosted card links and durable recovery (see below).
- Bridge.sol — GOAT native bridge (with full lifecycle: cancel / refund / replace-by-fee)
- LayerZero V2 OFT — general-purpose cross-chain protocol
- High Action density: 129 Actions across 16 plugins covering wallet, DEX, bridge, NFT, governance, payments, merchant management, agent identity, cross-chain,
.goatnaming, and x402 giftcard purchase — surpassing Coinbase Base AgentKit (50+) - x402 payment is the killer feature: Native Agent payment capability with full EIP-712 signing flow + 31-action merchant portal management + a real consumer flow (8-action giftcard purchase paying cross-chain in USDC/USDT), benchmarked against Coinbase but deployed on a Bitcoin L2
- Solid runtime engineering: Idempotency + policy gateway + Prometheus metrics + execution hooks + sensitive-input redaction + revealed-output gating — this is not a demo-grade SDK
- Five-framework one-shot adaptation: OpenAI / LangChain / MCP / Vercel AI / OpenAI Agents — define once, available everywhere
- Developer-friendly: CLI scaffolder (
create-goat-agent) + three end-user CLIs (agentkit-gns,agentkit-giftcard,agentkit-paykit) + complete examples +customActionProvidercustom extensions + dual WalletProvider - Unique Bitcoin ecosystem positioning: Through BitVM2 + Bridge.Sol + BTC light client, building an Agent economy on a Bitcoin L2 — a track that Base AgentKit does not cover
- ERC-8004 Trustless Agent identity +
.goatnaming: On-chain agent registration, metadata, reputation, and a full ENS-style.goatnamespace (commit-reveal registration, profile records, primary names, cross-chain x402-paid registration) — verifiable Agent identity with a human-readable name layer
This integration pins goatflow-paykit@0.4.0, goatflow-sdk@0.3.0, and
goatflow-sdk-server@0.4.0. Contracts were checked against the canonical
GOATNetwork/x402 release (db5be43) and the local fix/paykit-flow source
(c7367ba), with backend behavior checked in goat-x402 (33fe42a).
The local source repositories are reference inputs, not runtime file dependencies.
Use a trusted merchant link such as
https://flow-quickpay.goat.network/paykit/<merchant> (testnet:
https://flow-quickpay.testnet3.goat.network/paykit/<merchant>). Legacy
/quickpay/<merchant>, /agent.md, and /manifest.json links also work. Requests
stay on the link's origin; advertised endpoint URLs and redirects cannot move a
payment to another host. HTTPS is required except on loopback.
| Action | Input amount | Result |
|---|---|---|
goat.paykit.inspect |
None | Tokens, products, supported chains and MPP routes |
goat.paykit.pay |
Human decimal amount |
Public x402 payment confirmation |
goat.paykit.payProduct |
Merchant productKey; price verified from the manifest |
Public x402 payment confirmation |
goat.paykit.sessionStatus |
sessionId or journal operationId; optional original expectedFingerprint with operation ID |
Current public session state; no transfer |
goat.paykit.payMpp |
Manifest route; optional base-unit maxAmountWei cap |
GOAT Flow Payment-Receipt |
goat.paykit.resumeMpp |
Journal operationId, optional original expectedFingerprint |
Verification of the existing transaction; no new payment |
goat.paykit.createCardCheckout |
Product key or human decimal amount | Hosted link with human_action_required: true |
Public x402 success is PAYMENT_CONFIRMED. Native merchant order settlement can
continue to INVOICED; these are distinct state machines. MPP here is the
GOAT Flow MPP profile, not a generic MPP client. Receipt decoding is not
independent signature verification; protected merchant resources verify receipts.
# Read-only; no wallet or merchant credentials required
agentkit-paykit inspect https://flow-quickpay.testnet3.goat.network/paykit/acme --json
# Configure PAYKIT_PRIVATE_KEY and PAYKIT_RPC_4217 (or PAYKIT_RPC_URL) in your environment.
# Use a chain and token returned by inspect. GOAT_PRIVATE_KEY is a key fallback.
agentkit-paykit pay-x402 https://flow-quickpay.testnet3.goat.network/paykit/acme \
--chain-id 4217 --token USDC --amount 1 --idempotency-key invoice-101 --auto-confirm --json
# Check the same payment without submitting it again
agentkit-paykit status https://flow-quickpay.testnet3.goat.network/paykit/acme \
--operation-id invoice-101 --json
agentkit-paykit pay-product https://flow-quickpay.testnet3.goat.network/paykit/acme \
--chain-id 4217 --token USDC --product mug --idempotency-key purchase-102 --auto-confirm
agentkit-paykit pay-mpp https://flow-quickpay.testnet3.goat.network/paykit/acme \
--chain-id 4217 --route GET:api:data --idempotency-key request-103 --auto-confirm --reveal-secrets --json
agentkit-paykit resume-mpp https://flow-quickpay.testnet3.goat.network/paykit/acme \
--operation-id request-103 --reveal-secrets --json
# A human completes this link; the agent never handles card credentials.
agentkit-paykit create-card-checkout https://flow-quickpay.testnet3.goat.network/paykit/acme \
--product mug --idempotency-key card-104 --auto-confirm --reveal-secrets --jsonEach write needs a stable intent key and confirmation (--auto-confirm for unattended
use). The interactive prompt shows the payer address and which key variable supplied
it, the chain, token and amount (or product, route and cap), and the intent key.
Product and MPP prompts read the merchant manifest once before approval to quote the
price; if that quote cannot be loaded, no payment is attempted. The approved payment
runs against that same manifest, so a price the merchant changes after approval fails
the session or challenge terms checks before anything is signed. An intent the journal
already holds as completed or in flight (including a request claimed just before a crash)
is replayed or reported without a quote, so it
does not depend on the merchant being available. --rpc-url overrides RPC environment settings. The private journal lives in
~/.agentkit/paykit, configurable with PAYKIT_STATE_DIR or --state-dir.
For payment commands, --chain-id 4217 selects the SDK action context eip155:4217.
If a replay error names a journaled action context, use that chain ID with --chain-id.
SDK callers must use the lowercase eip155: prefix and a positive decimal chain ID;
mixed-case prefixes, hexadecimal, exponent, whitespace-padded and leading-zero spellings are rejected.
Session handles, checkout URLs and receipt bodies are redacted from normal output (the emergency receipt exception below prevents loss when all storage fails);
--reveal-secrets exposes them to the caller while runtime logs stay redacted.
Exit codes: 0 success, 1 input/confirmation error, 4 policy block, 10 upstream or
recovery-required failure, 99 other runtime errors. Timeout and abort results can
still carry recovery.operationId and a known transaction hash.
If cancellation cleanup finishes a payment and its completed result is durable,
the CLI reports success with exit code 0 and the usual output redaction.
Runtime timeoutMs and CLI --timeout-ms must be integers from 1 through
2,147,483,647 milliseconds; larger values are rejected rather than clamped by Node.
The SDK also accepts timeoutMs: 0 to disable the timeout for a call, overriding
defaultTimeoutMs. A zero or omitted defaultTimeoutMs disables the default;
caller cancellation still applies. The CLI flag requires a positive timeout.
import { PaykitService, createPaykitActions, FilePaymentRecoveryStore } from '@goatnetwork/agentkit/plugins';
const paykit = new PaykitService({
wallet, // EvmWalletProvider or ViemWalletProvider for x402
mppSigner: signer, // ethers signer for MPP, including replacement tracking
recoveryStore: new FilePaymentRecoveryStore('/private/agent-state/paykit'),
allowedChainIds: [4217],
});
for (const action of createPaykitActions(paykit)) provider.register(action);
const result = await runtime.run(
provider.get('goat.paykit.pay'),
{ traceId: 'invoice-101', network: 'eip155:4217', now: Date.now() },
{ url: merchantUrl, chainId: 4217, tokenSymbol: 'USDC', amount: '1', idempotencyKey: 'invoice-101' },
{ confirmed: true, timeoutMs: 120_000 },
);
// On failure retain result.recovery privately; use sessionStatus or resumeMpp.
// For MPP resume, pass expectedFingerprint: result.recovery.fingerprint to pin the intent.The policy allowlist must contain the action context's eip155:<chainId>; GOAT
contexts goat-mainnet and goat-testnet also bind to their actual chain IDs.
The wallet chain, payer and payment terms are checked before sending. A real
broadcastErc20 capability is required; existing transferErc20 methods retain
their wait-for-mining behavior for GNS and giftcard callers. Noop wallets cannot pay.
PayKit, core wallet transfers, approvals, contract writes/deployments, and the legacy EVM payer adapter require a signer capable of signing a populated transaction
without sending it. They prepare, check cancellation, sign, check cancellation, then
broadcast the signed bytes. A wallet exposing only sendTransaction is insufficient. Migrate to a local signer or an account
that supports raw signTransaction; unsupported signers report WALLET_SIGN_FAILED before submission.
Read-only callContract remains available without raw signing.
Ethers NonceManager reservations are preserved for sequential and concurrent writes,
including CommonJS/ESM consumers and calls sharing the manager with ethers directly. As with ethers, a reservation
remains consumed if preparation or signing fails or is cancelled. Reconcile outstanding
transactions before calling NonceManager.reset(); resetting while other reservations
are active can reuse a nonce.
Viem preparation also forwards a local account's configured nonce manager, preserving
its reservations when sharing the account with native viem calls.
Viem confirmation rejects reverted receipts and replacements that change the call.
A successful fee-bump replacement returns and checkpoints its mined hash.
Ethers wallets follow the same rules: ethers v6 rejects wait() even for a successful
fee bump, so AgentKit returns and checkpoints the mined replacement hash instead of
reporting a failure, and attributes cancelled, different-call or reverted
replacements to the replacement hash in recovery data.
Fresh-payment manifest, token/product, amount and memo validation runs before claiming
the key. A journal claim is persisted before a remote creation or transfer.
For x402, card checkout and MPP, an intent reservation and a separate immutable
request claim protect concurrent callers. A matching prepared reservation with
no request evidence can be retried after storage is repaired. Before any creation
request, one caller atomically claims the right to send. The x402 claim also contains
the exact session POST body for status recovery. Once a request claim exists,
retries require reconciliation even if the primary record could not be updated;
they never start another payer or card checkout. A requesting record without
request evidence is not treated as safe to retry. MPP resume consults the same request
marker, so it reports requesting and reconciliation guidance when a challenge/hash
checkpoint was not saved. Completed request snapshots omit creation bodies when read
through the store. File journals retain the older ordinary checkpoint, including its
session POST body; publishing a compact completion file does not erase that data. A repeated key with
different terms is rejected; an uncertain prior result produces RECOVERY_REQUIRED
instead of another transfer or checkout. This remains enforced when runtime retries
are enabled. After a broadcast, status failures, aborts and timeouts preserve the
known hash. The CLI cancels new work at its timeout, then waits for any in-flight
submission and journal writes before reporting recovery and closing the provider.
This cleanup can outlast --timeout-ms. The first Ctrl-C requests cancellation and prints
a draining notice; a second Ctrl-C force exits with code 130. Recover the same intent
from its journal before retrying after a forced exit. Built-in wallets persist signedTxHash
before the broadcast RPC. This identifies the signed transaction even if the response
is lost; it does not prove submission or inclusion. MPP recovery can verify this
candidate hash without sending again. Custom wallets should implement the
onSignedTransaction checkpoint to offer the same protection.
SDK callers also receive a non-serialized result.settled promise after an interrupted
action. Await it before closing the wallet provider to obtain the final recovery data:
if (!result.ok) {
const recovery = result.settled ? await result.settled : result.recovery;
// Save recovery privately and inspect/verify the existing payment.
}Runtime cancellation returns promptly during reads; accepted broadcast RPCs continue
until their hash can be retained. Core wallet write and legacy transfer confirmation errors carry
the already-broadcast hash in error.recovery. Runtime-mode idempotency locks remain held
until execution settles: a valid late success is cached, and a settled failure without
recovery evidence releases the key. A failed execution with a recovery checkpoint retains
its key, including confirmation cancellation after broadcast and failed cache writes.
Repeating that key returns IDEMPOTENCY_CONFLICT instead of executing another transfer.
This guard uses the store's existing TTL and durability; save the returned recovery handle
and reconcile the transaction before retrying, even after the key expires. An in-memory
runtime store does not preserve this guard across a process restart. Configure the idempotency store TTL to cover the full execution and settlement
window; these locks retain the existing TTL contract (default 3,600 seconds).
Runtime completion includes the acquired ownership token. Stores must atomically
cache when the key is absent, expired, or still in progress under that token, while
preserving a different unexpired owner or any unexpired completed result. A late
success with no successor is cached even after its lock expires; it cannot overwrite
a newer run's active lock or cached result. Expiry still allows reacquisition while
an action runs, so choose a TTL that covers the full execution window.
An otherwise retryable timeout can retry if cancellation finishes with failure
during the configured retry backoff and no recovery evidence is present. An
unfinished attempt is never retried concurrently. A validated success observed
during backoff returns success, with one success hook and latency observation. This also
applies when a success is already known at the timeout boundary and retries are disabled.
Unlike earlier timeout behavior, an action that ignores ctx.signal keeps its key
locked until execution settles or the store TTL expires. Its settled promise can
remain pending indefinitely; TTL expiry does not settle or cancel the action.
Action implementations should forward ctx.signal into wallet and network calls.
The built-in wallet write actions and HTTP actions (PayKit, x402, merchant, GNS,
giftcard and faucet) forward cancellation to their adapters. The current wallet
read and typed-data signing interfaces do not accept a signal: contract/balance
reads, chain detection and remote signing can therefore delay settlement in actions
that await them directly. This includes GNS contract preflight, giftcard/GNS payer
checks, and read actions in the wallet, Bitcoin, bridge, DEX, ERC-721, ERC-8004,
GOAT token, LayerZero and WGBTC plugins. Giftcard authentication also awaits its
cache/Redis calls and any shared authentication already in progress without
per-caller cancellation. BitVM2's signal-free deposit/withdraw/status methods are
currently in-process placeholders, not remote bridge requests. Custom adapters
must honor the signal themselves; raw transaction submission and recovery journal
writes intentionally finish after cancellation to retain transaction evidence.
A validated success after the caller has received ABORTED or TIMEOUT emits onActionSuccess and the success log with
afterInterruption: true, plus runtime.success and runtime.late_success metrics.
The earlier abort/timeout error describes the caller's wait; the late event describes
the actual execution outcome, with the same trace and normal output redaction.
Completed x402 journal replay
needs the payer address but does not request the live wallet chain.
Completed MPP replay through payMpp/pay-mpp uses the recorded payment terms,
without fetching the live manifest or asking the provider for its network. A removed,
disabled or re-chained merchant route therefore does not prevent replay. It still
requires an mppSigner with a provider and the original payer address, and enforces
the action-context chain and allowedChainIds. Reading a remote signer's address
may itself need network access. The CLI still requires a valid key and RPC URL
configuration for pay-mpp; resume-mpp needs the key but no RPC URL.
For a lost x402 session-create response, status --operation-id <key> replays the
exact journaled request with its original server idempotency key to recover the
session handle, then reads its status. It needs no wallet and never transfers,
even if the original request never arrived and the replay creates an unpaid session.
Older journal entries without the saved request still need a known session ID or merchant reconciliation.
Keep the same journal and key across restarts; deleting the journal or using a new
key removes this local duplicate-payment protection. A request marker that conflicts
with the primary journal requires reconciliation with the merchant, even if the
primary still says prepared. Do not retry creation or send another payment in
that state. Likewise, a missing or invalid MPP journal chain ID requires restoring
or reconciling the record; retrying the same command cannot repair it.
Native Flow order and checkout creation validate timeout configuration and cancellation
before reserving a reference. A prepared reference remains retryable until a separate
atomic request claim exists. Once that claim exists, reconcile ambiguous creation failures;
only the explicit topup: server replay contract permits another creation request.
Native Flow order and checkout creation also retry a failed completion write once,
without repeating the remote request. If both writes fail, the error retains the
completed order or checkout in recovery.result with a persistenceError diagnostic.
Save that result privately and reconcile the journal before retrying.
A created order must contain exactly one accept entry for the requested chain, amount
and (if given) token contract, and its token_symbol must match the requested symbol.
A returned calldataSignRequest is accepted only for a callback order created with
non-empty calldata: its struct type must match the order flow, payer must equal
fromAddress, orderId and calldataHash must equal the hashes of the order ID and
submitted calldata, and the domain chain must be the order's destination chain.
Its token and amount are pay-to terms and are verified by the server, not locally.
MPP payments require the manifest route to name a valid token contract and reject a challenge for any other token; a route identified only by symbol fails before the intent key is claimed.
The SDK defaults to a single-process InMemoryPaymentRecoveryStore, capped at 10,000
records (configurable in its constructor; all PayKit and native Flow creation rails use two records per intent, including the request claim). At capacity it blocks new intents while
allowing existing recovery and replay; it never evicts keys that prevent duplicate
payments. Completed snapshots drop creation bodies, challenge credentials and decoded
receipt copies. Receipts, checkout links and native order response terms needed for
replay remain available. This limits record count, not the byte size of an individual
server response. Use the file
store for a single host, or implement atomic claim/get/put in a shared durable
PaymentRecoveryStore for multiple workers. get may return undefined or null for
a missing key; a file containing JSON null is still a corrupt record. Completion is terminal: put must preserve
the first completed record across all writers and return that record when ignoring a
stale update. Returned completions must match the caller's intent fingerprint before
they can be adopted. Journal reads validate structure and chain IDs consistently across
built-in and caller-supplied stores: x402 and MPP records require a positive safe
integer chain ID, and MPP records require a valid payer address. A completed primary record must contain its result; only compact
internal request markers may omit it. Corrupt reads return RECOVERY_REQUIRED
without exposing the unvalidated record in recovery data. Restore or reconcile the
journal before retrying; a missing result is never reported as successful replay.
The file store publishes a separate immutable .complete.json file;
get prioritizes it over the ordinary checkpoint, so a delayed payment cannot erase a
receipt saved by recovery. The older checkpoint may retain private request evidence.
Back up the entire journal directory, including completion files; do not mix older
writers that cannot read completion files with this version. Runtime idempotency caching alone does
not substitute for the payment journal. Journaled write actions use idempotencyMode: 'action'
so a runtime cache hit cannot bypass payment-term or key-conflict checks.
Card checkout creation has no server replay
contract; a lost creation response needs merchant reconciliation. MPP recovery
needs the original payer key, challenge and hash; it calls verify only, subject to
the server's expiry/retry budget (at most 16 verification attempts per action).
MPP resume and x402 status recovery check surviving request markers for inconsistent
intents before exposing journaled results or replaying a session-creation POST.
To compare against the original payment terms, pass the original
recovery handle's fingerprint as expectedFingerprint (CLI: --expected-fingerprint).
For x402 status, this requires operationId / --operation-id. The fingerprint is
a deterministic hash of payment terms, not a secret, authentication credential or
unique attempt identifier. It detects changed terms, not a malicious rewrite that preserves them.
A mismatch, including a missing primary or a changed rail, origin or merchant in a
pinned lookup, returns RECOVERY_REQUIRED without exposing the replacement record.
An in-flight x402 primary must have its request marker before operation-ID status
recovery can proceed. Restore the entire journal if that marker is missing; a
completed emergency journal may contain only the primary result. Prepared-primary
marker contradictions and status lookups without recorded session/request evidence
require reconciliation without publishing that primary. These errors identify the
requested operation ID, without claiming a verified phase. Reconcile the journal
with merchant status and transaction history; a previously saved handle or emergency
journal helps if available, but is not assumed to exist.
Operation-ID-only recovery trusts a consistent journal; it cannot detect replacement
of the primary and all corroborating records together. After a journal conflict,
use the saved fingerprint or the emergency journal containing your own result;
do not take the expected fingerprint from the disputed primary record.
Before submission, journal writes must succeed. After a known broadcast, x402 status
polling and MPP verification retry failed checkpoints once and then proceed even if
storage is still unavailable, preserving the transaction evidence and storage diagnostic.
This allows payment confirmation or receipt retrieval while it is still available.
Successful checkpoint writes clear temporary persistence warnings in memory and on
disk, even if payment confirmation later fails. Adopted completions remain terminal
in memory, without repeated polling checkpoints after adoption. Semantically invalid
x402 status data (such as a changed session, chain or token) stops polling with the
specific validation error. Body-read failures, empty/non-JSON bodies and transient
transport failures retain the SDK's bounded retry policy.
Recovery callbacks in the runtime, built-in wallet actions, PayKit and native Flow
receive detached snapshots as best-effort notifications. Synchronous throws and
async rejections are isolated; callbacks are not awaited and cannot block a journal
write or replace a storage error. Use the recovery store for durable persistence;
wallet onSignedTransaction and onBroadcast callbacks remain awaited write boundaries.
Observer accessors are read inside the same isolation boundary, and method observers
retain their original context as this, including on direct native Flow calls.
Invocation does not access an observer's call or bind properties, so callable
proxies and functions with a null prototype are supported too.
Custom-store reads are copied before journal updates, so an in-memory completion
cannot mutate the store's cached record before the completion write succeeds.
Native Flow request cancellation reports ABORTED; unverified-recovery messages use
the caller's order or checkout reference while recovery handles retain the scoped storage key.
PayKit session status and native Flow reads distinguish caller cancellation (ABORTED)
from the payment API's request deadline (ADAPTER_REQUEST_FAILED, “Payment API request timed out”),
both before headers and during JSON body reads. Socket/body-read failures remain upstream
errors; only JSON syntax failures are described as invalid JSON. These transport failures
remain retryable by the x402 SDK's bounded poller. Classified errors reaching AgentKit's
journaled write-failure boundary retain their diagnostic and gain the rail's recovery
guidance when needed, including post-broadcast cancellation and rejected creation responses.
Completed operations, journal reconciliation/conflict errors, and errors explicitly marked
with recoveryGuidance keep their existing instructions instead of receiving an
"operation incomplete" suffix. Read-only failures do not gain write-recovery guidance.
AgentKit's shared JSON response reader reads response text and parses JSON itself, discarding parser exceptions because
their messages or stacks may quote private response contents. It does not call a fetch
implementation's response.json(), which may wrap parser errors under another name.
This sanitization does not cover parsing inside the upstream SDKs (including manifests, MPP challenges and receipts); SDK wrappers
may also replace or suppress the underlying transport/body-read diagnostic.
Saved x402 session-create bodies are checked for valid object JSON and a valid token
address at the journal read boundary, before any recovery POST. Corruption reports
RECOVERY_REQUIRED without attaching the stored request or its parser exception.
PayKit/native Flow journal snapshots omit undefined object fields, including optional
order IDs and nested result fields, and normalize undefined array entries to null
following JSON conventions. An undefined patch field does not erase earlier evidence.
Successful checkpoints omit persistenceError, and compact request markers omit result.
For all three rails, a failed completion checkpoint is retried once. If it still
fails, the operation reports the storage failure and retains its completed result
(x402 payment, card checkout link or verified MPP receipt) and persistence diagnostic
in private recovery data. SDK callers should save that data securely and repair
storage before reuse. An in-memory complete phase alone does not guarantee replay.
If a store returns another intent's completion or an invalid checkpoint after the payment completed, the
operation reports RECOVERY_REQUIRED, preserves only this caller's completed result,
and directs reconciliation; changing the key and paying again is not a remedy.
The CLI reports the persistence error and writes a private emergency journal in a
new directory (directory mode 0700, files 0600). Set --emergency-dir or
PAYKIT_EMERGENCY_DIR to a private parent on a different filesystem from the primary
journal to withstand a full primary filesystem. Provision this parent before use;
on POSIX it must belong to the current user with private permissions (mode 0700).
For payment, checkout creation and MPP resume commands, the CLI rejects a configured
parent equal to or inside the primary journal, resolving symlinks for the comparison.
Applicable key, RPC URL and emergency-directory validation run before any payment
confirmation prompt; an RPC provider is constructed only after approval, and the
only pre-approval request is the product/MPP manifest quote for an interactive prompt. inspect
and status skip emergency-directory preflight because they do not save emergency
results. A shared filesystem produces
a warning rather than rejection: it can help with directory permission failures, but
not disk exhaustion. A parent containing the primary directory is allowed because
the generated emergency copy is a separate sibling directory. That location is tried first, then
the system temporary directory. The temporary parent must already exist and be
accessible; recovery never provisions a missing TMPDIR. On POSIX it must be owned
by the current user or root, with the sticky bit set if group- or world-writable.
The temporary fallback is validated only if the configured location cannot save
the result. An unavailable or unsafe fallback is then skipped with a storage warning;
a successful configured save does not report errors for an unused fallback.
Temporary-directory aliases are resolved for deduplication and volatile-storage
warnings. A configured directory inside the canonical temporary root is also labelled
as temporary, including when reached through a symlink. Temporary storage often shares the primary filesystem;
it is not an independent disk-capacity guarantee. The CLI returns a successfully saved directory
as recovery.emergencyStateDir, with a redacted completed result unless
--reveal-secrets is set. Keep or move this directory to durable private storage;
resume-mpp --state-dir <emergencyStateDir> --expected-fingerprint <originalFingerprint> --reveal-secrets retrieves the completed
receipt without another transfer or verification request. For x402 and card, replay
the original command with this emergency --state-dir, the same key and terms, and
--reveal-secrets to retrieve the saved result without another creation or payment.
If emergency storage also fails, the CLI marks receiptRevealedForRecovery (MPP)
or resultRevealedForRecovery (x402/card) and returns the only result so the operator
can save it. Failed attempt paths are listed in failedEmergencyStateDirs, including
directories that may contain readable partial records after a failed link or fsync;
their durability is unconfirmed. A configuration rejected during recovery is recorded
in emergencyStorageWarnings and skipped; it is not reported as an attempted write.
This exception to default redaction never puts secrets into runtime
logs or hooks.
sessionStatus and resumeMpp have risk level recovery, ranked with low for
confirmation policy and blocked by writeEnabled: false: status recovery may replay
a session-creation POST, and MPP verification consumes the challenge. Neither sends
another transfer. Inspect remains a read action.
On POSIX filesystems, file journals flush completed temporary records, atomically publish claims without replacement, and sync the containing directory. A failed pre-publication write leaves the operation key unused. Only newly created directory entries require parent syncing; existing ancestors need traversal permission, not read access. Provision and persist the state directory before starting concurrent workers; existing ancestors are treated as operator-provisioned durable storage.
Windows defaults to { durability: 'file' }: atomic publication and file flushing
remain enabled, but Node cannot provide directory-entry power-loss durability there.
Select { durability: 'directory' } to require directory syncing, or provide a store
with the necessary platform guarantees. Directory-sync errors are never silently
ignored in that mode. Empty or corrupt published records still fail closed with
RECOVERY_REQUIRED. Keep completed keys
for as long as they can be reused. Archive a journal only when its keys are permanently
retired; automatic TTL/LRU eviction would remove payment deduplication.
An empty PAYKIT_STATE_DIR uses the default ~/.agentkit/paykit; an explicitly blank
directory is rejected instead of resolving to the current working directory.
Journal and emergency directory values are trimmed before resolving paths; interior
spaces in directory names are preserved.
PayKit actions are invoked through ExecutionRuntime.run (or the agent/CLI entry
points that use it). The runtime validates and parses each input once, then passes
that parsed value to the low-level execute callback. Calling execute directly
bypasses runtime validation and policy; direct service methods retain their own
payment preflight checks. The CLI previews input validation before wallet setup,
but passes the original input to the runtime so transformations are not compounded.
Schemas used for CLI preview should keep refinements and transforms pure.
Native Flow and TOTP actions follow the same runtime parsing boundary. Direct
Flow callers should use createOrder and createCheckout, which validate their
inputs; the FromParsedInput methods are low-level entry points for runtime actions.
GoatFlowGatewayAdapter targets https://flow-api.goat.network or
https://flow-api.testnet3.goat.network with an API key and secret. The three new
actions are goat.x402.order.create, goat.x402.checkout.create, and
goat.x402.payment.proof. Its constructor requires a separate trusted
checkoutOrigin before creating hosted links. Pass a durable recovery store for
restart protection, just as for PayKit.
Native order amounts (amountWei, or legacy payment.create.amount) are integer
base units. Native creation requires an explicit payer, source chain and unique
dappOrderId (legacy idempotencyKey); the old to field is not a payer fallback.
On GoatAdapter.x402CreatePayment, to is now optional and deprecated: native
Flow gets its recipient from the merchant configuration and uses fromAddress
as payer. The live smoke script requires GOATFLOW_SMOKE_PAYER and no longer uses
GOATFLOW_SMOKE_TO as a payer. Adapter configuration comparisons retain a digest,
not a serialized extra copy of the API secret.
GoatAdapter.x402CreatePayment now uses this validated adapter. Only order creation
accepts HTTP 402 as success. Missing order IDs and unknown states fail closed.
GoatAdapter retains its recovery store across credential/URL changes and accepts
{ recoveryStore, recoveryNamespace } for durable restart protection. Its default
namespace is per GOAT network; use a stable, separate namespace for each merchant
when sharing a store. Secret rotation preserves existing intent guards. Reusing a
key after changing the API key or origin fails with IDEMPOTENCY_CONFLICT, requiring
reconciliation; it does not silently create another order under the new configuration.
Direct GoatFlowGatewayAdapter callers can opt into the same stable
recoveryNamespace; otherwise their namespace remains tied to origin and API key.
All native Flow and PayKit rails share intent/request claims, completion retry and
request-marker compaction through the same journal implementation.
The custom-route HttpMerchantGatewayAdapter remains available for existing
merchant gateways and is separate from the native HMAC adapter.
Hosted checkout accepts DIRECT/DELEGATE, crypto/fiat rails, decimal prices or
fixed-wei DELEGATE terms, redirects, line items and metadata. Nested values are
JSON strings on the HMAC wire. clientReferenceId is required by AgentKit as a
local operation guard; remotely it is a unique correlation reference, not a
replay guarantee. Order recoverExistingOrder is restricted to topup: references;
it permits exact server-side replay after an ambiguous creation, including when
enabled on a retry. Changing the payment terms still fails locally.
Create-time callback calldata requires fixed-wei DELEGATE terms; a public
callback_template is only a UI hint, so the callback contract must validate buyer
inputs. The server's proof signature is a plain checksum, exposed with
cryptographicAttestation: false; use on-chain verification for independent proof.
The released HMAC format is preserved, including its legacy unescaped k=v&...
encoding. Changing that protocol requires a coordinated backend migration.
Merchant portal bearer authentication remains separate from public PayKit and
merchant HMAC credentials. Self-service registration now permits DIRECT only and
passwords of 8–72 UTF-8 bytes. Login may return { mfa_required, mfa_token };
complete it with goat.x402.merchant.auth.totp-verify and { mfa_token, code }, then
pass the resulting access token through ActionContext.accessToken.