Hkchain

Case 2 — Agentic Payment via HTTP 402

Scenario

An AI agent, operating on behalf of a KYC'd retail user, calls a paid data service. The service returns HTTP 402 with a payment envelope. The agent signs an on-chain HKD transfer; the service verifies inclusion and returns the data.

To demonstrate chain-level enforcement, the case shows two agents — both sub-keys bound to a KYC'd primary identity (single sub-role model: agents carry no KYC of their own; identity is parent-inherited at every gate):

  • Agent A, bound to a retail primary in good standing: succeeds end to end.
  • Agent B, bound to a primary on the sanctions list: fails at the chain level, not at the service. KycGate parent-resolves the signer to the blacklisted primary; the service does not need to implement sanction logic — the rail rejects it.

Why this matters

Agentic applications need payment rails that combine two properties:

  1. Micro-transaction granularity — pay-per-request is a natural unit for AI agents
  2. Compliance-bound identity — each agent action carries a verifiable identity, a KYC level, and an audit trail

Existing Web2 payment rails (Stripe, etc.) offer micro-transaction support but treat the compliance surface as the service's problem. Existing crypto rails offer the inverse: blockchains enforce custody but treat identity as out of scope. Hkchain gives both: HKD as a compliant-by-construction settlement primitive.

For HKMA, the meaningful framing: "every AI-agent payment on HKD rails is auditable at the micro-transaction level, and compliance is enforced by the rail, not by the service provider." This is what makes agentic commerce on HKD a regulable ecosystem rather than an emergent gray zone.

Actors

ActorRoleIdentity
User AliceEnd user delegating authority to her agentRetail L3 primary, KYC'd by HSBC
Agent AAlice's LLM-driven agentSub-key bound to Alice (no own KYC; identity inherited from parent)
MallorySanctioned principal (blacklisted by compliance authority)L0 blacklisted primary
Agent BMallory's agentSub-key bound to Mallory (no own KYC; KycGate parent-resolves to L0 → reject)
Data ServicePaid-per-request data providerL4 institutional address (receives payments)
Chain (hkchaind)Payment rail + compliance gate—

Note on agent identity: MVP follows the single sub-role model. Agent EOAs are sub-keys bound to a KYC'd primary; sub-keys carry no KYC record of their own. Every identity-touching decorator (KycGate, Sanctions, TravelRule, Threshold) parent-resolves the signer before any KYC lookup. Authority bounds (per-tx cap, daily cap, revocation) live in the AgentPolicy envelope on top of that substrate; see Roles and permissions.

Setup — Alice binds an agent sub-key

Before any 402 activity, Alice establishes her agent on chain:

MsgBindSubRole {
  parent: hkchain1alice…,
  sub_addr: hkchain1agentA…,
  role: agent,
  label: "market-data assistant",
  policy_envelope: {
    daily_cap:  10000,   // HKD 100.00/day
    per_tx_cap: 1000,    // HKD 10.00/tx
    revoked:    false
  }
}

Signed by Alice's primary key. AgentBound event emitted. The sub-key hkchain1agentA… can now sign MsgSend up to the limits in the envelope.

Flow — Agent A (succeeds)

Step 1 — Agent attempts the call

Agent A → GET https://data.example/v1/market-snapshot
          Authorization: none

Step 2 — Service returns 402 with payment envelope

HTTP/1.1 402 Payment Required
Content-Type: application/json
X-Payment-Method: x402

{
  "payment": {
    "chain": "hkchain-localnet-1",
    "denom": "hkd.hsbc",
    "amount": "10",               // HKD 0.10, below the HKD 8k enhanced threshold
    "to": "hkchain1dataservice…",
    "nonce": "req-a-7f3c9e",
    "expiry": "2026-04-19T10:45:00Z",
    "resource": "/v1/market-snapshot"
  },
  "travel_rule_required": "minimal"
}

Step 3 — Agent signs and broadcasts the tx

The agent constructs:

MsgSend {
  from: hkchain1agentA…, to: hkchain1dataservice…, denom: hkd.hsbc, amount: 10
}
tx extension: TravelRulePayload {
  originator: { vasp: HSBC, entity_ref: ALICE-007, agent_ref: AGENT-A },
  beneficiary: { vasp: Anchorpoint, entity_ref: DATASERVICE-001 },
  purpose: "402:/v1/market-snapshot:nonce=req-a-7f3c9e"
}

Broadcasts via JSON-RPC.

Step 4 — AnteHandler

