---
name: moove-agentic-payments
description: Accept crypto payments with Moove — create hosted payment links and check whether they have settled. Use when the user asks to take a payment, bill a customer, generate a checkout or payment link, add a "Pay now" button, or check whether a payment has arrived. Covers the Moove Receive Agent (one-off hosted payment links); Send, Swap and Ramp agents are not available yet.
---

# Moove Agentic Payments

Turn "I need to get paid" into a working integration, without writing a payments
integration.

Moove Agentic Payments is a family of agents, one per Moove product. Each gives your
coding agent a narrow, safe capability against a real payments backend:

| Agent | What it does | Status |
| ----- | ------------ | ------ |
| **Moove Receive Agent** | Create one-off hosted payment links and read their status | **Available** |
| Moove Send Agent | Pay out to a @moovehandle, wallet address or DNS alias | Not yet |
| Moove Swap Agent | Swap and bridge across chains | Not yet |
| Moove Ramp Agent | Move between local currency and crypto | Not yet |

**Only the Receive agent exists today.** If the user asks for sending, swapping or
on/off-ramping, say it is not available yet rather than improvising against an endpoint
that does not exist.

## The one rule that matters

**This skill never moves money.** It can *request* a payment; it cannot *make* one. There
is no signing key here, no custody, and nothing you can do with it that drains an
account. If a request needs funds to leave the user's wallet, stop and say so.

## Setup

Two values, both from <https://www.moove.xyz/dashboard/api-keys>:

| Variable             | What it is                                              |
| -------------------- | ------------------------------------------------------- |
| `MOOVE_API_KEY`      | Your API key, `mk_live_…`. Shown **once**, at creation. |
| `MOOVE_API_BASE_URL` | The API host, shown next to the key. Do not guess it.   |

Read both from the environment. **Never write an API key into source code, a committed
config file, a log line, or a message back to the user.** If `MOOVE_API_KEY` is unset,
stop and tell the user to create one — do not proceed with a placeholder.

A key acts as the Moove user who created it and can do strictly less than they can: it
can create and read payment links, nothing else. A lost key is revoked from the same
dashboard page, which kills it instantly.

## The contract, from the API itself

The API publishes its own OpenAPI schema, generated from the live routes rather than
written by hand. Read it before you write a request:

| Source                                              | What it gives you                                                           |
| --------------------------------------------------- | --------------------------------------------------------------------------- |
| `$MOOVE_API_BASE_URL/openapi.json`                  | The machine-readable contract — every published operation, field and error.  |
| `$MOOVE_API_BASE_URL/docs`                          | The same document, rendered to browse.                                       |
| <https://docs.moove.xyz/api-reference/introduction> | The written reference — authentication, errors and rate limits in prose.     |

**Fetch the schema from `$MOOVE_API_BASE_URL`, never from a hardcoded host.** Same rule as
every other call here: the base URL shown with your key names the deployment that key is
valid against, and a schema pulled from a different host describes a different deployment.
Production is `https://api.moove.xyz`.

**Where the schema and this file disagree, the schema wins.** This file is a summary and
can lag a release; the schema is generated at the source and cannot. If a field you want is
not in it, it does not exist — do not send it and hope.

The published document covers the payment-link surface a key can call: `POST /v1/payment-link`
and `GET /v1/payment-link`. Reading one link by id (`GET /v1/payment-link/{id}`, used below)
is served but deliberately left out of the document — it is the hosted checkout page's own
read. Its absence from the schema is not a sign that it was removed.

## Moove Receive Agent — creating a payment link

```bash
curl -sS -X POST "$MOOVE_API_BASE_URL/v1/payment-link" \
  -H "X-API-Key: $MOOVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "toAmount": "49.99",
        "description": "INV-2026-0142",
        "maxUsage": 1,
        "expirationDate": null
      }'
```

| Field            | Required | Notes                                                                    |
| ---------------- | -------- | ------------------------------------------------------------------------ |
| `toAmount`       | yes      | Decimal **string**, not a number. `"49.99"`, never `49.99`.              |
| `description`    | no       | Your reference number. See below — set it.                              |
| `maxUsage`       | no       | `1` for a one-off link. Omit or `null` for no cap.                       |
| `expirationDate` | no       | ISO-8601 UTC, e.g. `"2026-12-31T00:00:00Z"`. `null` means never expires. |

