Source Code

;; Agent Payments - Escrow & settlement for agent-to-agent STX payments
;; Flow: caller deposits STX -> gateway verifies service -> release to provider
;; Integrates with agent-registry for reputation tracking.

(define-constant CONTRACT_OWNER tx-sender)

;; -- errors ---------------------------------------------
(define-constant ERR_OWNER_ONLY     (err u400))
(define-constant ERR_NOT_FOUND      (err u401))
(define-constant ERR_INSUFFICIENT   (err u402))
(define-constant ERR_ALREADY_SETTLED (err u403))
(define-constant ERR_UNAUTHORIZED   (err u404))
(define-constant ERR_EXPIRED        (err u405))
(define-constant ERR_AGENT_INACTIVE (err u406))

;; Payment expires after ~144 blocks (~24h at 10min/block)
(define-constant PAYMENT_TTL u144)

;; -- state ----------------------------------------------
(define-data-var next-payment-id uint u1)

;; payment-id -> payment data
(define-map payments uint {
  caller:      principal,          ;; agent paying
  provider:    principal,          ;; agent receiving
  agent-id:    uint,               ;; provider's agent-id in registry
  amount:      uint,               ;; micro-STX
  status:      (string-ascii 10),  ;; "pending", "completed", "refunded"
  created-at:  uint                ;; block height
})

;; -- budget tracking (per principal per day-ish) --------
;; Simple daily budget: reset every ~144 blocks
(define-map budgets principal {
  daily-limit:  uint,              ;; max micro-STX per period
  spent:        uint,              ;; current period spending
  period-start: uint               ;; block height when period started
})

;; -- set budget (agent owner sets their own) ------------
(define-public (set-budget (daily-limit uint))
  (begin
    (map-set budgets tx-sender {
      daily-limit:  daily-limit,
      spent:        u0,
      period-start: block-height
    })
    (print { event: "budget-set", owner: tx-sender, limit: daily-limit })
    (ok true)
  )
)

;; -- create payment (caller pays STX into contract) -----
(define-public (create-payment (agent-id uint))
  (let (
    (agent (unwrap! (contract-call? .agent-registry get-agent agent-id) ERR_NOT_FOUND))
    (amount (get price-per-call agent))
    (id (var-get next-payment-id))
  )
    ;; Agent must be active
    (asserts! (get active agent) ERR_AGENT_INACTIVE)

    ;; Budget check
    (match (map-get? budgets tx-sender)
      budget
        (let (
          (period-age (- block-height (get period-start budget)))
          (current-spent (if (>= period-age PAYMENT_TTL)
                           u0                               ;; period reset
                           (get spent budget)))
          (new-spent (+ current-spent amount))
        )
          (asserts! (<= new-spent (get daily-limit budget)) ERR_INSUFFICIENT)
          (map-set budgets tx-sender {
            daily-limit:  (get daily-limit budget),
            spent:        new-spent,
            period-start: (if (>= period-age PAYMENT_TTL)
                            block-height
                            (get period-start budget))
          })
        )
      true  ;; no budget set = unlimited
    )

    ;; Transfer STX from caller to this contract
    (try! (stx-transfer? amount tx-sender (as-contract tx-sender)))

    ;; Record payment
    (map-set payments id {
      caller:      tx-sender,
      provider:    (get owner agent),
      agent-id:    agent-id,
      amount:      amount,
      status:      "pending",
      created-at:  block-height
    })
    (var-set next-payment-id (+ id u1))
    (print { event: "payment-created", id: id, caller: tx-sender, agent-id: agent-id, amount: amount })
    (ok id)
  )
)

;; -- complete payment (release to provider) -------------
;; Called by gateway/admin after service delivery confirmed
(define-public (complete-payment (payment-id uint))
  (let ((pmt (unwrap! (map-get? payments payment-id) ERR_NOT_FOUND)))
    ;; Only contract owner (gateway) can complete
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED)
    (asserts! (is-eq (get status pmt) "pending") ERR_ALREADY_SETTLED)

    ;; Transfer STX from contract to provider
    (try! (as-contract (stx-transfer? (get amount pmt) tx-sender (get provider pmt))))

    ;; Update status
    (map-set payments payment-id (merge pmt { status: "completed" }))

    ;; Record success in registry
    (try! (contract-call? .agent-registry record-call-success
      (get agent-id pmt) (get amount pmt)))

    (print { event: "payment-completed", id: payment-id, provider: (get provider pmt), amount: (get amount pmt) })
    (ok true)
  )
)

;; -- refund expired payment (caller reclaims) -----------
(define-public (refund-payment (payment-id uint))
  (let ((pmt (unwrap! (map-get? payments payment-id) ERR_NOT_FOUND)))
    (asserts! (is-eq (get status pmt) "pending") ERR_ALREADY_SETTLED)
    ;; Only caller or admin can refund
    (asserts! (or (is-eq tx-sender (get caller pmt))
                  (is-eq tx-sender CONTRACT_OWNER)) ERR_UNAUTHORIZED)
    ;; Must be expired
    (asserts! (>= (- block-height (get created-at pmt)) PAYMENT_TTL) ERR_EXPIRED)

    ;; Refund STX to caller
    (try! (as-contract (stx-transfer? (get amount pmt) tx-sender (get caller pmt))))

    ;; Update status
    (map-set payments payment-id (merge pmt { status: "refunded" }))

    ;; Record failure in registry
    (try! (contract-call? .agent-registry record-call-failure (get agent-id pmt)))

    (print { event: "payment-refunded", id: payment-id, caller: (get caller pmt), amount: (get amount pmt) })
    (ok true)
  )
)

;; -- read-only ------------------------------------------
(define-read-only (get-payment (payment-id uint))
  (map-get? payments payment-id)
)

(define-read-only (get-budget (owner principal))
  (map-get? budgets owner)
)

(define-read-only (get-total-payments)
  (- (var-get next-payment-id) u1)
)

Functions (7)

FunctionAccessArgs
set-budgetpublicdaily-limit: uint
create-paymentpublicagent-id: uint
complete-paymentpublicpayment-id: uint
refund-paymentpublicpayment-id: uint
get-paymentread-onlypayment-id: uint
get-budgetread-onlyowner: principal
get-total-paymentsread-only