GOAT Network
AgentKit

x402 Giftcard Purchase

The giftcard plugin is a real consumer flow built on x402: agents pay cross-chain in stablecoins, and the GOAT giftcard backend fulfills the order off-chain (delivers a digital giftcard code by email or hands off to a brand integration). It's a turnkey reference for "Agent pays → user receives off-chain good".

End users buy via the agentkit-giftcard CLI; programmatic consumers use the plugin's 8 actions directly.

Quick Start (CLI)

The CLI bundles the full create-order → pay-order → poll-until-fulfilled sequence into a single buy subcommand. Brand catalogs are region-scoped (the upstream API silently defaults to US if you don't pick one), so the very first step is to discover which regions you can purchase from:

Browse, buy, track
# 0. Discover available regions (e.g. US, CA, ...) and their brand counts
npx -p @goatnetwork/agentkit agentkit-giftcard regions

# 1. List brands in your chosen region (--country is required outside a TTY;
#    in an interactive terminal the CLI prompts you with a region picker)
npx -p @goatnetwork/agentkit agentkit-giftcard list-brands --country US

# 2. Pick a brand UUID and discover its products + face values
#    (get-brand is region-agnostic — brand UUIDs are global)
npx -p @goatnetwork/agentkit agentkit-giftcard get-brand <brand-id>

# 3. Buy (creates order, broadcasts source-chain transfer, polls until FULFILLED)
GOAT_PRIVATE_KEY=0x... \
PAYMENT_CHAIN_RPC_URL=https://bsc-dataseed.bnbchain.org \
PAYMENT_CHAIN_ID=56 \
  npx -p @goatnetwork/agentkit agentkit-giftcard buy \
    --product <product-id> --face-value 25 \
    --pay-chain 56 --pay-token USDT

# 4. (Later) re-check status or list past orders
npx -p @goatnetwork/agentkit agentkit-giftcard status <order-id>
npx -p @goatnetwork/agentkit agentkit-giftcard list-orders

Region matters: the upstream giftcard API silently defaults to US if --country is omitted, which hides catalogs for other regions (e.g. CA). The CLI forces you to pick — either interactively (TTY prompt with US as the default), or via --country <iso2> in non-interactive contexts. Run regions first to see what's available.

Use --reveal-secrets on status / list-orders to print the delivered card_code / card_pin / card_link in the clear (redacted by default).

Actions

ActionNameRiskDescription
listBrandsActiongoat.giftcard.listBrandsreadList supported giftcard brands (filter by country / category / language)
getBrandActiongoat.giftcard.getBrandreadDetails for one brand including products + denominations
listCategoriesActiongoat.giftcard.listCategoriesreadBrand category taxonomy
listSupportedTokensActiongoat.giftcard.listSupportedTokensreadPer-chain supported ERC-20 payment tokens
createOrderActiongoat.giftcard.createOrdermediumCreate a giftcard order. Requires product_id (UUID), face_value, pay_chain_id, pay_token_symbol, and an idempotency_key matching ExecutionOptions.idempotencyKey
payOrderActiongoat.giftcard.payOrderhighBroadcast the source-chain ERC-20 transfer that funds the order
getOrderActiongoat.giftcard.getOrderreadStatus snapshot (lifecycle below)
listOrdersActiongoat.giftcard.listOrdersreadList the agent's past orders

Supported Source Chains

The giftcard backend accepts cross-chain payments from:

ChainChain IDCommon Stablecoins
GOAT mainnet2345USDC, USDT (default — no PAYMENT_CHAIN_* env vars needed)
Polygon137USDC, USDT
Base8453USDC
Arbitrum42161USDC, USDT
Optimism10USDC, USDT
BSC56USDT (18 decimals — different from the 6-decimal USDC/USDT on other chains)
Metis1088USDC, USDT

Use goat.giftcard.listSupportedTokens for the authoritative per-chain token addresses + decimals at runtime.

Order Lifecycle

The full GiftcardOrderStatus enum:

Giftcard order lifecycle
CREATED              ← order accepted by backend, awaiting agent payment
        ↓ (backend transitions on payment readiness)
PAYMENT_PENDING      ← ready to receive the source-chain ERC-20 transfer
        ↓ (agent calls payOrder)
PAYMENT_CONFIRMED    ← chain watcher saw the transfer

FULFILLING           ← brand integration in flight

FULFILLMENT_POLLING  ← intermediate fulfillment poll (some brands)

FULFILLED            ← giftcard delivered (card_code / card_pin / card_link available)

Terminal failure / refund branch:
    FAILED, EXPIRED, CANCELLED       — paid-or-unpaid order ended without delivery
    REFUNDING → REFUNDED             — backend issued a refund to the source-chain payer
    REFUND_FAILED                    — refund attempt failed; contact support

The giftcard lifecycle includes PAYMENT_PENDING as a real state — distinct from the GNS x402 lifecycle, which never emits PAYMENT_PENDING. The two flows hit different backend services with different state machines; do not conflate them.

Architecture

Giftcard plugin building blocks
plugins/giftcard/
├── actions/                       # 8 actions above
└── adapters/
    ├── http-giftcard-api.ts       # HttpGiftcardGatewayAdapter — the HTTP client
    ├── eip712-auth-signer.ts      # Backend session auth (Redis-backed single-flight)
    └── types.ts                   # GiftcardOrderStatus, GiftcardBrand, ...

EIP-712 auth + Redis single-flight

Eip712AuthSigner obtains a session token from the giftcard backend by signing an EIP-712 typed message with the agent's wallet. Under concurrent load (e.g. two agents from the same wallet starting at the same time), it uses single-flight to ensure only one token request goes to the backend per (wallet, expiry-bucket) — duplicate concurrent callers wait for the in-flight request and share the result. Single-process callers (the default — including the CLI) use an in-memory single-flight; multi-process / multi-instance deployments can inject a GiftcardRedisClient into HttpGiftcardGatewayAdapter via the redis option for distributed deduplication.

The auth-signer constructor takes a WalletProvider (the wallet that signs the auth message) plus an optional Eip712AuthSignerOptions for domain override.

Safety posture (payOrder / CLI buy)

payOrder is a money path with multiple defenses:

  • Lifecycle gate: explicit status === 'PAYMENT_PENDING'. Refuses to double-pay any non-pending order (FAIL-CLOSED on missing / empty status to avoid truthy-gate bypass).
  • Chain binding: wallet.getChainId() === order.pay_chain_id (else INVALID_INPUT('chain_mismatch')); refuses to broadcast on the wrong chain.
  • Expiry safety margin: refuses to pay inside the last 30 seconds before expire_at (else the transfer could land after expiry).
  • NoopWalletProvider refusal: the CLI hard-refuses to run buy against a NoopWalletProvider outside DEMO_MOCK=true. A real signer is mandatory for any production purchase.
  • Per-orderId single-flight: module-scope Map dedupes concurrent payOrder calls for the same order_id (double-pay TOCTOU defense).
  • Idempotency-key contract: createOrder.idempotency_key MUST equal the ExecutionOptions.idempotencyKey passed to runtime.run(...); the action rejects with INVALID_INPUT before any HTTP traffic on mismatch, so the runtime cache short-circuit and the backend dedup both line up.

Programmatic Usage

Buy a giftcard via the SDK
import { randomUUID } from 'node:crypto';
import { JsonRpcProvider, Wallet } from 'ethers';
import { ActionProvider, ExecutionRuntime, PolicyEngine } from '@goatnetwork/agentkit';
import { EvmWalletProvider } from '@goatnetwork/agentkit/core';
import {
  HttpGiftcardGatewayAdapter,
  Eip712AuthSigner,
  listBrandsAction,
  getBrandAction,
  createOrderAction,
  payOrderAction,
  getOrderAction,
} from '@goatnetwork/agentkit/plugins';

// Wire up the source-chain wallet (the one that broadcasts the ERC-20 transfer).
const rpc = process.env.PAYMENT_CHAIN_RPC_URL!;
const signer = new Wallet(process.env.GOAT_PRIVATE_KEY!, new JsonRpcProvider(rpc));
const wallet = new EvmWalletProvider(signer, new JsonRpcProvider(rpc), 'source');

// Auth signer for the giftcard backend (EIP-712). Pass a WalletProvider.
const authSigner = new Eip712AuthSigner(wallet);
const gateway = new HttpGiftcardGatewayAdapter({
  baseUrl: process.env.GIFTCARD_API_BASE_URL ?? 'https://giftcard-api.goat.network',
  authSigner,
});

const provider = new ActionProvider();
provider.register(listBrandsAction(gateway));
provider.register(getBrandAction(gateway));
provider.register(createOrderAction(gateway));
provider.register(payOrderAction(gateway, wallet));
provider.register(getOrderAction(gateway));

const policy = new PolicyEngine({
  allowedNetworks: ['source'],
  maxRiskWithoutConfirm: 'low',
  writeEnabled: true,
});
const runtime = new ExecutionRuntime(policy, { maxRetries: 0, retryDelayMs: 200 });
const ctx = { traceId: 'gift-1', network: 'source', now: Date.now() };

// 1. Discover brands → pick a product UUID from getBrand.products[]
const brands = await runtime.run(provider.get('goat.giftcard.listBrands'), ctx, {});
const brand = await runtime.run(provider.get('goat.giftcard.getBrand'), ctx, {
  brand_id: '<brand-id>',
});
const product = brand.output!.products[0]; // pick one

// 2. Create an order. idempotency_key MUST match ExecutionOptions.idempotencyKey.
const idemKey = randomUUID();
const order = await runtime.run(
  provider.get('goat.giftcard.createOrder'),
  ctx,
  {
    product_id: product.id,
    face_value: 25,
    pay_chain_id: 56,            // BSC
    pay_token_symbol: 'USDT',
    idempotency_key: idemKey,
  },
  { confirmed: true, idempotencyKey: idemKey },
);

// 3. Pay (broadcast the source-chain transfer).
//    NOTE the input field is `orderId` (camelCase) — payOrder + getOrder
//    both take camelCase input keys even though createOrder + the output
//    OrderRecord shape use snake_case (`order_id`). This mismatch is real;
//    don't pass `order_id` to payOrder/getOrder or Zod will reject with
//    INVALID_INPUT before any HTTP traffic.
const paid = await runtime.run(
  provider.get('goat.giftcard.payOrder'),
  ctx,
  { orderId: order.output!.order_id },
  { confirmed: true },
);

// 4. Poll status until FULFILLED (or a terminal failure / refund state)
const TERMINAL = new Set(['FULFILLED', 'FAILED', 'CANCELLED', 'EXPIRED', 'REFUNDED', 'REFUND_FAILED']);
const deadline = Date.now() + 5 * 60_000;
while (Date.now() < deadline) {
  const snap = await runtime.run(provider.get('goat.giftcard.getOrder'), ctx, {
    orderId: order.output!.order_id,
  });
  const status = (snap.output as { status: string }).status;
  console.log('status:', status);
  if (TERMINAL.has(status)) break;
  await new Promise((r) => setTimeout(r, 5_000));
}

Environment Variables

VariableRequiredDescription
GOAT_PRIVATE_KEYYesWallet key that signs EIP-712 auth and (when paying on GOAT) broadcasts the source-chain transfer
PAYMENT_CHAIN_RPC_URLWhen --pay-chain is non-GOATRPC URL of the source chain you're paying from
PAYMENT_CHAIN_IDWhen --pay-chain is non-GOATNumeric chain id of the source chain (sanity check)
PAYMENT_CHAIN_PRIVATE_KEYOptionalSource-chain signer key; falls back to GOAT_PRIVATE_KEY when omitted
GIFTCARD_API_BASE_URLOptionalOverride default https://giftcard-api.goat.network
GIFTCARD_AUTH_DOMAIN_NAME / _VERSION / _CHAIN_IDOptionalEIP-712 auth domain override
DEMO_MOCKOptionaltrue allows the CLI to run buy --dry-run with a NoopWalletProvider for shape demos only

Redis-backed single-flight for the auth signer is enabled by injecting a GiftcardRedisClient into HttpGiftcardGatewayAdapter (the redis option), not via an environment variable. The CLI runs with the in-process fallback; production deployments that need cross-process dedup pass a Redis client to the adapter constructor directly.

Examples

  • CLIsagentkit-giftcard flags and env vars
  • Plugins Reference — full inventory of all 15 plugins
  • x402 Payments — the underlying agent payment protocol
  • GNS — the other consumer flow (.goat name registration paid cross-chain via x402)

On this page