DecoratorResult
KycGatepass (agent sub-key → parent Alice L3, service L4)
Sanctionspass
TravelRulepass (minimal payload ok for 0.10)
Thresholdpass
Velocitypass
RoleGatepass (agent allowlist: MsgSend ✓)
AgentPolicypass (0.10 ≤ per_tx_cap 10.00; 24h sum ≤ daily_cap 100.00); counter ticks up

Transfer executes. tx_hash emitted.

Step 5 — Agent retries the data request

Agent A → GET /v1/market-snapshot
          X-Payment-Tx: 0xa1b2c3…

Step 6 — Service verifies and returns data

Service queries hkchaind: is the tx included, does it send the expected amount to the expected address with a matching nonce purpose, was it not reverted? All yes — return the resource.

Agent-a's accepted payment tx on /explorer/tx/<hash> after scripts/demo-402.sh runs end-to-end. Sub-key signs MsgSend for 500 fen.hsbc (HKD 5) with a TravelRulePayload extension; all seven decorators pass green; header chip: ACCEPTED; charging-station controller releases the session. Live capture against the seeded localnet — narrative actor names ("Bob's payment agent" / Alice's agent) abstract over the seed cast (payer-good L2 retail-basic).
Agent-a's accepted payment tx on /explorer/tx/<hash> after scripts/demo-402.sh runs end-to-end. Sub-key signs MsgSend for 500 fen.hsbc (HKD 5) with a TravelRulePayload extension; all seven decorators pass green; header chip: ACCEPTED; charging-station controller releases the session. Live capture against the seeded localnet — narrative actor names ("Bob's payment agent" / Alice's agent) abstract over the seed cast (payer-good L2 retail-basic).

Elapsed time

Typically under 3 seconds end to end on localnet.

Flow — Agent B (blocked at the chain)

Step 1 — Agent B attempts the call

Agent B → GET /v1/market-snapshot

Step 2 — 402 response (identical)

Step 3 — Agent B constructs and broadcasts the tx

MsgSend { from: hkchain1agentB…, … }

Step 4 — AnteHandler

DecoratorResult
KycGatefail — sub-key parent-resolves to Mallory (L0 blacklisted)
(subsequent decorators not run)—

Transaction rejected at inclusion with error: "signer=hkchain1agentB… parent=hkchain1mallory…: address is blacklisted".

Step 5 — Agent B attempts data retry

