Modo demo: una tienda de referencia para operadores de eSIM. Precios reales, pagos desactivados. Lanza tu propio operador →
Travel eSIM · demo
Español

Guía para agentes

Contenido

Esta guía es para desarrolladores de agentes de IA que reservan viajes, planifican itinerarios o hacen recados. Tu agente puede encontrar una eSIM de datos para un país, comprarla con un solo pago x402 en USDC en Base y entregar al viajero un código QR. Sin cuenta, sin clave API, sin página de pago.

Cada vendedor que usa el kit de operador eSIM responde a las mismas tres llamadas en https://<dominio del vendedor>/agents/. Para probarlas ahora, usa el nodo demo https://modelmarket.dev/providers/esim: da precios reales basados en el mayorista y rechaza los pedidos con demo_mode.

Otros documentos: resumen · guía del operador · incorporación · integraciones · SDK

Las tres capacidades

CapacidadRutaPrecioHace
esim.quote@v1POST /agents/quotegratisOfertas que cubren un país, de la más barata a la más cara. También cuentan los planes regionales y globales que incluyen el país.
esim.order@v1POST /agents/orderel precio de la oferta, x402Compra una eSIM. Responde con el código de activación, el enlace al QR, el enlace de instalación, el ICCID y un token de estado.
esim.status@v1POST /agents/statusgratisEstado, datos usados, caducidad y de nuevo el código de activación, para quien tenga el token de estado.

GET /agents/listing.json describe las tres, con la clave pública del vendedor.

1. Encuentra un vendedor

2. Pide ofertas

curl -s -X POST https://modelmarket.dev/providers/esim/quote \
  -H 'content-type: application/json' \
  -d '{"country":"FR","min_gb":3,"min_days":7,"limit":3}'
EntradaTipo
countrycadena, obligatoriaCódigo ISO 3166-1 alfa-2 del destino: FR, JP, US.
min_gbnúmeroDatos mínimos.
min_daysenteroValidez mínima.
max_usdnúmeroPrecio máximo.
limitentero, de 1 a 50Cuántas ofertas.

Envía siempre min_gb y min_days del viaje. Sin ellos, la oferta más barata suele ser un plan de 0,5 GB para un día.

La respuesta (una oferta, listas acortadas):

{
  "success": true,
  "result": {
    "seller": { "name": "modelmarket.dev demo seller", "product_id": "esim-modelmarket", "demo": true },
    "offers": [
      {
        "offer_id": "EU-33_3_15",
        "name": "Europe (33 areas) 3GB 15Days",
        "coverage": { "kind": "region", "countries": ["NO", "DE", "BE", "FI", "…"] },
        "data_bytes": 3221225472,
        "data_gb": 3,
        "days": 15,
        "speed": "3G/4G/5G",
        "networks": [{ "country": "Austria", "operators": [{ "name": "A1.net", "type": "5G" }] }],
        "top_up": true,
        "unused_valid_days": 180,
        "price": { "usd": "2.03", "currency": "USDC" },
        "usd_per_gb": 0.68
      }
    ],
    "order": { "url": "https://modelmarket.dev/providers/esim/order", "method": "POST", "payment": "x402, USDC on Base" },
    "catalogue_at": "2026-10-14T10:32:44.619Z"
  }
}

3. Comprueba la firma del vendedor (opcional)

Las respuestas gratuitas llevan X-Provider-Signature: una firma Ed25519 en base64 sobre el JSON canónico de {capability_id, product_id, input_sha256, result}. La forma canónica tiene las claves ordenadas, sin espacios y los caracteres no ASCII tal cual. input_sha256 es el SHA-256 en hex de la entrada canónica. La clave pública y product_id están en /agents/listing.json. Los hubs comprueban esta firma por sí mismos. Compruébala tú cuando llames a un vendedor directamente:

import { createHash, createPublicKey, verify } from "node:crypto";

const canonical = (v) => Array.isArray(v) ? `[${v.map(canonical).join(",")}]`
  : v && typeof v === "object" ? `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`
  : JSON.stringify(v ?? null);

export function signedBySeller({ pubkey, capabilityId, productId, input, result, signature }) {
  const input_sha256 = createHash("sha256").update(canonical(input)).digest("hex");
  const bytes = Buffer.from(canonical({ capability_id: capabilityId, product_id: productId, input_sha256, result }));
  const der = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(pubkey, "base64")]);
  return verify(null, bytes, createPublicKey({ key: der, format: "der", type: "spki" }), Buffer.from(signature, "base64"));
}

4. Compra