**Always send `toAmount` as a string.** Floating-point money is a bug waiting to happen:
`0.1 + 0.2` is not `0.3`, and a rounding error here is a real mispriced invoice. Keep it a
decimal string end to end and let Moove parse it.

**Set `description` to your own reference number** — order id, invoice number, booking
reference. It is the only field that travels with the link into the owner's Moove
dashboard, so it is how a human reconciles a settled payment back to the order it belongs
to. Use the identifier the user's system already issued; do not mint a new random one.

The response carries the link's `id` and its shareable `url`. Give the user the `url`.

## Checking whether a link has been paid

```bash
curl -sS "$MOOVE_API_BASE_URL/v1/payment-link/$LINK_ID" \
  -H "X-API-Key: $MOOVE_API_KEY"
```

| Status      | Meaning                                                |
| ----------- | ------------------------------------------------------ |
| `active`    | Created, not yet paid.                                 |
| `completed` | Paid and settled. `receivedAmount` holds what arrived. |
| `inactive`  | Deactivated or expired. No longer payable.             |

**Poll politely.** Settlement is usually seconds but depends on the chain the payer chose.
Check on a human timescale — a few seconds between polls, backing off — and give up after
a couple of minutes. Requests are rate limited per key; a tight loop will get you
throttled and will not make the payment arrive sooner.

`active` is not failure. It means nobody has paid yet, which is the normal state of a link
created four seconds ago.

To reconcile several links at once — or to see links the owner created from the dashboard
or with a sibling key — list them instead of polling one id:

```bash
curl -sS "$MOOVE_API_BASE_URL/v1/payment-link?status=completed" \
  -H "X-API-Key: $MOOVE_API_KEY"
```

Newest first, 10 per page; page with `offset`, filter with `status`. This is the operation
the `payment_link:read` scope gates, and it is in the published schema — check there for
the exact response shape rather than assuming it matches the single-link read.

## Errors

| Code                     | What went wrong                                | What to do                                              |
| ------------------------ | ---------------------------------------------- | ------------------------------------------------------- |
| `INVALID_API_KEY`        | Key is wrong, revoked, or the header is missing | Stop. Ask the user to check the key in the dashboard.  |
| `EXPIRED_API_KEY`        | Key is past its expiry date                    | Stop. Ask the user to create a new one.                |
| `INSUFFICIENT_API_SCOPE` | Key lacks `payment_link:create` or `:read`     | Stop. The scope must be granted at creation time.      |

For all three: **stop and report.** Retrying an auth failure never fixes it, and a retry
loop against a revoked key looks like an attack.

A key can only read links its own owner created. A `404` on a link id you did not create
is correct behaviour, not a bug to work around.

## Worked example

> "Bill this customer $250 for invoice 8871 and give me the link."

1. Confirm `MOOVE_API_KEY` and `MOOVE_API_BASE_URL` are set. If not, stop and say so.
2. `POST /v1/payment-link` with `toAmount: "250.00"`, `description: "8871"`, `maxUsage: 1`.
3. Return the `url`, and mention the reference is `8871` so they can find it in their
   dashboard.
4. Only if asked to wait: poll `GET /v1/payment-link/{id}` every few seconds until
   `status` is `completed`, then report `receivedAmount`. Give up after ~2 minutes.

## Rules

- Never print, log, echo, or commit `MOOVE_API_KEY`.
- Never guess `MOOVE_API_BASE_URL`. It is shown with the key.
- Read `$MOOVE_API_BASE_URL/openapi.json` before writing a request. It is the contract;
  this file is a summary of it, and the schema wins where they differ.
- `toAmount` is always a decimal **string**.
- Always set `description` to a reference the user's own system can match.
- Never retry an auth error. Stop and report it.
- Only the Receive agent exists. Say so rather than improvising Send, Swap or Ramp.
- This skill requests payments. It cannot send, swap, or withdraw.