Agent B → GET /v1/market-snapshot
          X-Payment-Tx: 0xdeadbeef…   (either a fake hash or the rejected tx's hash)

Step 6 — Service queries the chain

The service queries hkchaind for the tx hash. If the hash doesn't exist (rejected before inclusion, no block record), service responds:

HTTP/1.1 402 Payment Required
X-Payment-Error: payment-not-verified

Alternatively, if the service is wired to check rejection traces (via the tx_trace endpoint), it can return:

HTTP/1.1 402 Payment Required
X-Payment-Error: payment-rejected-by-compliance
X-Payment-Error-Detail: "KycGate: sender blacklisted"

Either way, no data is served. The critical point: the service did not need its own blacklist logic. The rail handled it.

Agent-b's rejected tx on /explorer/tx/<hash> after the same demo-402 run. KycGate FAIL (red) with reason signer=<agent-b> parent=<payer-bad>: address is blacklisted; subsequent decorators not run; header chip: REJECTED. The rejected tx isn't in the cosmos.tx index (CheckTx short-circuit) — the trace surface is the trace ring buffer (256 slots / ~1h TTL) or the simulate_trace fallback. The service's 402 verification fails by absence of payment, not by a service-side blacklist call.
Agent-b's rejected tx on /explorer/tx/<hash> after the same demo-402 run. KycGate FAIL (red) with reason signer=<agent-b> parent=<payer-bad>: address is blacklisted; subsequent decorators not run; header chip: REJECTED. The rejected tx isn't in the cosmos.tx index (CheckTx short-circuit) — the trace surface is the trace ring buffer (256 slots / ~1h TTL) or the simulate_trace fallback. The service's 402 verification fails by absence of payment, not by a service-side blacklist call.

What each role sees

  • Service operator: sees a successful 402 flow (Agent A) and a failed payment verification (Agent B). Optionally integrates tx_trace for structured error messages. Does not operate a blacklist.
  • HSBC Compliance: sees routine micro-transaction flow for Agent A (L3 user's agent). If the blacklisting event for Agent B was originally triggered by earlier activity from HSBC's compliance team, they see the lineage.
  • HKMA Supervisor: sees aggregate data on agentic payment flow in the compliance event feed. Can filter the audit log by purpose: "402:*" to get an aggregate view of AI-economy activity on HKD rails.
  • Alice (Agent A's principal): sees her balance decrement by HKD 0.10 for each paid request. Optionally can review her agent's spending.

Demo specifics

The demo is orchestrated as a split-screen browser view:

  • Left: Agent A's terminal streaming request → 402 → payment → data. Fast. Green.
  • Right: Agent B's terminal streaming request → 402 → payment → REJECTED AT CHAIN. Red.
  • Top: Compliance Dashboard showing both events in the live feed, with click-through to the AnteHandler trace visualization for each.

Flow — Revocation (the killer moment, must-show #9)

At some point, Alice decides to revoke her agent. Perhaps the agent's job is done; perhaps she noticed spending faster than expected; perhaps there's a security concern. She signs:

MsgRevokeSubRole {
  parent: hkchain1alice…,
  sub_addr: hkchain1agentA…
}

Signed by Alice's primary key. Included in the next block. EventSubRoleRevoked (with role=SUB_ROLE_AGENT) emitted from x/compliance; the SubRoleBinding.RevokedAt tombstone is set, and on agent bindings the corresponding AgentPolicy.Revoked flips to true in the same handler. RoleGate honors the tombstone immediately; AgentPolicy keys off the envelope flag — defense-in-depth across two gates.

Agent A, unaware of the revocation (it may still be mid-request), next tries to pay:

MsgSend { from: hkchain1agentA…, to: hkchain1service…, denom: hkd.hsbc, amount: 10 }

AnteHandler:

DecoratorResult
KycGatepass
Sanctionspass
TravelRulepass
Thresholdpass
Velocitypass
RoleGateFAIL — "binding revoked"
AgentPolicy(not reached — RoleGate short-circuits first)

RoleGate's IsBindingActive check rejects any sub-role tx whose binding has a non-zero RevokedAt, regardless of the role — so revocation lands one gate earlier than the policy envelope. AgentPolicy's policy.Revoked flag is the second line of defense (caught in unit tests against the decorator in isolation; in production the trace recorder shows RoleGate FAIL and AgentPolicy not run). Service's 402 verification fails either way.

Narrative: "Alice's revocation is enforced by the chain within one block. No service provider had to check. No agent could bypass. No policy layer outside the chain was involved. The user's control over her agent is a chain primitive."

This is demonstrated live as must-show #9 by scripts/demo-agent-revocation.sh, which drives a four-act sequence against SEED_T15=1 localnet: within-cap accept → over-per_tx_cap reject → cumulative-over-daily_cap reject → revocation → post-revoke reject. See docs/design/05-mvp-scope-and-roadmap.md for scope.

Act-4 post-revocation tx on /explorer/tx/<hash>. Six green PASS rows — KycGate · Sanctions · TravelRule · Threshold · Velocity — then RoleGate FAIL (red) with reason Role-gate rejected: signer=<agent> binding revoked: sub-role signer not allowed to sign this message type. AgentPolicy is not reached because RoleGate short-circuits the chain — defense-in-depth holds: the binding-revoked tombstone catches the tx one gate before the policy envelope would. Header chip: REJECTED, captured from the trace ring buffer (rejected tx not indexed at cosmos.tx layer).
Act-4 post-revocation tx on /explorer/tx/<hash>. Six green PASS rows — KycGate · Sanctions · TravelRule · Threshold · Velocity — then RoleGate FAIL (red) with reason Role-gate rejected: signer=<agent> binding revoked: sub-role signer not allowed to sign this message type. AgentPolicy is not reached because RoleGate short-circuits the chain — defense-in-depth holds: the binding-revoked tombstone catches the tx one gate before the policy envelope would. Header chip: REJECTED, captured from the trace ring buffer (rejected tx not indexed at cosmos.tx layer).

Forward-looking notes (not in MVP)

  • Richer policy envelope: time windows (business hours only), merchant whitelists, velocity curves, per-denom caps. Each a separate future decision.
  • Re-bind after revocation: MVP revokes one-way (new bind requires new sub-addr). A grace-period re-bind pattern is roadmap.
  • Reputation: each agent key accrues on-chain behavior; good agents earn better terms (lower friction); bad agents hit thresholds and lose capability. AML/CFT infrastructure becomes reputation infrastructure.
  • AI-as-reviewer (different role entirely): see docs/future/ai-agent-compliance-review.md.
  • Cross-service pay-as-you-go: the same 402 pattern works for any resource provider — data APIs, third-party LLM gateways, on-chain compute. HKD becomes the unit of account for AI economy activity on the Hong Kong rail.