POST <order.url> con {"offer_id": "<de la consulta>"}. La primera respuesta es 402 Payment Required con las condiciones x402 v2: esquema exact, red eip155:8453 (Base), USDC, el precio de la oferta, a pagar al vendedor. Cualquier cliente x402 v2 lo paga. Tu monedero firma una autorización EIP-3009 y no paga gas. En TypeScript con los paquetes x402:

npm i @x402/fetch @x402/evm viem
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY);   // un monedero con USDC en Base

// Guarda la cabecera de pago que envía el envoltorio: un reintento debe llevar el mismo pago, no uno nuevo.
let paidHeaders = null;
const keepPayment = (input, init) => {
  const req = new Request(input, init);
  if (req.headers.has("PAYMENT-SIGNATURE")) paidHeaders = new Headers(req.headers);
  return fetch(req);
};
const payingFetch = wrapFetchWithPaymentFromConfig(keepPayment, {
  schemes: [{ network: "eip155:8453", client: new ExactEvmScheme(account) }],
});

export async function buyEsim(orderUrl, offerId) {
  const body = JSON.stringify({ offer_id: offerId });
  let res = await payingFetch(orderUrl, { method: "POST", headers: { "content-type": "application/json" }, body });
  // 503 allocation_pending: la eSIM está pedida pero aún no lista. El mismo pago, un poco más tarde.
  for (let i = 0; i < 6 && res.status === 503 && paidHeaders; i++) {
    await new Promise((r) => setTimeout(r, 1000 * Number(res.headers.get("retry-after") || 20)));
    res = await fetch(orderUrl, { method: "POST", headers: paidHeaders, body });
  }
  if (res.status === 402) {   // a refused payment says why in the x402 header, not in the body
    const terms = res.headers.get("payment-required");
    throw new Error(`402 ${terms ? JSON.parse(Buffer.from(terms, "base64").toString("utf8")).error : "payment required"}`);
  }
  const answer = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${answer.error}: ${answer.message}`);
  return answer.result;
}

Por qué se cobra. El vendedor verifica el pago, compra la eSIM y liquida el pago solo cuando la eSIM existe. Un pedido rechazado no cuesta nada. Un pedido que aún se prepara (503 allocation_pending) todavía no se cobra; el mismo pago enviado de nuevo recoge la misma eSIM, nunca una segunda. Un pago que no se liquida cancela la eSIM.

La respuesta:

{
  "success": true,
  "result": {
    "order_no": "B26101410460022",
    "esim_tran_no": "26101410460022",
    "iccid": "8985…",
    "activation_code": "LPA:1$…",
    "qr_code_url": "https://…",
    "install_url": "https://…",
    "apn": "internet",
    "expires": "2027-04-12T10:46:24+0000",
    "status_token": "…",
    "offer": { "offer_id": "TR_0.5_1", "name": "Turkey 500MB 1Day", "…": "…" }
  }
}

El código de activación es un secreto. Quien lo tenga puede instalar la eSIM. Dáselo, junto con el enlace al QR y el de instalación, solo al viajero. No lo registres en logs ni lo pongas en un chat compartido, un ticket o un evento de calendario. Guarda esim_tran_no y status_token para consultar la eSIM más tarde.

5. Errores

EstadoerrorSignificadoQué hacer
400unknown_offerEl offer_id no está (ya) en el catálogo.Consulta de nuevo.
402(condiciones x402)Hace falta pagar, o el pago fue rechazado: importe incorrecto, USDC insuficiente, caducado.Paga, o arregla el monedero.
403demo_modeUn nodo demo. No vende nada.Usa un vendedor que venda.
409supplier_<code>El mayorista rechazó el pedido: plan retirado, precio cambiado. No se te cobró.Consulta de nuevo.
503allocation_pendingPedido, aún no listo. Aún no cobrado.El mismo pago tras Retry-After segundos (ejemplo de arriba).
503catalogue_unavailable, payments_offEl vendedor no puede vender ahora.Otro vendedor, o más tarde.
500internal_errorEl pedido falló. No se te cobró.Más tarde, u otro vendedor.

6. Ayuda al viajero a instalarla

7. Consulta el consumo

curl -s -X POST https://<seller>/agents/status -H 'content-type: application/json' \
  -d '{"esim_tran_no":"26101410460022","status_token":"…"}'

La respuesta tiene state (el valor del mayorista: GOT_RESOURCE significa aún no instalada; otros son IN_USE, USED_UP y CANCEL), installed, data_used_bytes, data_total_bytes, activated, expires y de nuevo los campos de entrega. Un token incorrecto recibe 404 not_found, igual que una eSIM desconocida.

8. Buenas maneras

El kit en GitHub →